Migrate: tags spec to playwright (#17758)

This commit is contained in:
Shailesh Parmar 2024-09-09 11:48:27 +05:30 committed by GitHub
parent 1c90eaaf3d
commit ae9de3057d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 444 additions and 381 deletions

View File

@ -1,379 +0,0 @@
/*
* Copyright 2022 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
addNewTagToEntity,
descriptionBox,
interceptURL,
verifyResponseStatusCode,
} from '../../common/common';
import {
deleteClassification,
submitForm,
validateForm,
visitClassificationPage,
} from '../../common/TagUtils';
import { visitEntityDetailsPage } from '../../common/Utils/Entity';
import { assignTags, removeTags } from '../../common/Utils/Tags';
import {
DELETE_TERM,
NEW_CLASSIFICATION,
NEW_TAG,
SEARCH_ENTITY_TABLE,
} from '../../constants/constants';
import { EntityType } from '../../constants/Entity.interface';
const permanentDeleteModal = (entity) => {
cy.get('[data-testid="delete-confirmation-modal"]')
.should('exist')
.then(() => {
cy.get('[role="dialog"]').should('be.visible');
cy.get('[data-testid="modal-header"]').should('be.visible');
});
cy.get('[data-testid="modal-header"]')
.should('be.visible')
.should('contain', `Delete ${entity}`);
cy.get('[data-testid="confirmation-text-input"]')
.should('be.visible')
.type(DELETE_TERM);
cy.get('[data-testid="confirm-button"]')
.should('be.visible')
.should('not.disabled')
.click();
};
describe('Classification Page', { tags: 'Governance' }, () => {
beforeEach(() => {
cy.login();
interceptURL(
'GET',
`/api/v1/tags?fields=usageCount&parent=${NEW_CLASSIFICATION.name}&limit=10`,
'getTagList'
);
interceptURL('GET', `/api/v1/permissions/classification/*`, 'permissions');
interceptURL(
'GET',
`/api/v1/search/query?q=*%20AND%20disabled:false&index=tag_search_index*`,
'suggestTag'
);
visitClassificationPage();
});
it('Should render basic elements on page', () => {
cy.get('[data-testid="add-classification"]').should('be.visible');
cy.get('[data-testid="add-new-tag-button"]').should('be.visible');
cy.get('[data-testid="manage-button"]').should('be.visible');
cy.get('[data-testid="description-container"]').should('be.visible');
cy.get('[data-testid="table"]').should('be.visible');
cy.get('.ant-table-thead > tr > .ant-table-cell')
.eq(0)
.contains('Tag')
.should('be.visible');
cy.get('.ant-table-thead > tr > .ant-table-cell')
.eq(1)
.contains('Display Name')
.should('be.visible');
cy.get('.ant-table-thead > tr > .ant-table-cell')
.eq(2)
.contains('Description')
.should('be.visible');
cy.get('.ant-table-thead > tr > .ant-table-cell')
.eq(3)
.contains('Actions')
.should('be.visible');
cy.get('.activeCategory > .tag-category')
.should('be.visible')
.invoke('text')
.then((text) => {
cy.get('.activeCategory > .tag-category')
.should('be.visible')
.invoke('text')
.then((heading) => {
expect(text).to.equal(heading);
});
});
});
it('Create classification with validation checks', () => {
interceptURL('POST', 'api/v1/classifications', 'createTagCategory');
cy.get('[data-testid="add-classification"]').should('be.visible').click();
cy.get('[data-testid="modal-container"]')
.should('exist')
.then(() => {
cy.get('[role="dialog"]').should('be.visible');
});
// validation should work
validateForm();
cy.get('[data-testid="name"]')
.should('be.visible')
.clear()
.type(NEW_CLASSIFICATION.name);
cy.get('[data-testid="displayName"]')
.should('be.visible')
.type(NEW_CLASSIFICATION.displayName);
cy.get(descriptionBox)
.should('be.visible')
.type(NEW_CLASSIFICATION.description);
cy.get('[data-testid="mutually-exclusive-button"]')
.scrollIntoView()
.should('be.visible')
.click();
submitForm();
verifyResponseStatusCode('@createTagCategory', 201);
cy.get('[data-testid="modal-container"]').should('not.exist');
cy.get('[data-testid="data-summary-container"]')
.should('be.visible')
.and('contain', NEW_CLASSIFICATION.displayName);
});
it('Create tag with validation checks', () => {
cy.get('[data-testid="data-summary-container"]')
.contains(NEW_CLASSIFICATION.displayName)
.should('be.visible')
.as('newCategory');
cy.get('@newCategory')
.click()
.parent()
.should('have.class', 'activeCategory');
cy.get('[data-testid="add-new-tag-button"]').should('be.visible').click();
cy.get('[data-testid="modal-container"]')
.should('exist')
.then(() => {
cy.get('[role="dialog"]').should('be.visible');
});
// validation should work
validateForm();
cy.get('[data-testid="name"]')
.should('be.visible')
.clear()
.type(NEW_TAG.name);
cy.get('[data-testid="displayName"]')
.should('be.visible')
.type(NEW_TAG.displayName);
cy.get(descriptionBox).should('be.visible').type(NEW_TAG.description);
cy.get('[data-testid="icon-url"]').scrollIntoView().type(NEW_TAG.icon);
cy.get('[data-testid="tags_color-color-input"]')
.scrollIntoView()
.type(NEW_TAG.color);
interceptURL('POST', '/api/v1/tags', 'createTag');
submitForm();
verifyResponseStatusCode('@createTag', 201);
cy.get('[data-testid="table"]').should('contain', NEW_TAG.name);
});
it(`Assign tag to table ${SEARCH_ENTITY_TABLE.table_3.displayName}`, () => {
const entity = SEARCH_ENTITY_TABLE.table_3;
visitEntityDetailsPage({
term: entity.term,
serviceName: entity.serviceName,
entity: entity.entity,
});
addNewTagToEntity(NEW_TAG);
});
it('Assign tag to DatabaseSchema', () => {
interceptURL(
'GET',
'/api/v1/permissions/databaseSchema/name/*',
'permissions'
);
interceptURL('PUT', '/api/v1/feed/tasks/*/resolve', 'taskResolve');
interceptURL(
'GET',
'/api/v1/databaseSchemas/name/*',
'databaseSchemasPage'
);
interceptURL('PATCH', '/api/v1/databaseSchemas/*', 'addTags');
const entity = SEARCH_ENTITY_TABLE.table_3;
const tag = 'PII.Sensitive';
visitEntityDetailsPage({
term: entity.term,
serviceName: entity.serviceName,
entity: entity.entity,
});
cy.get('[data-testid="breadcrumb-link"]')
.should('be.visible')
.contains(entity.schemaName)
.click();
verifyResponseStatusCode('@databaseSchemasPage', 200);
verifyResponseStatusCode('@permissions', 200);
assignTags(tag, EntityType.DatabaseSchema);
removeTags(tag, EntityType.DatabaseSchema);
});
it('Assign tag using Task & Suggestion flow to DatabaseSchema', () => {
interceptURL(
'GET',
'/api/v1/permissions/databaseSchema/name/*',
'permissions'
);
interceptURL('PUT', '/api/v1/feed/tasks/*/resolve', 'taskResolve');
interceptURL(
'GET',
'/api/v1/databaseSchemas/name/*',
'databaseSchemasPage'
);
const entity = SEARCH_ENTITY_TABLE.table_2;
const tag = 'Personal';
const assignee = 'admin';
visitEntityDetailsPage({
term: entity.term,
serviceName: entity.serviceName,
entity: entity.entity,
});
cy.get('[data-testid="breadcrumb-link"]')
.should('be.visible')
.contains(entity.schemaName)
.click();
verifyResponseStatusCode('@databaseSchemasPage', 200);
verifyResponseStatusCode('@permissions', 200);
// Create task to add tags
interceptURL('POST', '/api/v1/feed', 'taskCreated');
cy.get('[data-testid="request-entity-tags"]').should('exist').click();
// set assignees for task
cy.get(
'[data-testid="select-assignee"] > .ant-select-selector > .ant-select-selection-overflow'
)
.click()
.type(assignee);
cy.get(`[data-testid="${assignee}"]`).scrollIntoView().click();
// click outside the select box
cy.clickOutside();
cy.get('[data-testid="tag-selector"]').click().type(tag);
verifyResponseStatusCode('@suggestTag', 200);
cy.get('[data-testid="tag-PersonalData.Personal"]').click();
cy.get('[data-testid="tags-label"]').click();
cy.get('[data-testid="submit-tag-request"]').click();
verifyResponseStatusCode('@taskCreated', 201);
// Accept the tag suggestion which is created
cy.get('.ant-btn-compact-first-item').contains('Accept Suggestion').click();
verifyResponseStatusCode('@taskResolve', 200);
verifyResponseStatusCode('@databaseSchemasPage', 200);
cy.get('[data-testid="table"]').click();
cy.reload();
verifyResponseStatusCode('@databaseSchemasPage', 200);
cy.get('[data-testid="tags-container"]').scrollIntoView().contains(tag);
cy.get('[data-testid="edit-button"]').click();
// Remove all added tags
cy.get('[data-testid="remove-tags"]').click({ multiple: true });
interceptURL('PATCH', '/api/v1/databaseSchemas/*', 'removeTags');
cy.get('[data-testid="saveAssociatedTag"]').scrollIntoView().click();
verifyResponseStatusCode('@removeTags', 200);
});
it('Should have correct tag usage count and redirection should work', () => {
cy.get('[data-testid="data-summary-container"]')
.contains(NEW_CLASSIFICATION.displayName)
.should('be.visible')
.as('newCategory');
cy.get('@newCategory')
.click()
.parent()
.should('have.class', 'activeCategory');
verifyResponseStatusCode('@permissions', 200);
cy.get('[data-testid="entity-header-display-name"]')
.invoke('text')
.then((text) => {
// Get the text of the first menu item
if (text !== NEW_CLASSIFICATION.displayName) {
verifyResponseStatusCode('@getTags', 200);
}
});
cy.get('[data-testid="usage-count"]').should('be.visible').as('count');
cy.get('@count')
.invoke('text')
.then((text) => {
expect(text).to.equal('1');
});
interceptURL(
'GET',
'api/v1/search/query?q=&index=**',
'getEntityDetailsPage'
);
cy.get('@count').click();
verifyResponseStatusCode('@getEntityDetailsPage', 200);
});
it('Remove tag', () => {
interceptURL(
'DELETE',
'/api/v1/tags/*?recursive=true&hardDelete=true',
'deleteTag'
);
cy.get('[data-testid="data-summary-container"]')
.contains(NEW_CLASSIFICATION.displayName)
.click()
.parent()
.should('have.class', 'activeCategory');
verifyResponseStatusCode('@permissions', 200);
cy.get('[data-testid="table"]').should('contain', NEW_TAG.name);
cy.get('[data-testid="table"]').find('[data-testid="delete-tag"]').click();
cy.wait(500); // adding manual wait to open modal, as it depends on click not an api.
permanentDeleteModal(NEW_TAG.name);
verifyResponseStatusCode('@deleteTag', 200);
cy.wait(500);
cy.get('[data-testid="table"]')
.contains(NEW_TAG.name)
.should('not.be.exist');
});
it('Remove classification', () => {
deleteClassification(NEW_CLASSIFICATION);
});
});

