Can't have abstract method in a non-abstract class, despite method is implemented in trait

This question probably belongs on StackOverflow instead of here, because it is purely a programming-related question…

but I am facing a slew of Errors from what looks like a combo of traits and inheritance…

I have this class, called MemberLeadListPage, which is defined to be:

package com.signaturemd.pages.list

import static com.kms.katalon.core.testobject.ObjectRepository.findTestObject

import java.time.LocalDateTime

import org.openqa.selenium.By
import org.openqa.selenium.WebElement

import com.kms.katalon.core.model.FailureHandling
import com.kms.katalon.core.testobject.TestObject
import com.kms.katalon.core.webui.common.WebUiCommonHelper
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI
import com.signaturemd.utils.ActionHandler
import com.signaturemd.utils.GeneralWebUIUtils

public class MemberLeadListPage extends BasePersonListPage {
	public final String linkXpath = "//span[@title = 'Member Lead Name']/a",
		stageLabelXpath = "//span[contains(@data-component, '\"COLUMNNAME\":\"STAGE\"')]/span";
	
	public MemberLeadListPage() {
		super("https://crm.zoho.com/crm/org724168703/tab/Potentials/custom-view/4623170000000087545/canvas/4623170000000293236");
	}
	
	@Override
	public void deleteAllCreatedAfter(LocalDateTime time) {
		((this) as BaseSublistActor).doActionsToAllCreatedAfter(time, [
			{ List<TestObject> newlyCreatedRecords -> 
				newlyCreatedRecords
					.collect { TestObject to -> return WebUiCommonHelper.findWebElement(to, 1) }
					.each { WebElement rowElement ->
						if (WebUI.getText(getChildAsTestObject(rowElement, By.xpath(stageLabelXpath)))
							.equals("Open"))
							return;
						
						GeneralWebUIUtils.OpenLinkInNewTab(getChildAsTestObject(rowElement, By.xpath(linkXpath))) 
						
						WebUI.waitForPageLoad(5);
						
						WebUI.waitForElementPresent(findTestObject('Page_Member Lead/First Row/Member Lead Name') , 5)
						
						final TestObject openStageItem = findTestObject("Page_Member Lead/Second Row/Open step item")
						
						GeneralWebUIUtils.ScrollDropdownOptionIntoView(openStageItem)
						
						WebUI.click(openStageItem)
						
						GeneralWebUIUtils.WaitForElementTextMatches(findTestObject("Page_Member Lead/First Row/Stage label"), 
							"Open", 
							5)
						
						GeneralWebUIUtils.CloseLastTab()
					}
			},
			((this) as BaseSublistActor).onClickOnCheckboxes(),
			this.onMassDelete(),
		]);
		
	}
	
	private TestObject getChildAsTestObject(WebElement parentElement, By by) {
		return WebUI.convertWebElementToTestObject(parentElement.findElement(by));
	}
	
	@Override
	public TestObject getFirstResult() {
		return findTestObject("Page_Member Lead List/Member Table/First Result link");
	}

	@Override
	public TestObject getOrganizationFilterCheckbox() {
		return findTestObject('Page_Member Lead List/Filter Sidebar/Filter By Fields Subsection/Organization Filter/Organization checkbox')
	}

	@Override
	public TestObject getOrganizationFilterTextField() {
		return findTestObject('Page_Member Lead List/Filter Sidebar/Filter By Fields Subsection/Organization Filter/Organization text field');
	}

	@Override
	public String getTableRowListXpath() {
		return "${this.getTableDataXpath()}//tr[@data-zcqa = 'detailView']"
	}

	@Override
	public String getTableDataXpath() {
		return "//table[@data-zcqa = 'listViewTable']";
	}

	@Override
	public String getTableRowCheckboxXpath(int rowNum) {
		return "(${this.getTableDataXpath()}//span[@data-zcqa = 'selectEntity'])[${rowNum}]"
	}

	@Override
	public String getTimestampRelativeXpath() {
		return "//span[contains(@data-component, '\"COLUMNNAME\":\"CREATEDTIME\"')]";
	}
}

…and BasePersonListPage extends BaseListPage

…and BaseListPage implements SublistDeleter, the latter of which is defined to be:

package com.signaturemd.pages.list

