From bafccb33b567a0da7b5ac9051bfb7f0aab52f9b2 Mon Sep 17 00:00:00 2001 From: Nicolas Molina Monroy Date: Tue, 17 Dec 2024 16:44:27 -0400 Subject: [PATCH 1/3] fix(scss): fix error with hover (#30964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Parent Issue #30959 ### Proposed Changes This pull request includes changes to the `core-web/libs/dotcms-scss/shared/_colors.scss` file to improve the color accessibility and consistency in the codebase. Improvements to color accessibility: * Changed the value of `--color-palette-primary-op-10` to use `hsla` with a specific lightness and opacity, ensuring better color accessibility. ### Checklist - [x] Tests - [x] Translations - [x] Security Implications Contemplated (add notes if applicable) ### Screenshots Screenshot 2024-12-17 at 3 56 40 PM --- core-web/libs/dotcms-scss/shared/_colors.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core-web/libs/dotcms-scss/shared/_colors.scss b/core-web/libs/dotcms-scss/shared/_colors.scss index a1173a3e7d35..f5b5b416e768 100644 --- a/core-web/libs/dotcms-scss/shared/_colors.scss +++ b/core-web/libs/dotcms-scss/shared/_colors.scss @@ -236,7 +236,7 @@ $success: $color-accessible-text-green; --primary-800: var(--color-palette-primary-800); --primary-900: var(--color-palette-primary-900); - --color-palette-primary-op-10: var(--color-palette-primary-op-10); + --color-palette-primary-op-10: hsla(var(--color-primary-h), var(--color-primary-s), 60%, 0.1); --color-palette-primary-op-20: hsla(var(--color-primary-h), var(--color-primary-s), 60%, 0.2); --color-palette-primary-op-30: hsla(var(--color-primary-h), var(--color-primary-s), 60%, 0.3); --color-palette-primary-op-40: hsla(var(--color-primary-h), var(--color-primary-s), 60%, 0.4); From 1d026039a8590b25c63d77e8beaff16835299080 Mon Sep 17 00:00:00 2001 From: freddyDOTCMS <147462678+freddyDOTCMS@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:47:05 -0600 Subject: [PATCH 2/3] =?UTF-8?q?Fixing=20error=20when=20try=20to=20publish?= =?UTF-8?q?=20a=20Template=20and=20the=20Unqiue=20feilds=20dat=E2=80=A6=20?= =?UTF-8?q?(#30965)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Proposed Changes * RUn the afterPublish method just for Contentlet --- .../business/VersionableAPIImpl.java | 4 +-- .../templates/business/TemplateAPITest.java | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java index 243dffc331c8..199b0ab8c542 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java @@ -524,6 +524,8 @@ public void setLive ( final Versionable versionable ) throws DotDataException, D newInfo.setLiveInode(versionable.getInode()); newInfo.setPublishDate(new Date()); versionableFactory.saveContentletVersionInfo( newInfo, true ); + + uniqueFieldValidationStrategyResolver.get().afterPublish(versionable.getInode()); } else { final VersionInfo info = versionableFactory.getVersionInfo( versionable.getVersionId() ); @@ -534,8 +536,6 @@ public void setLive ( final Versionable versionable ) throws DotDataException, D info.setLiveInode( versionable.getInode() ); this.versionableFactory.saveVersionInfo( info, true ); } - - uniqueFieldValidationStrategyResolver.get().afterPublish(versionable.getInode()); } /** diff --git a/dotcms-integration/src/test/java/com/dotmarketing/portlets/templates/business/TemplateAPITest.java b/dotcms-integration/src/test/java/com/dotmarketing/portlets/templates/business/TemplateAPITest.java index 1c87d8c96076..09bdaf5689e0 100644 --- a/dotcms-integration/src/test/java/com/dotmarketing/portlets/templates/business/TemplateAPITest.java +++ b/dotcms-integration/src/test/java/com/dotmarketing/portlets/templates/business/TemplateAPITest.java @@ -12,6 +12,7 @@ import com.dotcms.IntegrationTestBase; import com.dotcms.api.web.HttpServletRequestThreadLocal; import com.dotcms.content.elasticsearch.ESQueryCache; +import com.dotcms.content.elasticsearch.business.ESContentletAPIImpl; import com.dotcms.contenttype.model.type.ContentType; import com.dotcms.datagen.*; import com.dotcms.rendering.velocity.viewtools.DotTemplateTool; @@ -63,6 +64,7 @@ import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import org.quartz.JobExecutionException; import javax.servlet.http.HttpServletRequest; @@ -317,6 +319,39 @@ public void publishTemplate_expects_live_true() throws Exception { assertTrue(templateSaved.isLive()); } + + /** + * Method to test: {@link TemplateAPIImpl#publishTemplate(Template, User, boolean)} + * When: Publish a Template with the UniqueField Database Validation set to true + * should: Template should be live true + */ + @Test + public void publishTemplateWithUniqueFieldDatbaseValidationEnabled() throws Exception { + final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); + + try { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(true); + final Host host = hostAPI.findDefaultHost(user, false); + final String body = " I'm mostly empty "; + final String title = "empty test template " + UUIDGenerator.generateUuid(); + final Template template = new Template(); + template.setTitle(title); + template.setBody(body); + final Template templateSaved = templateAPI.saveTemplate(template, host, user, false); + assertTrue(UtilMethods.isSet(templateSaved.getInode())); + assertTrue(UtilMethods.isSet(templateSaved.getIdentifier())); + assertEquals(templateSaved.getBody(), body); + assertEquals(templateSaved.getTitle(), title); + assertFalse(templateSaved.isLive()); + + templateAPI.publishTemplate(templateSaved, user, false); + assertTrue(templateSaved.isLive()); + } finally { + + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(oldEnabledDataBaseValidation); + } + } + /** * Method to test: unpublishTemplate * Given Scenario: Create a template, publish and unpublish it From 9bd0ce770b8ec63b69f453bc6ffd4083cd92e7b9 Mon Sep 17 00:00:00 2001 From: Bryan Date: Tue, 17 Dec 2024 21:43:30 -0600 Subject: [PATCH 3/3] Issue 30682 automation phase1 (#30946) ### Proposed Changes * Adding content editing test ### Checklist - [x] Tests --- .../frontend/locators/globalLocators.ts | 5 ++ .../tests/contentSearch/contentData.ts | 8 ++ .../contentSearch/contentEditing.spec.ts | 51 +++++++++++- .../contentSearch/portletIntegrity.spec.ts | 1 + .../frontend/utils/contentUtils.ts | 81 ++++++++++++++----- .../frontend/utils/dotCMSUtils.ts | 19 ++++- 6 files changed, 137 insertions(+), 28 deletions(-) diff --git a/e2e/dotcms-e2e-node/frontend/locators/globalLocators.ts b/e2e/dotcms-e2e-node/frontend/locators/globalLocators.ts index e613d4d2e616..f1a99a2e1959 100644 --- a/e2e/dotcms-e2e-node/frontend/locators/globalLocators.ts +++ b/e2e/dotcms-e2e-node/frontend/locators/globalLocators.ts @@ -33,6 +33,11 @@ export const contentGeneric = { label: "Content (Generic)" } +export const fileAsset = { + locator: "attach_fileFile Asset", + label: "File Asset" +} + export { } from './navigation/menuLocators'; diff --git a/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentData.ts b/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentData.ts index e7a44c8b7db4..57aa0c7d21ef 100644 --- a/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentData.ts +++ b/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentData.ts @@ -20,4 +20,12 @@ export const contentProperties = { deleteWfAction: "Delete" } +export const fileAssetContent = { + title: "File Asset title", + body: "This is a sample file asset content", + fromURL:"https://upload.wikimedia.org/wikipedia/commons/0/03/DotCMS-logo.svg", + newFileName:"New file asset", + newFileText:"This is a new file asset content", + host:"default" +} diff --git a/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentEditing.spec.ts b/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentEditing.spec.ts index e98b53a8e79e..4d52ecd95d1f 100644 --- a/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentEditing.spec.ts +++ b/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentEditing.spec.ts @@ -1,4 +1,4 @@ -import {expect, test} from '@playwright/test'; +import {expect, Page, test} from '@playwright/test'; import {dotCMSUtils, waitForVisibleAndCallback} from '../../utils/dotCMSUtils'; import { GroupEntriesLocators, @@ -6,8 +6,8 @@ import { ToolEntriesLocators } from '../../locators/navigation/menuLocators'; import {ContentUtils} from "../../utils/contentUtils"; -import {iFramesLocators, contentGeneric} from "../../locators/globalLocators"; -import {genericContent1, contentProperties} from "./contentData"; +import {iFramesLocators, contentGeneric, fileAsset} from "../../locators/globalLocators"; +import {genericContent1, contentProperties, fileAssetContent} from "./contentData"; import {assert} from "console"; const cmsUtils = new dotCMSUtils(); @@ -35,7 +35,9 @@ test.beforeEach('Navigate to content portlet', async ({page}) => { await waitForVisibleAndCallback(breadcrumbLocator, () => expect(breadcrumbLocator).toContainText('Search All')); }); - +/** + * test to add a new piece of content (generic content) + */ test('Add a new pice of content', async ({page}) => { const contentUtils = new ContentUtils(page); const iframe = page.frameLocator(iFramesLocators.main_iframe); @@ -43,6 +45,8 @@ test('Add a new pice of content', async ({page}) => { // Adding new rich text content await contentUtils.addNewContentAction(page, contentGeneric.locator, contentGeneric.label); await contentUtils.fillRichTextForm(page, genericContent1.title, genericContent1.body, contentProperties.publishWfAction); + await contentUtils.workflowExecutionValidationAndClose(page, 'Content saved'); + await waitForVisibleAndCallback(iframe.locator('#results_table tbody tr').first(), async () => {}); await contentUtils.validateContentExist(page, genericContent1.title).then(assert); @@ -73,5 +77,44 @@ test('Delete a piece of content', async ({ page }) => { } ); +/** + * 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 iframe = page.frameLocator(iFramesLocators.main_iframe).first(); + + await contentUtils.addNewContentAction(page, contentGeneric.locator, contentGeneric.label); + await contentUtils.fillRichTextForm(page, '', genericContent1.body, contentProperties.publishWfAction); + await expect(iframe.getByText('Error x')).toBeVisible(); + await expect(iframe.getByText('The field Title is required.')).toBeVisible(); +}); + +/** Please enable after fixing the issue #30748 + * Test to make sure we are validating the required of blockEditor fields on the content creation + */ +/** +test('Validate required on blockContent fields', async ({page}) => { + const contentUtils = new ContentUtils(page); + const iframe = page.frameLocator(iFramesLocators.main_iframe).first(); + + await contentUtils.addNewContentAction(page, contentGeneric.locator, contentGeneric.label); + await contentUtils.fillRichTextForm(page, genericContent1.title, '', contentProperties.publishWfAction); + await expect(iframe.getByText('Error x')).toBeVisible(); + await expect(iframe.getByText('The field Title is required.')).toBeVisible(); +}); +*/ +/** + * Test to validate you are able to add file assets importing from url + */ +test('Validate you are able to add file assets importing from url', async ({page}) => { + const contentUtils = new ContentUtils(page); + const iframe = page.frameLocator(iFramesLocators.main_iframe); + await contentUtils.addNewContentAction(page, fileAsset.locator, fileAsset.label); + await contentUtils.fillFileAssetForm(page, fileAssetContent.host, fileAssetContent.title, contentProperties.publishWfAction, null, fileAssetContent.fromURL ); + //fileName?: string, fromURL?: string, newFileName?: string, newFileText?: string) { + await contentUtils.workflowExecutionValidationAndClose(page, 'Content saved'); + await contentUtils.validateContentExist(page, fileAssetContent.title).then(assert); +}); \ No newline at end of file diff --git a/e2e/dotcms-e2e-node/frontend/tests/contentSearch/portletIntegrity.spec.ts b/e2e/dotcms-e2e-node/frontend/tests/contentSearch/portletIntegrity.spec.ts index 876ec9b7cf0c..5b9a55722856 100644 --- a/e2e/dotcms-e2e-node/frontend/tests/contentSearch/portletIntegrity.spec.ts +++ b/e2e/dotcms-e2e-node/frontend/tests/contentSearch/portletIntegrity.spec.ts @@ -55,6 +55,7 @@ test('Search filter', async ({page}) => { // Adding new rich text content await contentUtils.addNewContentAction(page, contentGeneric.locator, contentGeneric.label); await contentUtils.fillRichTextForm(page, genericContent1.title, genericContent1.body, contentProperties.publishWfAction); + await contentUtils.workflowExecutionValidationAndClose(page, 'Content saved'); // Validate the content has been created await expect.soft(iframe.getByRole('link', {name: genericContent1.title}).first()).toBeVisible(); diff --git a/e2e/dotcms-e2e-node/frontend/utils/contentUtils.ts b/e2e/dotcms-e2e-node/frontend/utils/contentUtils.ts index 267f9e4ad408..a12eff7db9b6 100644 --- a/e2e/dotcms-e2e-node/frontend/utils/contentUtils.ts +++ b/e2e/dotcms-e2e-node/frontend/utils/contentUtils.ts @@ -1,5 +1,5 @@ import {expect, FrameLocator, Locator, Page} from '@playwright/test'; -import {contentGeneric, iFramesLocators} from '../locators/globalLocators'; +import {contentGeneric, iFramesLocators, fileAsset } from '../locators/globalLocators'; import {waitForVisibleAndCallback} from './dotCMSUtils'; import {contentProperties} from "../tests/contentSearch/contentData"; @@ -27,14 +27,53 @@ export class ContentUtils { await dotIframe.locator('#title').fill(title); //Fill body await dotIframe.locator('#block-editor-body div').nth(1).fill(body); - - //await dotIframe.locator(iFramesLocators.wysiwygFrame).contentFrame().locator('#tinymce').fill(body); //Click on action await dotIframe.getByText(action).first().click(); - //Wait for the content to be saved + } + + /** + * Fill the file asset form + * @param page + * @param host + * @param title + * @param action + * @param fileName + * @param fromURL + * @param newFileName + * @param newFileText + */ + async fillFileAssetForm(page: Page, host: string, title: string, action:string, fileName?: string, fromURL?: string, newFileName?: string, newFileText?: string) { + const dotIframe = page.frameLocator(iFramesLocators.dot_iframe); + + const headingLocator = page.getByRole('heading'); + await waitForVisibleAndCallback(headingLocator, () => expect.soft(headingLocator).toContainText( fileAsset.label )); + + await dotIframe.locator('#HostSelector-hostFolderSelect').fill(host); + if (newFileName && newFileText) { + await dotIframe.getByTestId('editor-file-name').fill(newFileName); + await dotIframe.getByLabel('Editor content;Press Alt+F1').fill(newFileText); + await dotIframe.getByRole('button', { name: 'Save' }).click(); + } else { + if (fromURL) { + await dotIframe.getByRole('button', {name: ' Import from URL'}).click(); + await dotIframe.getByTestId('url-input').fill(fromURL); + await dotIframe.getByRole('button', { name: ' Import' }).click(); + } + } + const titleField = dotIframe.locator('#title'); + await waitForVisibleAndCallback(headingLocator, () => titleField.fill(title)); + await dotIframe.getByText(action).first().click(); + } - await expect(dotIframe.getByText('Content saved')).toBeVisible({timeout: 9000}); - await expect(dotIframe.getByText('Content saved')).toBeHidden(); + /** + * Validate the workflow execution and close the modal + * @param page + */ + async workflowExecutionValidationAndClose(page: Page, message: string) { + const dotIframe = page.frameLocator(iFramesLocators.dot_iframe); + + await expect(dotIframe.getByText(message)).toBeVisible({timeout: 9000}); + await expect(dotIframe.getByText(message)).toBeHidden(); //Click on close const closeBtnLocator = page.getByTestId('close-button').getByRole('button'); await waitForVisibleAndCallback(closeBtnLocator, () => closeBtnLocator.click()); @@ -48,16 +87,10 @@ export class ContentUtils { */ async addNewContentAction(page: Page, typeLocator: string, typeString: string) { const iframe = page.frameLocator(iFramesLocators.main_iframe); + const structureINodeLocator = iframe.locator('#structure_inode'); await waitForVisibleAndCallback(structureINodeLocator, () => expect(structureINodeLocator).toBeVisible()); - //TODO remove this - await page.waitForTimeout(1000); - const structureINodeDivLocator = iframe.locator('#widget_structure_inode div').first(); - await waitForVisibleAndCallback(structureINodeDivLocator, () => structureINodeDivLocator.click()); - //TODO remove this - await page.waitForTimeout(1000); - const typeLocatorByTextLocator = iframe.getByText(typeLocator); - await waitForVisibleAndCallback(typeLocatorByTextLocator, () => typeLocatorByTextLocator.click()); + await this.selectTypeOnFilter(page, typeLocator); await iframe.locator('#dijit_form_DropDownButton_0').click(); await expect(iframe.getByLabel('actionPrimaryMenu')).toBeVisible(); @@ -72,12 +105,13 @@ export class ContentUtils { * @param typeLocator * @param typeString */ - async selectTypeOnFilter(page: Page, typeLocator: string, typeString: string) { + async selectTypeOnFilter(page: Page, typeLocator: string) { const iframe = page.frameLocator(iFramesLocators.main_iframe); - await expect.soft(iframe.locator('#structure_inode')).toBeVisible(); - await iframe.locator('#widget_structure_inode div').first().click(); - await iframe.getByText(typeLocator).click(); + const structureINodeDivLocator = iframe.locator('#widget_structure_inode div').first(); + await waitForVisibleAndCallback(structureINodeDivLocator, () => structureINodeDivLocator.click()); + const typeLocatorByTextLocator = iframe.getByText(typeLocator); + await waitForVisibleAndCallback(typeLocatorByTextLocator, () => typeLocatorByTextLocator.click()); } /** @@ -151,6 +185,7 @@ export class ContentUtils { return; } await this.fillRichTextForm(page, newTitle, newBody, action); + await this.workflowExecutionValidationAndClose(page, 'Content saved'); } /** @@ -172,7 +207,6 @@ export class ContentUtils { await iframe.locator('#widget_showingSelect div').first().click(); const dropDownMenu = iframe.getByRole('option', { name: 'Archived' }); await waitForVisibleAndCallback(dropDownMenu, () => dropDownMenu.click()); - await page.waitForTimeout(1000) } else if (contentState === 'archived') { await this.performWorkflowAction(page, title, contentProperties.deleteWfAction); return; @@ -198,10 +232,15 @@ export class ContentUtils { } const actionBtnLocator = iframe.getByRole('menuitem', { name: action }); await waitForVisibleAndCallback(actionBtnLocator, () => actionBtnLocator.getByText(action).click()); - await expect.soft(iframe.getByText('Workflow executed')).toBeVisible(); - await expect.soft(iframe.getByText('Workflow executed')).toBeHidden(); + const executionConfirmation = iframe.getByText('Workflow executed'); + await waitForVisibleAndCallback(executionConfirmation, () => expect(executionConfirmation).toBeVisible()); + await waitForVisibleAndCallback(executionConfirmation, () => expect(executionConfirmation).toBeHidden()); } + /** + * Get the content state from the results table on the content portle + * @param page + */ async getContentState(page: Page, title: string): Promise { const iframe = page.frameLocator(iFramesLocators.main_iframe); await iframe.locator('#results_table tbody tr').first().waitFor({ state: 'visible' }); diff --git a/e2e/dotcms-e2e-node/frontend/utils/dotCMSUtils.ts b/e2e/dotcms-e2e-node/frontend/utils/dotCMSUtils.ts index d84c1bdb69dc..cc47cc1aa72a 100644 --- a/e2e/dotcms-e2e-node/frontend/utils/dotCMSUtils.ts +++ b/e2e/dotcms-e2e-node/frontend/utils/dotCMSUtils.ts @@ -7,8 +7,8 @@ export class dotCMSUtils { /** * Login to dotCMS * @param page - * @param username - * @param password + * @param username + * @param password */ async login(page: Page, username: string, password: string) { await page.goto('/dotAdmin'); @@ -34,16 +34,29 @@ export class dotCMSUtils { } }; - +/** + * 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 + */ 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); }; \ No newline at end of file