View File

@ -0,0 +1,382 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, Page, test } from '@playwright/test';
import { SidebarItem } from '../../constant/sidebar';
import { TableClass } from '../../support/entity/TableClass';
import {
clickOutside,
createNewPage,
descriptionBox,
redirectToHomePage,
uuid,
} from '../../utils/common';
import { sidebarClick } from '../../utils/sidebar';
import { submitForm, validateForm } from '../../utils/tag';
const NEW_CLASSIFICATION = {
name: `PlaywrightClassification-${uuid()}`,
displayName: `PlaywrightClassification-${uuid()}`,
description: 'This is the PlaywrightClassification',
};
const NEW_TAG = {
name: `PlaywrightTag-${uuid()}`,
displayName: `PlaywrightTag-${uuid()}`,
renamedName: `PlaywrightTag-${uuid()}`,
description: 'This is the PlaywrightTag',
color: '#FF5733',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAF8AAACFCAMAAAAKN9SOAAAAA1BMVEXmGSCqexgYAAAAI0lEQVRoge3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAHgaMeAAAUWJHZ4AAAAASUVORK5CYII=',
};
const tagFqn = `${NEW_CLASSIFICATION.name}.${NEW_TAG.name}`;
const permanentDeleteModal = async (page: Page, entity: string) => {
await page.waitForSelector('.ant-modal-content', {
state: 'visible',
});
await expect(page.locator('.ant-modal-content')).toBeVisible();
await expect(page.locator('[data-testid="modal-header"]')).toContainText(
`Delete ${entity}`
);
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
await page.click('[data-testid="confirm-button"]');
};
test.describe.configure({ mode: 'serial' });
// use the admin user to login
test.use({ storageState: 'playwright/.auth/admin.json' });
const table = new TableClass();
test.beforeAll(async ({ browser }) => {
const { apiContext, afterAction } = await createNewPage(browser);
await table.create(apiContext);
await afterAction();
});
test.afterAll(async ({ browser }) => {
const { apiContext, afterAction } = await createNewPage(browser);
await table.delete(apiContext);
await afterAction();
});
test.beforeEach(async ({ page }) => {
await redirectToHomePage(page);
});
test('Classification Page', async ({ page }) => {
await test.step('Should render basic elements on page', async () => {
const getTags = page.waitForResponse('/api/v1/tags*');
await sidebarClick(page, SidebarItem.TAGS);
await getTags;
await expect(
page.locator('[data-testid="add-classification"]')
).toBeVisible();
await expect(
page.locator('[data-testid="add-new-tag-button"]')
).toBeVisible();
await expect(page.locator('[data-testid="manage-button"]')).toBeVisible();
await expect(
page.locator('[data-testid="description-container"]')
).toBeVisible();
await expect(page.locator('[data-testid="table"]')).toBeVisible();
const headers = await page
.locator('.ant-table-thead > tr > .ant-table-cell')
.allTextContents();
expect(headers).toEqual(['Tag', 'Display Name', 'Description', 'Actions']);
});
await test.step('Create classification with validation checks', async () => {
await page.click('[data-testid="add-classification"]');
await page.waitForSelector('.ant-modal-content', {
state: 'visible',
});
await expect(page.locator('.ant-modal-content')).toBeVisible();
await validateForm(page);
await page.fill('[data-testid="name"]', NEW_CLASSIFICATION.name);
await page.fill(
'[data-testid="displayName"]',
NEW_CLASSIFICATION.displayName
);
await page.fill(descriptionBox, NEW_CLASSIFICATION.description);
await page.click('[data-testid="mutually-exclusive-button"]');
const createTagCategoryResponse = page.waitForResponse(
'api/v1/classifications'
);
await submitForm(page);
await createTagCategoryResponse;
await expect(
page.locator('[data-testid="modal-container"]')
).not.toBeVisible();
await expect(
page.locator('[data-testid="data-summary-container"]')
).toContainText(NEW_CLASSIFICATION.displayName);
});
await test.step('Create tag with validation checks', async () => {
await page.click(`text=${NEW_CLASSIFICATION.displayName}`);
await expect(page.locator('.activeCategory')).toContainText(
NEW_CLASSIFICATION.displayName
);
await page.click('[data-testid="add-new-tag-button"]');
await page.waitForSelector('.ant-modal-content', {
state: 'visible',
});
await expect(page.locator('.ant-modal-content')).toBeVisible();
await validateForm(page);
await page.fill('[data-testid="name"]', NEW_TAG.name);
await page.fill('[data-testid="displayName"]', NEW_TAG.displayName);
await page.fill(descriptionBox, NEW_TAG.description);
await page.fill('[data-testid="icon-url"]', NEW_TAG.icon);
await page.fill('[data-testid="tags_color-color-input"]', NEW_TAG.color);
const createTagResponse = page.waitForResponse('api/v1/tags');
await submitForm(page);
await createTagResponse;
await expect(page.locator('[data-testid="table"]')).toContainText(
NEW_TAG.name
);
});
await test.step(`Assign tag to table`, async () => {
await table.visitEntityPage(page);
const { name, displayName } = NEW_TAG;
await page.click(
'[data-testid="classification-tags-0"] [data-testid="entity-tags"] [data-testid="add-tag"]'
);
await page.fill('[data-testid="tag-selector"] input', name);
await page.click(`[data-testid="tag-${tagFqn}"]`);
await expect(
page.locator('[data-testid="tag-selector"] > .ant-select-selector')
).toContainText(displayName);
const saveAssociatedTag = page.waitForResponse(
(response) =>
response.request().method() === 'PATCH' &&
response
.url()
.includes(`/api/v1/tables/${table.entityResponseData?.['id']}`)
);
await page.click('[data-testid="saveAssociatedTag"]');
await saveAssociatedTag;
await page.waitForSelector('.ant-select-dropdown', {
state: 'detached',
});
await expect(
page
.getByRole('row', { name: 'user_id numeric Unique' })
.getByTestId('tags-container')
).toContainText(displayName);
await expect(
page.locator(
'[data-testid="classification-tags-0"] [data-testid="tags-container"] [data-testid="icon"]'
)
).toBeVisible();
});
await test.step(
'Assign tag using Task & Suggestion flow to DatabaseSchema',
async () => {
const entity = table.schema;
const tag = 'Personal';
const assignee = 'admin';
const databaseSchemaPage = page.waitForResponse(
'api/v1/databaseSchemas/name/*'
);
const permissions = page.waitForResponse(
'api/v1/permissions/databaseSchema/name/*'
);
await page.click(
`[data-testid="breadcrumb-link"]:has-text("${entity.name}")`
);
await databaseSchemaPage;
await permissions;
await page.click('[data-testid="request-entity-tags"]');
await page.click('[data-testid="select-assignee"]');
const assigneeResponse = page.waitForResponse(
'/api/v1/search/suggest?q=*&index=user_search_index*team_search_index*'
);
await page.keyboard.type(assignee);
await page.click(`[data-testid="${assignee}"]`);
await assigneeResponse;
await clickOutside(page);
const suggestTag = page.waitForResponse(
'api/v1/search/query?q=*%20AND%20disabled:false&index=tag_search_index*'
);
await page.click('[data-testid="tag-selector"]');
await page.keyboard.type(tag);
await suggestTag;
await page.click('[data-testid="tag-PersonalData.Personal"]');
await page.click('[data-testid="tags-label"]');
const taskCreated = page.waitForResponse(
(response) =>
response.request().method() === 'POST' &&
response.url().includes('api/v1/feed')
);
await page.click('[data-testid="submit-tag-request"]');
await taskCreated;
const acceptSuggestion = page.waitForResponse(
(response) =>
response.request().method() === 'PUT' &&
response.url().includes('/api/v1/feed/tasks/') &&
response.url().includes('/resolve')
);
await page.click(
'.ant-btn-compact-first-item:has-text("Accept Suggestion")'
);
await acceptSuggestion;
await page.click('[data-testid="table"]');
const databaseSchemasPage = page.waitForResponse(
'api/v1/databaseSchemas/name/*'
);
await page.reload();
await databaseSchemasPage;
await expect(
page.locator('[data-testid="tags-container"]')
).toContainText(tag);
await page.click('[data-testid="edit-button"]');
await page.click('[data-testid="remove-tags"]');
const removeTags = page.waitForResponse(
(response) =>
response.request().method() === 'PATCH' &&
response.url().includes('/api/v1/databaseSchemas/')
);
await page.click('[data-testid="saveAssociatedTag"]');
await removeTags;
}
);
await test.step(
'Should have correct tag usage count and redirection should work',
async () => {
const getTags = page.waitForResponse('/api/v1/tags*');
await sidebarClick(page, SidebarItem.TAGS);
await getTags;
await page
.locator(`[data-testid="side-panel-classification"]`)
.filter({ hasText: NEW_CLASSIFICATION.displayName })
.click();
await expect(page.locator('.activeCategory')).toContainText(
NEW_CLASSIFICATION.displayName
);
const count = await page
.locator('[data-testid="usage-count"]')
.textContent();
expect(count).toBe('1');
const getEntityDetailsPage = page.waitForResponse(
'api/v1/search/query?q=&index=**'
);
await page.click('[data-testid="usage-count"]');
await getEntityDetailsPage;
}
);
await test.step('Delete tag', async () => {
const getTags = page.waitForResponse('/api/v1/tags*');
await sidebarClick(page, SidebarItem.TAGS);
await getTags;
await page
.locator(`[data-testid="side-panel-classification"]`)
.filter({ hasText: NEW_CLASSIFICATION.displayName })
.click();
await expect(page.locator('.activeCategory')).toContainText(
NEW_CLASSIFICATION.displayName
);
await expect(page.locator('[data-testid="table"]')).toContainText(
NEW_TAG.name
);
await page.click('[data-testid="table"] [data-testid="delete-tag"]');
await page.waitForTimeout(500); // adding manual wait to open modal, as it depends on click not an api.
const deleteTag = page.waitForResponse(
(response) =>
response.request().method() === 'DELETE' &&
response.url().includes('/api/v1/tags/')
);
await permanentDeleteModal(page, NEW_TAG.name);
await deleteTag;
await page.waitForTimeout(500);
await expect(page.locator('[data-testid="table"]')).not.toContainText(
NEW_TAG.name
);
});
await test.step('Remove classification', async () => {
await expect(page.getByTestId('entity-header-display-name')).toContainText(
NEW_CLASSIFICATION.displayName
);
await page.click('[data-testid="manage-button"]');
await page.click('[data-testid="delete-button"]');
await page.click('[data-testid="hard-delete-option"]');
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
const deleteClassification = page.waitForResponse(
(response) =>
response.request().method() === 'DELETE' &&
response.url().includes('/api/v1/classifications/')
);
await page.click('[data-testid="confirm-button"]');
await deleteClassification;
await expect(
page
.locator('[data-testid="data-summary-container"]')
.filter({ hasText: NEW_CLASSIFICATION.name })
).not.toBeVisible();
});
});