import static com.kms.katalon.core.testobject.ObjectRepository.findTestObject

import java.time.LocalDateTime

import org.openqa.selenium.WebElement

import com.kms.katalon.core.testobject.TestObject
import com.kms.katalon.core.util.KeywordUtil
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI
import com.signaturemd.utils.GeneralWebUIUtils




public trait SublistDeleter extends BaseSublistActor {
	public void deleteAllCreatedAfter(LocalDateTime time) {
		this.doActionsToAllCreatedAfter(time, [
			this.onClickOnCheckboxes(),
			this.onMassDelete(),
		]);
	}
	
	public Closure onMassDelete() {
		return { List<TestObject> recordsToDelete -> 
			KeywordUtil.logInfo("Clicking the dropdown button and option to delete all selected records...");
			
			WebUI.click(findTestObject("Shared repository/Zoho pages/CRM/List pages/Actions dropdown button"))
	
			final TestObject deleteDropdownOption = findTestObject("Shared repository/Zoho pages/CRM/List pages/Actions dropdown/Delete dropdown option")
	
			WebUI.waitForElementVisible(deleteDropdownOption, 2)
	
			WebUI.click(deleteDropdownOption)
	
			WebUI.waitForElementNotVisible(deleteDropdownOption, 2)
	
			GeneralWebUIUtils.HandleConfirmableModal(findTestObject("Shared repository/Zoho pages/CRM/Modal/Modal"),
					findTestObject("Shared repository/Zoho pages/CRM/List pages/Modal/Delete button"),
					GeneralWebUIUtils.OnWaitForDisappear(WebUI.&waitForElementNotPresent, 4))
	
			KeywordUtil.logInfo("All selected records successfully deleted!");
		}
	}
}

The BaseSublistActor is defined to be:

package com.signaturemd.pages.list

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

import org.openqa.selenium.By
import org.openqa.selenium.WebElement

import com.kms.katalon.core.testobject.ConditionType
import com.kms.katalon.core.testobject.TestObject
import com.kms.katalon.core.util.KeywordUtil
import com.kms.katalon.core.webui.driver.DriverFactory
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI
import com.signaturemd.utils.SMDDateUtils

public trait BaseSublistActor {
	public static final DateTimeFormatter DateTimeFormat = DateTimeFormatter.ofPattern("MM-dd-yyyy h:mm a");
	
	public void doActionsToAllCreatedAfter(LocalDateTime time, List<Closure> onActionsList) {
		KeywordUtil.logInfo("Fetching all records created after or at ${time.toString()}...")

		final List<TestObject> allNewlyCreatedRecords = this.getAllCreatedAfter(time);
		if (allNewlyCreatedRecords.isEmpty()) {
			KeywordUtil.markWarning("There are no newly created records for this logged-in user...");
			return;
		}
		 
		onActionsList.each { Closure onAction -> 
			onAction(allNewlyCreatedRecords);
		};
	}
	
	public List<TestObject> getAllCreatedAfter(LocalDateTime time) {
		// Get the timezone offset of the user
		LocalDateTime earliestCreationTime = DriverFactory.getWebDriver()
				.findElements(By.xpath(this.getTableRowListXpath()))
				.stream()
				.map { WebElement rowElement ->
					return ((this) as BaseSublistActor).getCreatedTimeFromRowElement(rowElement)
				}
				.min { LocalDateTime firstTime, LocalDateTime secondTime ->
					return firstTime.compareTo(secondTime);
				}
				.orElse(LocalDateTime.now())

		final int timezoneDifference = SMDDateUtils.GetHoursDifference(time, earliestCreationTime);

		return DriverFactory.getWebDriver()
				.findElements(By.xpath(this.getTableRowListXpath()))
				.stream()
				.filter { WebElement rowElement ->
					final LocalDateTime createdTime = ((this) as BaseSublistActor).getCreatedTimeFromRowElement(rowElement);

					final LocalDateTime adjustedTime = time.minusHours(timezoneDifference);

					return createdTime.isAfter(adjustedTime) || createdTime.equals(adjustedTime);
				}
				.collect { WebElement rowElement -> return WebUI.convertWebElementToTestObject(rowElement) }
	}
	
