From cb72fba85944ceb249ed92b5ab50709e62158320 Mon Sep 17 00:00:00 2001 From: Bryan Date: Fri, 13 Dec 2024 10:47:58 -0600 Subject: [PATCH] Testing running test on pipeline after some configuration (#30932) ### Proposed Changes * Content Search Portlet integrity test * Content Editing tests ### Checklist - [x] Tests --- e2e/dotcms-e2e-node/frontend/.env | 2 +- .../frontend/locators/globalLocators.ts | 2 +- .../tests/contentSearch/contentData.ts | 12 +- .../contentSearch/contentEditing.spec.ts | 77 +++++++++ .../contentSearch/portletIntegrity.spec.ts | 16 +- .../frontend/utils/contentUtils.ts | 156 ++++++++++++++++-- .../frontend/utils/dotCMSUtils.ts | 27 +-- 7 files changed, 253 insertions(+), 39 deletions(-) create mode 100644 e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentEditing.spec.ts diff --git a/e2e/dotcms-e2e-node/frontend/.env b/e2e/dotcms-e2e-node/frontend/.env index 7da71e2d3fa8..7e3e1cd00aa6 100644 --- a/e2e/dotcms-e2e-node/frontend/.env +++ b/e2e/dotcms-e2e-node/frontend/.env @@ -2,7 +2,7 @@ CI=false DEV=false BASE_URL=http://localhost:8080 HEADLESS=false -RETRIES=1 +RETRIES=0 WORKERS=1 REUSE_SERVER=false INCLUDE_HTML=true diff --git a/e2e/dotcms-e2e-node/frontend/locators/globalLocators.ts b/e2e/dotcms-e2e-node/frontend/locators/globalLocators.ts index ef759cb80f46..e613d4d2e616 100644 --- a/e2e/dotcms-e2e-node/frontend/locators/globalLocators.ts +++ b/e2e/dotcms-e2e-node/frontend/locators/globalLocators.ts @@ -28,7 +28,7 @@ export const addContent = { /** * Locators for the Rich Text functionality. */ -export const richText = { +export const contentGeneric = { locator: "articleContent (Generic)", label: "Content (Generic)" } diff --git a/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentData.ts b/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentData.ts index f57860e32692..e7a44c8b7db4 100644 --- a/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentData.ts +++ b/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentData.ts @@ -1,9 +1,11 @@ /** * Content to add a Rich Text content */ -export const richTextContent = { +export const genericContent1 = { title: "Automation Test", - body: "This is a sample content" + body: "This is a sample content", + newTitle : "Automation Test edited", + newBody : "This is a sample content edited" } /** @@ -11,7 +13,11 @@ export const richTextContent = { */ export const contentProperties = { language: "English (US)", - publishWfAction: "Publish" + publishWfAction: "Publish", + unpublishWfAction: "Unpublish", + unlockWfAction: "Unlock", + archiveWfAction: "Archive", + deleteWfAction: "Delete" } diff --git a/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentEditing.spec.ts b/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentEditing.spec.ts new file mode 100644 index 000000000000..e98b53a8e79e --- /dev/null +++ b/e2e/dotcms-e2e-node/frontend/tests/contentSearch/contentEditing.spec.ts @@ -0,0 +1,77 @@ +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 {iFramesLocators, contentGeneric} from "../../locators/globalLocators"; +import {genericContent1, contentProperties} from "./contentData"; +import {assert} from "console"; + +const cmsUtils = new dotCMSUtils(); + +/** + * 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); + + // 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); + + // Validate the portlet title + const breadcrumbLocator = page.locator('p-breadcrumb'); + await waitForVisibleAndCallback(breadcrumbLocator, () => expect(breadcrumbLocator).toContainText('Search All')); +}); + + +test('Add a new pice of content', async ({page}) => { + const contentUtils = new ContentUtils(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, genericContent1.title, genericContent1.body, contentProperties.publishWfAction); + await waitForVisibleAndCallback(iframe.locator('#results_table tbody tr').first(), async () => {}); + + await contentUtils.validateContentExist(page, genericContent1.title).then(assert); +}); + +/** + * Test to edit an existing piece of content + */ +test('Edit a piece of content', async ({page}) => { + const contentUtils = new ContentUtils(page); + const iframe = page.frameLocator(iFramesLocators.main_iframe); + + // Edit the content + await contentUtils.editContent(page, genericContent1.title, genericContent1.newTitle, genericContent1.newBody, contentProperties.publishWfAction); + await waitForVisibleAndCallback(iframe.locator('#results_table tbody tr').first(), async () => {}); + + await contentUtils.validateContentExist(page, genericContent1.newTitle).then(assert); +}); + + +/** + * Test to delete an existing piece of content + */ +test('Delete a piece of content', async ({ page }) => { + const contentUtils = new ContentUtils(page); + // Delete the content + await contentUtils.deleteContent(page, genericContent1.newTitle); + } +); + + + 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 25861a57fffa..876ec9b7cf0c 100644 --- a/e2e/dotcms-e2e-node/frontend/tests/contentSearch/portletIntegrity.spec.ts +++ b/e2e/dotcms-e2e-node/frontend/tests/contentSearch/portletIntegrity.spec.ts @@ -1,13 +1,13 @@ import {expect, test} from '@playwright/test'; import {dotCMSUtils, waitForVisibleAndCallback} from '../../utils/dotCMSUtils'; import {ContentUtils} from '../../utils/contentUtils'; -import {addContent, iFramesLocators, richText} from '../../locators/globalLocators'; +import {addContent, iFramesLocators, contentGeneric} from '../../locators/globalLocators'; import { GroupEntriesLocators, MenuEntriesLocators, ToolEntriesLocators } from '../../locators/navigation/menuLocators'; -import {contentProperties, richTextContent} from './contentData'; +import {contentProperties, genericContent1} from './contentData'; const cmsUtils = new dotCMSUtils(); @@ -32,8 +32,6 @@ test.beforeEach('Navigate to content portlet', async ({page}) => { // Validate the portlet title const breadcrumbLocator = page.locator('p-breadcrumb'); await waitForVisibleAndCallback(breadcrumbLocator, () => expect(breadcrumbLocator).toContainText('Search All')); - - await expect(page.locator('p-breadcrumb')).toContainText('Search All'); }); @@ -55,16 +53,16 @@ test('Search filter', async ({page}) => { const iframe = page.frameLocator(iFramesLocators.main_iframe); // Adding new rich text content - await contentUtils.addNewContentAction(page, richText.locator, richText.label); - await contentUtils.fillRichTextForm(page, richTextContent.title, richTextContent.body, contentProperties.publishWfAction); + await contentUtils.addNewContentAction(page, contentGeneric.locator, contentGeneric.label); + await contentUtils.fillRichTextForm(page, genericContent1.title, genericContent1.body, contentProperties.publishWfAction); // Validate the content has been created - await expect.soft(iframe.getByRole('link', {name: 'Automation Test'}).first()).toBeVisible(); - await iframe.locator('#allFieldTB').fill(richTextContent.title); + await expect.soft(iframe.getByRole('link', {name: genericContent1.title}).first()).toBeVisible(); + await iframe.locator('#allFieldTB').fill(genericContent1.title); await page.keyboard.press('Enter'); //validate the search filter is working - await expect(iframe.getByRole('link', {name: 'Automation Test'}).first()).toBeVisible(); + await expect(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 b0e6d154f25f..267f9e4ad408 100644 --- a/e2e/dotcms-e2e-node/frontend/utils/contentUtils.ts +++ b/e2e/dotcms-e2e-node/frontend/utils/contentUtils.ts @@ -1,16 +1,18 @@ -import {Page, expect, FrameLocator} from '@playwright/test'; -import { iFramesLocators, richText } from '../locators/globalLocators'; -import { waitForVisibleAndCallback} from './dotCMSUtils'; +import {expect, FrameLocator, Locator, Page} from '@playwright/test'; +import {contentGeneric, iFramesLocators} from '../locators/globalLocators'; +import {waitForVisibleAndCallback} from './dotCMSUtils'; +import {contentProperties} from "../tests/contentSearch/contentData"; export class ContentUtils { page: Page; + constructor(page: Page) { - this.page = page; + this.page = page; } /** * Fill the rich text form - * @param page + * @param page * @param title * @param body * @param action @@ -19,7 +21,7 @@ export class ContentUtils { const dotIframe = page.frameLocator(iFramesLocators.dot_iframe); const headingLocator = page.getByRole('heading'); - await waitForVisibleAndCallback(headingLocator, () => expect.soft(headingLocator).toContainText(richText.label)); + await waitForVisibleAndCallback(headingLocator, () => expect.soft(headingLocator).toContainText(contentGeneric.label)); //Fill title await dotIframe.locator('#title').fill(title); @@ -28,10 +30,10 @@ export class ContentUtils { //await dotIframe.locator(iFramesLocators.wysiwygFrame).contentFrame().locator('#tinymce').fill(body); //Click on action - await dotIframe.getByText(action).click(); + await dotIframe.getByText(action).first().click(); //Wait for the content to be saved - await expect(dotIframe.getByText('Content saved')).toBeVisible({ timeout: 9000 }); + await expect(dotIframe.getByText('Content saved')).toBeVisible({timeout: 9000}); await expect(dotIframe.getByText('Content saved')).toBeHidden(); //Click on close const closeBtnLocator = page.getByTestId('close-button').getByRole('button'); @@ -82,17 +84,147 @@ export class ContentUtils { * Show query on the content portlet * @param iframe */ - async showQuery(iframe : FrameLocator) { - const createOptionsBtnLocator = iframe.getByRole('button', { name: 'createOptions' }); + async showQuery(iframe: FrameLocator) { + const createOptionsBtnLocator = iframe.getByRole('button', {name: 'createOptions'}); await waitForVisibleAndCallback(createOptionsBtnLocator, () => createOptionsBtnLocator.click()); //Validate the search button has a sub-menu - await expect (iframe.getByLabel('Search ▼').getByText('Search')).toBeVisible(); - await expect (iframe.getByText('Show Query')).toBeVisible(); + await expect(iframe.getByLabel('Search ▼').getByText('Search')).toBeVisible(); + await expect(iframe.getByText('Show Query')).toBeVisible(); // Click on show query await iframe.getByText('Show Query').click(); } + + /** + * Validate if the content exists in the results table on the content portlet + * @param page + * @param title + */ + async validateContentExist(page: Page, title: string) { + const iframe = page.frameLocator(iFramesLocators.main_iframe); + + await iframe.locator('#results_table tbody tr').first().waitFor({ state: 'visible' }); + const secondCell = iframe.locator('#results_table tbody tr:nth-of-type(2) td:nth-of-type(2)'); + const hasAutomationLink = await secondCell.locator(`a:has-text("${title}")`).count() > 0; + + console.log(`The content with the title ${title} ${hasAutomationLink ? 'exists' : 'does not exist'}`); + return hasAutomationLink; + } + + /** + * Get the content element from the results table on the content portlet + * @param page + * @param title + */ + async getContentElement(page: Page, title: string): Promise { + const iframe = page.frameLocator(iFramesLocators.main_iframe); + + await iframe.locator('#results_table tbody tr').first().waitFor({ state: 'visible' }); + const secondCell = iframe.locator('#results_table tbody tr:nth-of-type(2) td:nth-of-type(2)'); + const element = secondCell.locator(`a:has-text("${title}")`); + + const elementCount = await element.count(); + if (elementCount > 0) { + return element.first(); + } else { + console.log(`The content with the title ${title} does not exist`); + return null; + } + } + + /** + * Edit content on the content portlet + * @param page + * @param title + * @param newTitle + * @param newBody + * @param action + */ + async editContent(page: Page, title: string, newTitle: string, newBody: string, action: string) { + const iframe = page.frameLocator(iFramesLocators.main_iframe); + const contentElement = await this.getContentElement(page, title); + if (contentElement) { + await contentElement.click(); + }else { + console.log('Content not found'); + return; + } + await this.fillRichTextForm(page, newTitle, newBody, action); + } + + /** + * Delete content on the content portlet + * @param page + * @param title + */ + async deleteContent(page: Page, title: string) { + const iframe = page.frameLocator(iFramesLocators.main_iframe); + + while (await this.getContentState(page, title) !== null) { + const contentState = await this.getContentState(page, title); + + if (contentState === 'published') { + await this.performWorkflowAction(page, title, contentProperties.unpublishWfAction); + } else if (contentState === 'draft') { + await this.performWorkflowAction(page, title, contentProperties.archiveWfAction); + await iframe.getByRole('link', { name: 'Advanced' }).click(); + 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; + } + + await page.waitForLoadState(); + } + } + + /** + * Perform workflow action for some specific content + * @param page + * @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); + if (contentElement) { + await contentElement.click({ + button: 'right' + }); + } + 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(); + } + + 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' }); + + const titleCell = iframe.locator('#results_table tbody tr:nth-of-type(2) td:nth-of-type(2)'); + const element = titleCell.locator(`a:has-text("${title}")`); + const elementCount = await element.count(); + if (elementCount > 0) { + const stateColumn = iframe.locator('#results_table tbody tr:nth-of-type(2) td:nth-of-type(3)'); + const targetDiv = stateColumn.locator('div#icon'); + return await targetDiv.getAttribute('class'); + } else { + console.log('Content not found'); + return null; + } + } + + } + + + + + diff --git a/e2e/dotcms-e2e-node/frontend/utils/dotCMSUtils.ts b/e2e/dotcms-e2e-node/frontend/utils/dotCMSUtils.ts index 6138a5702d55..d84c1bdb69dc 100644 --- a/e2e/dotcms-e2e-node/frontend/utils/dotCMSUtils.ts +++ b/e2e/dotcms-e2e-node/frontend/utils/dotCMSUtils.ts @@ -1,19 +1,6 @@ import { Page, expect, Locator } from '@playwright/test'; import { loginLocators } from '../locators/globalLocators'; -export const waitFor = async (locator: Locator, state: "attached" | "detached" | "visible" | "hidden"): Promise => { - await locator.waitFor({state: state}); -} - -export const waitForAndCallback = async (locator: Locator, state: "attached" | "detached" | "visible" | "hidden", callback: () => Promise): Promise => { - await waitFor(locator, state); - await callback(); -}; - -export const waitForVisibleAndCallback = async (locator: Locator, callback: () => Promise): Promise => { - await waitForAndCallback(locator, 'visible', callback); -}; - export class dotCMSUtils { page: Page; @@ -45,4 +32,18 @@ export class dotCMSUtils { await group.click(); await tool.click(); } +}; + + +export const waitFor = async (locator: Locator, state: "attached" | "detached" | "visible" | "hidden"): Promise => { + await locator.waitFor({state: state}); +} + +export const waitForAndCallback = async (locator: Locator, state: "attached" | "detached" | "visible" | "hidden", callback: () => Promise): Promise => { + await waitFor(locator, state); + await callback(); +}; + +export const waitForVisibleAndCallback = async (locator: Locator, callback: () => Promise): Promise => { + await waitForAndCallback(locator, 'visible', callback); }; \ No newline at end of file