View File

@ -10,11 +10,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Page } from '@playwright/test';
import { expect, Page } from '@playwright/test';
import { SidebarItem } from '../constant/sidebar';
import { redirectToHomePage } from './common';
import {
NAME_MIN_MAX_LENGTH_VALIDATION_ERROR,
NAME_VALIDATION_ERROR,
redirectToHomePage,
} from './common';
import { sidebarClick } from './sidebar';
export const TAG_INVALID_NAMES = {
MIN_LENGTH: 'c',
MAX_LENGTH: 'a87439625b1c2d3e4f5061728394a5b6c7d8e90a1b2c3d4e5f67890ab',
WITH_SPECIAL_CHARS: '!@#$%^&*()',
};
export const visitClassificationPage = async (
page: Page,
classificationName: string
@ -27,3 +37,53 @@ export const visitClassificationPage = async (
await classificationResponse;
await page.getByRole('menuitem', { name: classificationName }).click();
};
export async function submitForm(page: Page) {
await page.locator('button[type="submit"]').scrollIntoViewIfNeeded();
await page.locator('button[type="submit"]').click();
}
export async function validateForm(page: Page) {
// submit form without any data to trigger validation
await submitForm(page);
// error messages
await expect(page.locator('#tags_name_help')).toBeVisible();
await expect(page.locator('#tags_name_help')).toContainText(
'Name is required'
);
await expect(page.locator('#tags_description_help')).toBeVisible();
await expect(page.locator('#tags_description_help')).toContainText(
'Description is required'
);
// validation should work for invalid names
// min length validation
await page.locator('[data-testid="name"]').scrollIntoViewIfNeeded();
await page.locator('[data-testid="name"]').clear();
await page.locator('[data-testid="name"]').fill(TAG_INVALID_NAMES.MIN_LENGTH);
await expect(page.locator('#tags_name_help')).toContainText(
NAME_MIN_MAX_LENGTH_VALIDATION_ERROR
);
// max length validation
await page.locator('[data-testid="name"]').clear();
await page.locator('[data-testid="name"]').fill(TAG_INVALID_NAMES.MAX_LENGTH);
await expect(page.locator('#tags_name_help')).toContainText(
NAME_MIN_MAX_LENGTH_VALIDATION_ERROR
);
// with special char validation
await page.locator('[data-testid="name"]').clear();
await page
.locator('[data-testid="name"]')
.fill(TAG_INVALID_NAMES.WITH_SPECIAL_CHARS);
await expect(page.locator('#tags_name_help')).toContainText(
NAME_VALIDATION_ERROR
);
}