	public String getTableRowListXpath() {
		return "${this.getTableDataXpath()}//lyte-exptable-tr";
	}

	public String getTableDataXpath() {
		return "//lyte-yield[@yield-name = 'contentYield']";
	}

	public LocalDateTime getCreatedTimeFromRowElement(WebElement rowElement) {
		return LocalDateTime.parse(rowElement
				.findElement(By.xpath(this.getTimestampRelativeXpath()))
				.getText(),
			this.DateTimeFormat);
	}

	public String getTimestampRelativeXpath() {
		return "//*[contains(concat(' ', @class, ' '), ' newDTField ')]";
	}
	
	public Closure onClickOnCheckboxes() { 
		return { List<TestObject> allNewlyCreatedRecords -> 
			allNewlyCreatedRecords
				.eachWithIndex { TestObject tableRow, int idx ->
					final int rowNum = idx + 1;
	
					KeywordUtil.logInfo("Scrolling to and clicking checkbox to delete for row #${rowNum}...")
	
					WebUI.scrollToElement(tableRow, 2);
	
					WebUI.click(getTableRowCheckbox(rowNum));
				}
		}
	}
	
	public TestObject getTableRowCheckbox(int rowNum) {
		return new TestObject("Shared repository/Zoho pages/CRM/List pages/Results Table/#${rowNum} table row checkbox")
			.addProperty("xpath",
				ConditionType.EQUALS,
				this.getTableRowCheckboxXpath(rowNum),
			)
	}

	public String getTableRowCheckboxXpath(int rowNum) {
		return "(${this.getTableDataXpath()}//*[@lt-prop-class = 'customCheckBox'])[${rowNum}]";
	}
}


How’s come the IDE doesn’t recognize the implemented methods on the derived class that are coming from the base trait BaseSublistActor, or even the concrete implementation of SublistDeleter.onMassDelete()?

UPDATE: Code updated as of March 24, 2023, to the current version in my code base

The screenshot you posted above does not show the whole error message text. I can not read it.

If I expanded it, the error messages would be off the screen

The error message is the starting point to investigate any issues. Without the full error message disclosed, it is difficult to study anything.

Description Resource Path Location Type
Groovy:Can’t have an abstract method in a non-abstract class. The class ‘com.signaturemd.pages.list.MemberLeadListPage’ must be declared abstract or the method ‘groovy.lang.Closure onMassDelete()’ must be implemented. MemberLeadListPage.groovy /C%%Users%Eliza%Desktop%Upwork jobs%SignatureMD%zoho-web-test-suite%Zoho Katalon Project%My First Web UI Project.prj/Keywords/com/signaturemd/pages/list line 17 Java Problem
Groovy:Method ‘getTableDataXpath’ from class ‘com.signaturemd.pages.list.MemberLeadListPage’ does not override method from its superclass or interfaces but is annotated with @Override. MemberLeadListPage.groovy /C%%Users%Eliza%Desktop%Upwork jobs%SignatureMD%zoho-web-test-suite%Zoho Katalon Project%My First Web UI Project.prj/Keywords/com/signaturemd/pages/list line 85 Java Problem
Groovy:Method ‘getTableRowCheckboxXpath’ from class ‘com.signaturemd.pages.list.MemberLeadListPage’ does not override method from its superclass or interfaces but is annotated with @Override. MemberLeadListPage.groovy /C%%Users%Eliza%Desktop%Upwork jobs%SignatureMD%zoho-web-test-suite%Zoho Katalon Project%My First Web UI Project.prj/Keywords/com/signaturemd/pages/list line 90 Java Problem
Groovy:Method ‘getTableRowListXpath’ from class ‘com.signaturemd.pages.list.MemberLeadListPage’ does not override method from its superclass or interfaces but is annotated with @Override. MemberLeadListPage.groovy /C%%Users%Eliza%Desktop%Upwork jobs%SignatureMD%zoho-web-test-suite%Zoho Katalon Project%My First Web UI Project.prj/Keywords/com/signaturemd/pages/list line 80 Java Problem
Groovy:Method ‘getTimestampRelativeXpath’ from class ‘com.signaturemd.pages.list.MemberLeadListPage’ does not override method from its superclass or interfaces but is annotated with @Override. MemberLeadListPage.groovy /C%%Users%Eliza%Desktop%Upwork jobs%SignatureMD%zoho-web-test-suite%Zoho Katalon Project%My First Web UI Project.prj/Keywords/com/signaturemd/pages/list line 95 Java Problem

