Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ui-test/src/main/java/base/BasePage.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
Expand Down Expand Up @@ -119,7 +120,7 @@ public boolean isElementVisible(WebElement element, String stepDesc) {
waitForElementVisible(element);
logStep(stepDesc + " - Verified visibility", element);
return element.isDisplayed();
} catch (NoSuchElementException e) {
} catch (NoSuchElementException | TimeoutException e) {
LOGGER.warn("Element not visible: {}", element);
ExtentReportManager.getTest().log(Status.WARNING, "Element not visible: " + describeElement(element));
return false;
Expand Down
75 changes: 55 additions & 20 deletions ui-test/src/main/java/base/BaseTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,34 @@ public static void beforeAll() {

CLIENT_CONFIG_MAP.put(PAR_TAG, new String[] { "$ID:CreateOIDCClient_par_required_Smoke_sid_clientId$",
"$CLIENT_ASSERTION_PAR_JWT_PAR_REQUIRED$" });

CLIENT_CONFIG_MAP.put("@TitleOnlyPurposeLogin",
new String[] { "$ID:CreateOIDCClient_with_title_only_purpose_login_Smoke_sid_clientId$",
"$CLIENT_ASSERTION_PAR_JWT_TITLE_ONLY_LOGIN$" });

CLIENT_CONFIG_MAP.put("@TitleOnlyPurposeVerify",
new String[] { "$ID:CreateOIDCClient_with_title_only_purpose_verify_Smoke_sid_clientId$",
"$CLIENT_ASSERTION_PAR_JWT_TITLE_ONLY_VERIFY$" });

CLIENT_CONFIG_MAP.put("@TitleOnlyPurposeLink",
new String[] { "$ID:CreateOIDCClient_with_title_only_purpose_link_Smoke_sid_clientId$",
"$CLIENT_ASSERTION_PAR_JWT_TITLE_ONLY_LINK$" });

CLIENT_CONFIG_MAP.put("@SubtitleOnlyPurposeLogin",
new String[] { "$ID:CreateOIDCClient_with_subtitle_only_purpose_login_Smoke_sid_clientId$",
"$CLIENT_ASSERTION_PAR_JWT_SUBTITLE_ONLY_LOGIN$" });

CLIENT_CONFIG_MAP.put("@SubtitleOnlyPurposeVerify",
new String[] { "$ID:CreateOIDCClient_with_subtitle_only_purpose_verify_Smoke_sid_clientId$",
"$CLIENT_ASSERTION_PAR_JWT_SUBTITLE_ONLY_VERIFY$" });

CLIENT_CONFIG_MAP.put("@SubtitleOnlyPurposeLink",
new String[] { "$ID:CreateOIDCClient_with_subtitle_only_purpose_link_Smoke_sid_clientId$",
"$CLIENT_ASSERTION_PAR_JWT_SUBTITLE_ONLY_LINK$" });

CLIENT_CONFIG_MAP.put("@UpdatedTitleAndSubTitle",
new String[] { "$ID:CreateOIDCClient_with_updated_title_and_subtitle_Smoke_sid_clientId$",
"$CLIENT_ASSERTION_PAR_JWT_UPDATED_TITLE$" });
}

// Runs before every other @Before hook (lowest order), unconditionally, so every scenario -
Expand Down Expand Up @@ -367,25 +395,14 @@ public void afterScenario(Scenario scenario) {
}

try {
if (scenario.isFailed()) {

failedCount++;
ExtentReportManager.incrementFailed();

// Use scenario name + failed step (fallback to scenario name if step unknown)
String failedStepName = scenario.getName().replaceAll("[^a-zA-Z0-9]", "_");

// Attach single screenshot when a driver exists
if (driver != null) {
ScreenshotUtil.attachScreenshot(driver, failedStepName);
} else {
ExtentReportManager.getTest().warning("Screenshot skipped because WebDriver was not initialized.");
}
String status = scenario.getStatus().toString();

ExtentReportManager.getTest().fail("❌ Scenario Failed: " + scenario.getName());

} else if (scenario.getStatus().toString().equalsIgnoreCase("SKIPPED")
&& runners.Runner.knownIssues.containsKey(scenario.getName())) {
// Cucumber's Scenario.isFailed() is true ONLY for status == FAILED - it does NOT cover
// UNDEFINED (unmatched step), PENDING, or AMBIGUOUS (duplicate step match). Branching on
// isFailed() let those statuses fall through to the "else" below and get reported as
// "✅ Scenario Passed", even though the scenario never actually ran to completion. Branch
// on status directly so anything that isn't PASSED or SKIPPED is treated as a failure.
if (status.equalsIgnoreCase("SKIPPED") && runners.Runner.knownIssues.containsKey(scenario.getName())) {

String bugId = runners.Runner.knownIssues.get(scenario.getName());
String bugUrl = "https://mosip.atlassian.net/browse/" + bugId;
Expand All @@ -394,15 +411,33 @@ public void afterScenario(Scenario scenario) {
ExtentReportManager.getTest().skip(
"🟠 Skipped due to Known Issue → <a href='" + bugUrl + "' target='_blank'>" + bugId + "</a>");

} else if (scenario.getStatus().toString().equalsIgnoreCase("SKIPPED")) {
} else if (status.equalsIgnoreCase("SKIPPED")) {

ExtentReportManager.incrementSkipped();
ExtentReportManager.getTest().skip("⚠️ Scenario Skipped: " + scenario.getName());

} else {
} else if (status.equalsIgnoreCase("PASSED")) {

passedCount++;
ExtentReportManager.incrementPassed();
ExtentReportManager.getTest().pass("✅ Scenario Passed: " + scenario.getName());

} else {

failedCount++;
ExtentReportManager.incrementFailed();

// Use scenario name + failed step (fallback to scenario name if step unknown)
String failedStepName = scenario.getName().replaceAll("[^a-zA-Z0-9]", "_");

// Attach single screenshot when a driver exists
if (driver != null) {
ScreenshotUtil.attachScreenshot(driver, failedStepName);
} else {
ExtentReportManager.getTest().warning("Screenshot skipped because WebDriver was not initialized.");
}

ExtentReportManager.getTest().fail("❌ Scenario " + status + ": " + scenario.getName());
}

ExtentReportManager.flushReport();
Expand Down
22 changes: 21 additions & 1 deletion ui-test/src/main/java/pages/ConsentPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,11 @@ public void checkTermsAndCondition() {
}

public void clickOnProceedButtonInTermsAndConditionPage() {
clickOnElement(proceedBtnInTandCPage, "Clicked on proceed button in terms and condition screen");
// Same reasoning as clickOnProceedButtonInServiceProviderPage(): this button sits right
// behind the terms-and-condition checkbox's onChange re-render, so it can still be
// disabled/covered for a moment after checkTermsAndCondition() clicks the box - a plain
// clickOnElement() (visibility only) races that and intermittently throws.
clickWhenClickable(proceedBtnInTandCPage);
}

public void clickOnProceedButtonInCameraPreviewPage() {
Expand Down Expand Up @@ -586,6 +590,22 @@ public String getLoginSubTitleText() {
return loginSubTitle.getText().trim();
}

/**
* The subtitle briefly renders with a placeholder value while Login.js's async purpose/language
* effects settle (its initial state seeds subheading from the title's translation key, not the
* subtitle's), before updating to the real text. A plain visibility-then-read can catch that
* placeholder, so poll for the expected substring instead of reading once.
*/
public boolean waitForLoginSubTitleToContain(String expectedSubstring) {
try {
new WebDriverWait(driver, Duration.ofSeconds(15))
.until(ExpectedConditions.textToBePresentInElement(loginSubTitle, expectedSubstring));
return true;
} catch (Exception e) {
return false;
}
}

public String getSelectPreferredModeHeaderText() {
waitForElementVisible(selectPreferredModeHeader);
return selectPreferredModeHeader.getText().trim();
Expand Down
155 changes: 155 additions & 0 deletions ui-test/src/main/java/pages/LoginOptionsPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ public LoginOptionsPage(WebDriver driver) {
@FindBy(id = "Otp_login_dropdown_button")
WebElement prefixNumberField;

@FindBy(id = "Password_login_dropdown_button")
WebElement passwordScreenMobilePrefixDropdownButton;

@FindBy(id = "KHM")
WebElement khmCountryCode;

Expand Down Expand Up @@ -122,12 +125,36 @@ public LoginOptionsPage(WebDriver driver) {
@FindBy(id = "Otp_email")
WebElement emailField;

@FindBy(id = "Password_mobile")
WebElement mobilePasswordField;

@FindBy(id = "Password_IND")
WebElement mobileNumberFieldInPasswordScreen;

@FindBy(id = "verify_password")
WebElement passwordWithLoginButton;

@FindBy(id = "Password_vid")
WebElement vidPasswordField;

@FindBy(id = "Password_email")
WebElement emailPasswordField;

@FindBy(id = "Password_login_dropdown_button")
WebElement prefixNumberFieldInPasswordScreen;

@FindBy(xpath = "//input[@id='vid' and @placeholder='Enter UIN/VID']")
WebElement vidFieldInPasswordScreen;

@FindBy(id = "sbi_vid")
WebElement biometricVidField;

@FindBy(id = "secure-biometric-interface-integration")
WebElement biometricIntegrationContainer;

@FindBy(id = "try_again")
WebElement networkErrorTryAgainButton;

public boolean isLogoDisplayed() {
return isElementVisible(brandLogo, "Verified is logo displayed");
}
Expand All @@ -140,6 +167,34 @@ public boolean isLanguageDropdownDisplayed() {
return isElementVisible(languageDropdown, "Verified language dropdown is visible");
}

/**
* The NetworkError page (see oidc-ui/src/pages/NetworkError.js) doesn't render a NavHeader,
* so it has no language dropdown at all - isLanguageDropdownDisplayed() correctly returns
* false there without needing a page-specific locator.
*/
public boolean isNetworkErrorScreenDisplayed() {
return isElementVisible(networkErrorTryAgainButton, "Verified network error screen is displayed");
}

/**
* NetworkError.js's tryAgain() calls window.location.replace(location.state.path) - a full
* browser navigation, not a React Router route change - so it doesn't happen automatically once
* the network is back. location.state.path is the URL of the last screen visited before going
* offline, captured by AppRouter's currentUrl effect; it identifies the login-mode-selection
* screen, not whichever login-option sub-view (e.g. the OTP identifier field) was open within it,
* since that's in-memory React state with no URL of its own. So the reload always lands back on
Comment thread
damodarguru marked this conversation as resolved.
Outdated
* mode selection ("Select a preferred mode to continue") - confirmed via the failure screenshot
* of an earlier attempt that waited on the OTP identifier field and never saw it appear. Wait on
* that screen's own button, with a longer, dedicated timeout since a full SPA reboot (re-fetches
* config/i18n) routinely takes longer than the suite's default explicit-wait - document.readyState
* turns "complete" as soon as the raw HTML loads, well before the SPA has actually re-rendered.
*/
public void clickTryAgainOnNetworkErrorScreen() {
clickOnElement(networkErrorTryAgainButton, "Clicked try again on the network error screen");
new WebDriverWait(driver, Duration.ofSeconds(30))
.until(ExpectedConditions.visibilityOf(loginWithOtpBtn));
}

public void clickOnLanguageDropdown() {
clickOnElement(languageDropdown, "Clicked on language dropdown");
}
Expand Down Expand Up @@ -232,6 +287,29 @@ public boolean isMobileNumberOptionDisplayed() {
return isElementVisible(mobileNumberOption, "Verified mobile number option is displayed for authentication");
}

public void clickOnMobileNumberOption() {
clickOnElement(mobileNumberOption, "Clicked on mobile number option");
}

public void clickOnPasswordScreenMobilePrefixDropdownButton() {
clickOnElement(passwordScreenMobilePrefixDropdownButton,
"Clicked on mobile prefix dropdown button on the password login screen");
}

/**
* Types text into the Mobile Number identifier field on the password login screen once the IND
* prefix is selected. InputWithPrefix.js gives this input the id "Password_" + the selected
* country label (e.g. "Password_IND"), not the "mobile" selector-button id - see
* mobileNumberFieldInPasswordScreen, already used elsewhere in this class for that same field.
*/
Comment thread
damodarguru marked this conversation as resolved.
Outdated
public void typeIntoMobileNumberFieldOnPasswordScreen(String text) {
mobileNumberFieldInPasswordScreen.sendKeys(text);
}

public String getMobileNumberFieldValueOnPasswordScreen() {
return mobileNumberFieldInPasswordScreen.getAttribute("value");
}

public boolean isNrcIdOptionDisplayed() {
return isElementVisible(nrcIdOption, "Verified nrc option is displayed for authentication");
}
Expand All @@ -244,6 +322,30 @@ public boolean isEmailOptionDisplayed() {
return isElementVisible(emailOption, "Verified email option is displayed for authentication");
}

/**
* Counts how many of the known login-ID-option buttons (mobile, nrc, vid,
* email) are currently rendered. Uses the non-waiting isElementDisplayed()
* check (not isMobileNumberOptionDisplayed() etc., which wait for
* visibility and would throw rather than return false for an option that
* is legitimately absent from the configured set).
*/
public int getDisplayedLoginIdOptionsCount() {
int count = 0;
if (isElementDisplayed(mobileNumberOption)) {
count++;
}
if (isElementDisplayed(nrcIdOption)) {
count++;
}
if (isElementDisplayed(vidOption)) {
count++;
}
if (isElementDisplayed(emailOption)) {
count++;
}
return count;
}

public boolean isBackButtonDisplayed() {
return isElementVisible(backButton, "Verified back button is visible for return to Select a preferred mode");
}
Expand Down Expand Up @@ -329,6 +431,59 @@ public void enterEmail(String email) {
enterText(emailField, email, "Entered email in email field");
}

public boolean isLoginHeaderIsDisplayed() {
return isElementVisible(loginHeader,
"Verified login header is displayed");
}

public boolean isLoginSubHeaderIsDisplayed() {
return isElementVisible(loginSubHeader,
"Verified invalid login sub header is displayed");
}

public boolean isSelectPreferredIdHeaderIsDisplayed() {
return isElementVisible(selectPreferredIdHeader,
"Verified select preferred id header is displayed");
}

public void enterPasswordForMobileId(String password) {
mobilePasswordField.clear();
enterText(mobilePasswordField, password, "Entered password in password field");
}

public void enterMobileNumberInPasswordScreen(String mobileNumber) {
waitForElementVisible(mobileNumberFieldInPasswordScreen);
mobileNumberFieldInPasswordScreen.clear();
enterText(mobileNumberFieldInPasswordScreen, mobileNumber, "Entered mobile number in mobile number field");
}

public void clickOnLoginButtonWithPasswordButton() {
clickOnElement(passwordWithLoginButton, "Clicked on password with login button");
}

public boolean isPasswordWithLoginButtonEnabled() {
return isButtonEnabled(passwordWithLoginButton, "Verified password with login button is enabled");
}

public void enterPasswordForVidId(String password) {
vidPasswordField.clear();
enterText(vidPasswordField, password, "Entered password in password field");
}

public void enterPasswordForEmailId(String password) {
emailPasswordField.clear();
enterText(emailPasswordField, password, "Entered password in password field");
}

public void clickOnPrefixNumberFieldButtonInPasswordScreen() {
clickOnElement(prefixNumberFieldInPasswordScreen, "Clicked on Prefix Number Field button");
}

public void enterVidInPasswordScreen(String vid) {
vidFieldInPasswordScreen.clear();
enterText(vidFieldInPasswordScreen, vid, "Entered vid in vid field");
}

public boolean isBiometricIntegrationContainerDisplayed() {
return isElementVisible(biometricIntegrationContainer,
"Verified secure biometric interface integration container is displayed");
Expand Down
5 changes: 5 additions & 0 deletions ui-test/src/main/java/pages/SignupFormDynamicFiller.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ public void fillFormFromUiSpec(Map<String, Map<String, Object>> uiSpecFields) th
} else if ("khm".equalsIgnoreCase(lang)) {
nameField.sendKeys(names.khmer);
RegisteredDetails.setFullName(names.khmer);
} else if ("ar".equalsIgnoreCase(lang) || "ara".equalsIgnoreCase(lang)) {
// The rest of this codebase isn't consistent between the 2-letter ("ar") and
// 3-letter ("ara") Arabic code - see ClaimsUtil's "ar"/"ara" case - so match
// both rather than gambling on the one this specific form actually renders.
nameField.sendKeys(names.arabic);
}
}
continue;
Expand Down
Loading