diff --git a/datahub-web-react/src/alchemy-components/components/Modal/Modal.tsx b/datahub-web-react/src/alchemy-components/components/Modal/Modal.tsx index 006dbac611..04bb6d9aea 100644 --- a/datahub-web-react/src/alchemy-components/components/Modal/Modal.tsx +++ b/datahub-web-react/src/alchemy-components/components/Modal/Modal.tsx @@ -50,7 +50,9 @@ const ButtonsContainer = styled.div` export interface ModalButton extends ButtonProps { text: string; + key?: string; onClick: () => void; + buttonDataTestId?: string; } export interface ModalProps { @@ -78,6 +80,7 @@ export function Modal({ onCancel={onCancel} closeIcon={} hasChildren={!!children} + data-testid={dataTestId} title={ @@ -93,10 +96,10 @@ export function Modal({ footer={ !!buttons.length && ( - {buttons.map(({ text, variant, onClick, ...buttonProps }, index) => ( + {buttons.map(({ text, variant, onClick, key, buttonDataTestId, ...buttonProps }, index) => ( diff --git a/datahub-web-react/src/app/tags/CreateNewTagModal/CreateNewTagModal.tsx b/datahub-web-react/src/app/tags/CreateNewTagModal/CreateNewTagModal.tsx index 8cc5d63a95..a2add3cbc4 100644 --- a/datahub-web-react/src/app/tags/CreateNewTagModal/CreateNewTagModal.tsx +++ b/datahub-web-react/src/app/tags/CreateNewTagModal/CreateNewTagModal.tsx @@ -2,10 +2,11 @@ import { Modal } from '@components'; import { message } from 'antd'; import React, { useState } from 'react'; +import { ModalButton } from '@components/components/Modal/Modal'; + import { useEnterKeyListener } from '@app/shared/useEnterKeyListener'; import OwnersSection, { PendingOwner } from '@app/sharedV2/owners/OwnersSection'; import TagDetailsSection from '@app/tags/CreateNewTagModal/TagDetailsSection'; -import { ModalButton } from '@app/tags/CreateNewTagModal/types'; import { useBatchAddOwnersMutation, useSetTagColorMutation } from '@graphql/mutations.generated'; import { useCreateTagMutation } from '@graphql/tag.generated'; @@ -120,6 +121,7 @@ const CreateNewTagModal: React.FC = ({ onClose, open }) color: 'violet', variant: 'text', onClick: onClose, + buttonDataTestId: 'create-tag-modal-cancel-button', }, { text: 'Create', @@ -129,6 +131,7 @@ const CreateNewTagModal: React.FC = ({ onClose, open }) onClick: onOk, disabled: !tagName || isLoading, isLoading, + buttonDataTestId: 'create-tag-modal-create-button', }, ]; diff --git a/datahub-web-react/src/app/tags/CreateNewTagModal/TagDetailsSection.tsx b/datahub-web-react/src/app/tags/CreateNewTagModal/TagDetailsSection.tsx index e802b2a9f0..7221a6c0f8 100644 --- a/datahub-web-react/src/app/tags/CreateNewTagModal/TagDetailsSection.tsx +++ b/datahub-web-react/src/app/tags/CreateNewTagModal/TagDetailsSection.tsx @@ -59,6 +59,7 @@ const TagDetailsSection: React.FC = ({ value={tagName} setValue={handleTagNameChange} placeholder="Enter tag name" + data-testid="tag-name-field" required /> @@ -69,6 +70,7 @@ const TagDetailsSection: React.FC = ({ value={tagDescription} setValue={handleDescriptionChange} placeholder="Add a description for your new tag" + data-testid="tag-description-field" type="textarea" /> diff --git a/datahub-web-react/src/app/tags/CreateNewTagModal/types.ts b/datahub-web-react/src/app/tags/CreateNewTagModal/types.ts index 158b572bec..d8ff0bb9b7 100644 --- a/datahub-web-react/src/app/tags/CreateNewTagModal/types.ts +++ b/datahub-web-react/src/app/tags/CreateNewTagModal/types.ts @@ -1,14 +1,3 @@ -// Interface for modal buttons matching the expected ButtonProps -export interface ModalButton { - text: string; - color: 'violet' | 'white' | 'black' | 'green' | 'red' | 'blue' | 'yellow' | 'gray'; - variant: 'text' | 'filled' | 'outline'; - onClick: () => void; - id?: string; - disabled?: boolean; - isLoading?: boolean; -} - // Common styled components export const FormSection = { marginBottom: '16px', diff --git a/datahub-web-react/src/app/tags/ManageTag.tsx b/datahub-web-react/src/app/tags/ManageTag.tsx index 725b42f81a..5ee45cb86e 100644 --- a/datahub-web-react/src/app/tags/ManageTag.tsx +++ b/datahub-web-react/src/app/tags/ManageTag.tsx @@ -1,8 +1,10 @@ -import { ButtonProps, ColorPicker, Input, Modal } from '@components'; +import { ColorPicker, Input, Modal } from '@components'; import { message } from 'antd'; import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; +import { ModalButton } from '@components/components/Modal/Modal'; + import OwnersSection from '@app/sharedV2/owners/OwnersSection'; import { useEntityRegistry } from '@src/app/useEntityRegistry'; import { @@ -24,12 +26,6 @@ interface Props { isModalOpen?: boolean; } -// Define a compatible interface for modal buttons -interface ModalButton extends ButtonProps { - text: string; - onClick: () => void; -} - // Interface for pending owner interface PendingOwner { ownerUrn: string; @@ -239,6 +235,7 @@ const ManageTag = ({ tagUrn, onClose, onSave, isModalOpen = false }: Props) => { variant: 'filled', onClick: handleSave, disabled: !hasChanges(), + buttonDataTestId: 'update-tag-button', }, ]; @@ -251,7 +248,15 @@ const ManageTag = ({ tagUrn, onClose, onSave, isModalOpen = false }: Props) => { const modalTitle = tagDisplayName ? `Edit Tag: ${tagDisplayName}` : 'Edit Tag'; return ( - +
{ setValue={handleDescriptionChange} placeholder="Tag description" type="textarea" + data-testid="tag-description-field" /> diff --git a/datahub-web-react/src/app/tags/ManageTags.tsx b/datahub-web-react/src/app/tags/ManageTags.tsx index 2c1c8ac7a3..e00a9c106f 100644 --- a/datahub-web-react/src/app/tags/ManageTags.tsx +++ b/datahub-web-react/src/app/tags/ManageTags.tsx @@ -145,6 +145,7 @@ const ManageTags = () => { size="md" color="violet" icon={{ icon: 'Plus', source: 'phosphor' }} + data-testid="add-tag-button" > Create Tag diff --git a/datahub-web-react/src/app/tags/TagsTable.tsx b/datahub-web-react/src/app/tags/TagsTable.tsx index eca9df34d5..9db9e5647c 100644 --- a/datahub-web-react/src/app/tags/TagsTable.tsx +++ b/datahub-web-react/src/app/tags/TagsTable.tsx @@ -233,6 +233,7 @@ const TagsTable = ({ searchQuery, searchData, loading: propLoading, networkStatu color: 'red', variant: 'filled', onClick: handleDeleteTag, + buttonDataTestId: 'delete-tag-button', }, ]} > diff --git a/datahub-web-react/src/app/tags/TagsTableColumns.tsx b/datahub-web-react/src/app/tags/TagsTableColumns.tsx index ea50901f76..faf0fa21e0 100644 --- a/datahub-web-react/src/app/tags/TagsTableColumns.tsx +++ b/datahub-web-react/src/app/tags/TagsTableColumns.tsx @@ -290,7 +290,7 @@ export const TagActionsColumn = React.memo( return ( - + ); diff --git a/smoke-test/tests/cypress/cypress/e2e/manage_tags/manage_tags.js b/smoke-test/tests/cypress/cypress/e2e/manage_tags/manage_tags.js index afd85bfb67..b485ed82f1 100644 --- a/smoke-test/tests/cypress/cypress/e2e/manage_tags/manage_tags.js +++ b/smoke-test/tests/cypress/cypress/e2e/manage_tags/manage_tags.js @@ -1,6 +1,10 @@ describe("manage tags", () => { - it("Manage Tags Page - Verify search bar placeholder", () => { + beforeEach(() => { + cy.setIsThemeV2Enabled(false); cy.login(); + }); + + it("Manage Tags Page - Verify search bar placeholder", () => { cy.visit("/tags"); cy.get('[data-testid="tag-search-input"]').should( "have.attr", @@ -8,8 +12,8 @@ describe("manage tags", () => { "Search tags...", ); }); + it("Manage Tags Page - Verify Title, Search, and Results", () => { - cy.login(); cy.visit("/tags"); cy.get('[data-testid="page-title"]').should("contain.text", "Manage Tags"); cy.get('[data-testid="urn:li:tag:Cypress-name"]').should( @@ -22,15 +26,15 @@ describe("manage tags", () => { "Cypress", ); }); + it("Manage Tags Page - Verify search not exists", () => { - cy.login(); cy.visit("/tags"); cy.get('[data-testid="page-title"]').should("contain.text", "Manage Tags"); cy.get('[data-testid="urn:li:tag:Cypress-name"]').should( "contain.text", "Cypress", ); - cy.get('[data-testid="tag-search-input"]').type("test"); + cy.get('[data-testid="tag-search-input"]').type("invalidvalue"); cy.get('[data-testid="tags-not-found"]').should( "contain.text", "No tags found for your search query", diff --git a/smoke-test/tests/cypress/cypress/e2e/manage_tagsV2/helpers/dataset_helper.js b/smoke-test/tests/cypress/cypress/e2e/manage_tagsV2/helpers/dataset_helper.js new file mode 100644 index 0000000000..fc5099af0b --- /dev/null +++ b/smoke-test/tests/cypress/cypress/e2e/manage_tagsV2/helpers/dataset_helper.js @@ -0,0 +1,53 @@ +export default class DatasetHelper { + static openDataset(urn, name) { + cy.goToDataset(urn, name); + } + + static assignTag(name) { + cy.get("#entity-profile-tags").within(() => { + cy.clickOptionWithTestId("AddRoundedIcon"); + }); + + cy.getWithTestId("tag-term-modal-input").within(() => { + cy.get("input").focus({ force: true }).type(name); + }); + + cy.get(`[name="${name}"]`).click(); + cy.clickOptionWithTestId("add-tag-term-from-modal-btn"); + cy.waitTextVisible("Added Tags!"); + } + + static ensureTagIsAssigned(name) { + cy.getWithTestId(`tag-${name}`).should("be.visible"); + } + + static unassignTag(name) { + cy.getWithTestId(`tag-${name}`).within(() => { + cy.get(".ant-tag-close-icon").click(); + }); + + cy.get(".ant-modal-confirm-confirm").within(() => { + cy.get(".ant-btn-primary").click(); + }); + + cy.waitTextVisible("Removed Tag!"); + } + + static ensureTagIsNotAssigned(name) { + cy.getWithTestId(`tag-${name}`).should("not.exist"); + } + + static searchByTag(tagName) { + cy.visit( + `/search?filter_tags___false___EQUAL___0=urn%3Ali%3Atag%3A${tagName}&page=1&query=%2A&unionType=0`, + ); + } + + static ensureEntityIsInSearchResults(urn) { + cy.getWithTestId(`preview-${urn}`).should("be.visible"); + } + + static ensureEntityIsNotInSearchResults(urn) { + cy.getWithTestId(`preview-${urn}`).should("not.exist"); + } +} diff --git a/smoke-test/tests/cypress/cypress/e2e/manage_tagsV2/helpers/tags_page_helper.js b/smoke-test/tests/cypress/cypress/e2e/manage_tagsV2/helpers/tags_page_helper.js new file mode 100644 index 0000000000..f77d67543b --- /dev/null +++ b/smoke-test/tests/cypress/cypress/e2e/manage_tagsV2/helpers/tags_page_helper.js @@ -0,0 +1,71 @@ +export default class TagsPageHelper { + static openPage() { + cy.visit("/tags"); + } + + static getTagUrn(name) { + return `urn:li:tag:${name}`; + } + + static create(name, description, shouldBeSuccessfullyCreated = true) { + cy.clickOptionWithTestId("add-tag-button"); + cy.getWithTestId("tag-name-field").within(() => + cy.get("input").focus().type(name), + ); + cy.getWithTestId("tag-description-field").within(() => + cy.get("input").focus().type(description), + ); + cy.clickOptionWithTestId("create-tag-modal-create-button"); + + if (shouldBeSuccessfullyCreated) { + cy.waitTextVisible(`Tag "${name}" successfully created`); + } else { + cy.waitTextVisible("Failed to create tag. An unexpected error occurred"); + cy.clickOptionWithTestId("create-tag-modal-cancel-button"); + } + } + + static remove(name) { + cy.getWithTestId("tag-search-input").focus().type(name, { delay: 0 }); + cy.clickOptionWithTestId(`${TagsPageHelper.getTagUrn(name)}-actions`); + cy.clickOptionWithTestId("action-delete"); + cy.clickOptionWithTestId("delete-tag-button"); + cy.getWithTestId("tag-search-input").clear(); + } + + static edit(name, newDescription) { + cy.getWithTestId("tag-search-input").focus().type(name, { delay: 0 }); + cy.clickOptionWithTestId(`${TagsPageHelper.getTagUrn(name)}-actions`); + cy.clickOptionWithTestId("action-edit"); + + cy.getWithTestId("edit-tag-modal").within(() => { + cy.getWithTestId("tag-description-field").within(() => + cy.get("input").focus().clear().type(newDescription), + ); + }); + + cy.clickOptionWithTestId("update-tag-button"); + cy.getWithTestId("tag-search-input").clear(); + } + + static ensureTagIsInTable(name, description) { + cy.getWithTestId("tag-search-input").focus().type(name, { delay: 0 }); + cy.getWithTestId(`${TagsPageHelper.getTagUrn(name)}-name`).should( + "contain", + name, + ); + cy.getWithTestId(`${TagsPageHelper.getTagUrn(name)}-description`).should( + "contain", + description, + ); + cy.getWithTestId("tag-search-input").clear(); + } + + static ensureTagIsNotInTable(name) { + cy.getWithTestId("tag-search-input").focus().type(name, { delay: 0 }); + cy.getWithTestId(`${TagsPageHelper.getTagUrn(name)}-name`).should( + "not.exist", + ); + cy.getWithTestId("tag-search-input").clear(); + } +} diff --git a/smoke-test/tests/cypress/cypress/e2e/manage_tagsV2/manage_tagsV2.js b/smoke-test/tests/cypress/cypress/e2e/manage_tagsV2/manage_tagsV2.js new file mode 100644 index 0000000000..e39894a50e --- /dev/null +++ b/smoke-test/tests/cypress/cypress/e2e/manage_tagsV2/manage_tagsV2.js @@ -0,0 +1,92 @@ +import DatasetHelper from "./helpers/dataset_helper"; +import TagsPageHelper from "./helpers/tags_page_helper"; + +const test_id = `manage_tagsV2_${new Date().getTime()}`; + +const SAMPLE_DATASET_URN = + "urn:li:dataset:(urn:li:dataPlatform:hive,SampleCypressHiveDataset,PROD)"; +const SAMPLE_DATASET_NAME = "SampleCypressHiveDataset"; + +describe("tags", () => { + beforeEach(() => { + cy.setIsThemeV2Enabled(true); + cy.login(); + }); + + it("verify search bar placeholder", () => { + cy.visit("/tags"); + cy.get('[data-testid="tag-search-input"]').should( + "have.attr", + "placeholder", + "Search tags...", + ); + }); + + it("verify title, search, and results", () => { + cy.visit("/tags"); + cy.get('[data-testid="page-title"]').should("contain.text", "Manage Tags"); + cy.get('[data-testid="urn:li:tag:Cypress-name"]').should( + "contain.text", + "Cypress", + ); + cy.get('[data-testid="tag-search-input"]').type("Cypress"); + cy.get('[data-testid="urn:li:tag:Cypress-name"]').should( + "contain.text", + "Cypress", + ); + }); + + it("verify search not exists", () => { + cy.visit("/tags"); + cy.get('[data-testid="page-title"]').should("contain.text", "Manage Tags"); + cy.get('[data-testid="urn:li:tag:Cypress-name"]').should( + "contain.text", + "Cypress", + ); + cy.get('[data-testid="tag-search-input"]').type("invalidvalue"); + cy.get('[data-testid="tags-not-found"]').should( + "contain.text", + "No tags found for your search query", + ); + }); + + it("should allow to create/edit/remove tags on tags page", () => { + const tagName = `tag_${test_id}_tags_page`; + const tagDescription = `${tagName} description`; + + TagsPageHelper.openPage(); + TagsPageHelper.create(tagName, tagDescription); + TagsPageHelper.ensureTagIsInTable(tagName, tagDescription); + // ensure that we can't to create tag with the same name + TagsPageHelper.create(tagName, tagDescription, false); + TagsPageHelper.edit(tagName, `${tagDescription} edited`); + TagsPageHelper.ensureTagIsInTable(tagName, `${tagDescription} edited`); + TagsPageHelper.remove(tagName); + TagsPageHelper.ensureTagIsNotInTable(tagName); + }); + + it("should allow to assign/unassign tags on a dataset", () => { + const tagName = `tag_${test_id}_dataset`; + const tagDescription = `${tagName} description`; + + TagsPageHelper.openPage(); + TagsPageHelper.create(tagName, tagDescription); + + DatasetHelper.openDataset(SAMPLE_DATASET_URN, SAMPLE_DATASET_NAME); + DatasetHelper.assignTag(tagName); + DatasetHelper.ensureTagIsAssigned(tagName); + + DatasetHelper.searchByTag(tagName); + DatasetHelper.ensureEntityIsInSearchResults(SAMPLE_DATASET_URN); + + DatasetHelper.openDataset(SAMPLE_DATASET_URN, SAMPLE_DATASET_NAME); + DatasetHelper.unassignTag(tagName); + DatasetHelper.ensureTagIsNotAssigned(tagName); + + DatasetHelper.searchByTag(tagName); + DatasetHelper.ensureEntityIsNotInSearchResults(SAMPLE_DATASET_URN); + + TagsPageHelper.openPage(); + TagsPageHelper.remove(tagName); + }); +});