Happy now?

Perhaps it’s the version of Groovy? Just a guess, I don’t keep abreast of those kind of details.

@duyluong does this error make sense to you?

it is not about making a comunity member happy here.
it is about how the comunity can make you happy.
without access to relevant logs and code, seriously, how would you expect some valuable help?

we will have to rely only on your eyes and fingers.
sounds fair to you?

This message advises us to read the source of MemberLeadListPage, which you already disclosed to us, as follows:

public class MemberLeadListPage extends BasePersonListPage {
    ...
}

I found that the MemberLeadListPage does not explicitly implement the onMassDelete method. So I guess that the BasePersonListPage class should implement the onMassDelete. You wrote:

…and BasePersonListPage extends BaseListPage
…and BaseListPage implements SublistDeleter, the latter of which is defined to be:

You disclosed the source of SublistDeleter which, yes, implements the onMassDelete().

However you haven’t disclosed the source of BaseListPage to us. Therefore we can not verify if your statement “BaseListPage implements SublistDeleter” is true or not.

I have a doubt: your BaseListPage class does NOT actually implement the SublistDeleter trait. Therefore the errors occurred.

BaseListPage references none of the concerns addressed by SublistDeleter or BaseSublistActor.
MemberLeadListPage hits none of the methods on BaseListPage.

Hence, we can MVCE this by simply having empty

public class BaseListPage implements SublistDeleter {

}

but that’s beside the point. Point is that, concrete method implementations are being provided in the traits, and either:

  • used in the derived classes, which may cause me to have to cast this to BaseSublistActor to use any derived methods that come from the BaseSublistActor (e.g. onClickOnCheckboxes()), or
  • we @Override those provided methods, and face errors over that (see the other four errors on the table), or even more plainly
  • the environment can’t seem to realize that the derived class that is using these concrete trait methods implements that trait…and I have a hypothesis about that…

When Googling for the error I am facing, I see that it has historically been faced in other IDEs. Something similar seem to have happened in Eclipse.

Let’s look into that last bullet point…

Perplexed by this, I reach out to a long-time de-facto programming mentor of mine, who happened to be former employee of parent company KMS (yes, the same KMS that created Katalon. No, he didn’t have a hand in creating that, he was working on Kobiton.) After looking into the issue, together, we found out that:

so it’s just like scala: helper class called from the implementing class
TraitFoo name will be munged to TraitFoo$Trait$Helper for the helper class name
which is an inner class to the interface TraitFoo that’s generated
and it generates a separate helper interface for fields, TraitFoo$Trait$FieldHelper

He cited this deep dive into Groovy compiler.

It looks like I shouldn’t use traits for this :sob:

Version of Groovy?

I am using Katalon v8.5.1 . When I search the plugins folder for groovy plugin, I see v2.4.20.

On second thought…why are we using traits anyways?

I use them to try to split up the god-class that I accidentally turned BaseListPage into…

It had 9 methods before I started putting sublist-deletion concerns on it… and then that method count literally doubled!

There were two options to handle this:

  • move the new sublist-deletion concerns to a base abstract class that BaseListPage will extend, or
  • move the new sublist-deletion concerns to a trait

At the time, I could see this concern popping up on a page that is NOT a BaseListPage. Hence, I thought, let’s make these into traits!

But thinking about the use case of this, right now, that is a total YAGNI. Even just reading off the names of the classes and traits, we should only concern ourselves with deleting a sublist of something on a page that lists that something off.

Hence, we should import this concern into only one place: the BaseListPage. Because that functionality only belongs there, we should switch to classes instead.

NOTE: This solved the issues I face (the errors), however it is still simply unacceptable that concrete trait methods don’t carry over to classes that (indirectly) implement the traits that define them, let alone the IDE not acknowledging this indirect trait implementation.

Hence, I’ll leave this question open until someone comes up with a way to solve this bug.

@mwarren04011990 Good post.

I edited your post to fix the broken “deep dive” link. You should check I did it ok.

1 Like

You did it ok