mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-14 02:08:54 +00:00

* feat(#15380): replace the description editor with a new block editor. * chore: Add placeholder support to BlockEditor * feat: Add autofocus support to BlockEditor * chore: implement block editor in richtext editor * chore: Remove unused CSS import in RichTextEditor * fix: ensure safe access to getEditorContent in Markdown editor components * minor change * fix: add support for ttf file extension in pom.xml * fix: adjust block editor styles for better usability and overflow handling * fix: update RichTextEditorPreviewer to RichTextEditorPreviewerV1 and adjust block editor styles * fix: update description box selector to use om-block-editor for consistency * fix: disable autoFocus on BlockEditor in RichTextEditorPreviewerV1 * refactor: update RichTextEditorPreviewer references to RichTextEditorPreviewerV1 in tests * test: update timer handling in ApplicationCard and SuggestionsAlert tests * fix(diff): update diff view rendering and attributes for consistency * fix(RichTextEditor): add placeholder for empty markdown descriptions * Add data-diff in <span> * Fix test * fix: update description box selector to use locator method for better reliability * feat: integrate RichTextEditorPreviewerV1 for description rendering in Notification and Observability Alerts pages * feat: add markdown conversion for mentions and hashtags in BlockEditorUtils * fix: update initial value handling in TagsForm and formUtils * MINOR - Update description handling in Playwright tests and utilities * Refactor Playwright tests to use descriptionBox for tag and team descriptions * Refactor custom property handling and update description check logic * Enhance entity version page tests by adding description box read-only check and updating data-testid attributes * Refactor description handling and improve diff rendering logic in task pages * Fix user description clearing logic in UserDetails.spec.ts * Enhance Rich Text Editor by adding custom styles and converting markdown to HTML for backward compatibility * Remove test for rendering alert description when not present in AlertDetailsPage * Clean up RichTextEditorPreviewerV1 by removing unnecessary comments and improving readability * Update SearchIndexApplication.spec.ts to select 'Table' instead of 'Topic' in the tree widget * Refactor BlockEditor and FeedUtils to improve code organization and readability * Fix regex in getTextFromHtmlString to correctly remove HTML tags * Add tests for getTextFromHtmlString and improve HTML tag removal regex --------- Co-authored-by: mohitdeuex <mohit.y@deuexsolutions.com>
271 lines
8.5 KiB
TypeScript
271 lines
8.5 KiB
TypeScript
/*
|
|
* 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 { Browser, expect, Page, request } from '@playwright/test';
|
|
import { randomUUID } from 'crypto';
|
|
import { SidebarItem } from '../constant/sidebar';
|
|
import { adjectives, nouns } from '../constant/user';
|
|
import { Domain } from '../support/domain/Domain';
|
|
import { sidebarClick } from './sidebar';
|
|
|
|
export const uuid = () => randomUUID().split('-')[0];
|
|
|
|
export const descriptionBox = '.om-block-editor[contenteditable="true"]';
|
|
export const descriptionBoxReadOnly =
|
|
'.om-block-editor[contenteditable="false"]';
|
|
|
|
export const INVALID_NAMES = {
|
|
MAX_LENGTH:
|
|
'a87439625b1c2d3e4f5061728394a5b6c7d8e90a1b2c3d4e5f67890aba87439625b1c2d3e4f5061728394a5b6c7d8e90a1b2c3d4e5f67890abName can be a maximum of 128 characters',
|
|
WITH_SPECIAL_CHARS: '::normalName::',
|
|
};
|
|
|
|
export const NAME_VALIDATION_ERROR =
|
|
'Name must contain only letters, numbers, underscores, hyphens, periods, parenthesis, and ampersands.';
|
|
|
|
export const NAME_MIN_MAX_LENGTH_VALIDATION_ERROR =
|
|
'Name size must be between 2 and 64';
|
|
|
|
export const NAME_MAX_LENGTH_VALIDATION_ERROR =
|
|
'Name can be a maximum of 128 characters';
|
|
|
|
export const getToken = async (page: Page) => {
|
|
return page.evaluate(
|
|
() =>
|
|
JSON.parse(localStorage.getItem('om-session') ?? '{}')?.state
|
|
?.oidcIdToken ?? ''
|
|
);
|
|
};
|
|
|
|
export const getAuthContext = async (token: string) => {
|
|
return await request.newContext({
|
|
extraHTTPHeaders: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
};
|
|
|
|
export const redirectToHomePage = async (page: Page) => {
|
|
await page.goto('/');
|
|
await page.waitForURL('**/my-data');
|
|
};
|
|
|
|
export const removeLandingBanner = async (page: Page) => {
|
|
const widgetResponse = page.waitForResponse('/api/v1/search/query?q=**');
|
|
await page.click('[data-testid="welcome-screen-close-btn"]');
|
|
await widgetResponse;
|
|
};
|
|
|
|
export const createNewPage = async (browser: Browser) => {
|
|
// create a new page
|
|
const page = await browser.newPage();
|
|
await redirectToHomePage(page);
|
|
|
|
// get the token from localStorage
|
|
const token = await getToken(page);
|
|
|
|
// create a new context with the token
|
|
const apiContext = await getAuthContext(token);
|
|
|
|
const afterAction = async () => {
|
|
await apiContext.dispose();
|
|
await page.close();
|
|
};
|
|
|
|
return { page, apiContext, afterAction };
|
|
};
|
|
|
|
/**
|
|
* Retrieves the API context for the given page.
|
|
* @param page The Playwright page object.
|
|
* @returns An object containing the API context and a cleanup function.
|
|
*/
|
|
export const getApiContext = async (page: Page) => {
|
|
const token = await getToken(page);
|
|
const apiContext = await getAuthContext(token);
|
|
const afterAction = async () => await apiContext.dispose();
|
|
|
|
return { apiContext, afterAction };
|
|
};
|
|
|
|
export const getEntityTypeSearchIndexMapping = (entityType: string) => {
|
|
const entityMapping = {
|
|
Table: 'table_search_index',
|
|
Topic: 'topic_search_index',
|
|
Dashboard: 'dashboard_search_index',
|
|
Pipeline: 'pipeline_search_index',
|
|
MlModel: 'mlmodel_search_index',
|
|
Container: 'container_search_index',
|
|
SearchIndex: 'search_entity_search_index',
|
|
ApiEndpoint: 'api_endpoint_search_index',
|
|
Metric: 'metric_search_index',
|
|
};
|
|
|
|
return entityMapping[entityType as keyof typeof entityMapping];
|
|
};
|
|
|
|
export const toastNotification = async (
|
|
page: Page,
|
|
message: string | RegExp
|
|
) => {
|
|
await expect(
|
|
page.locator('.Toastify__toast-body[role="alert"]').first()
|
|
).toHaveText(message);
|
|
|
|
await page
|
|
.locator('.Toastify__toast')
|
|
.getByLabel('close', { exact: true })
|
|
.first()
|
|
.click();
|
|
};
|
|
|
|
export const clickOutside = async (page: Page) => {
|
|
await page.locator('body').click({
|
|
position: {
|
|
x: 0,
|
|
y: 0,
|
|
},
|
|
}); // with this action left menu bar is getting opened
|
|
await page.mouse.move(1280, 0); // moving out side left menu bar to avoid random failure due to left menu bar
|
|
};
|
|
|
|
export const visitOwnProfilePage = async (page: Page) => {
|
|
await page.locator('[data-testid="dropdown-profile"] svg').click();
|
|
await page.waitForSelector('[role="menu"].profile-dropdown', {
|
|
state: 'visible',
|
|
});
|
|
const userResponse = page.waitForResponse(
|
|
'/api/v1/users/name/*?fields=*&include=all'
|
|
);
|
|
await page.getByTestId('user-name').click();
|
|
await userResponse;
|
|
await clickOutside(page);
|
|
};
|
|
|
|
export const assignDomain = async (
|
|
page: Page,
|
|
domain: { name: string; displayName: string }
|
|
) => {
|
|
await page.getByTestId('add-domain').click();
|
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
|
const searchDomain = page.waitForResponse(
|
|
`/api/v1/search/query?q=*${encodeURIComponent(domain.name)}*`
|
|
);
|
|
await page
|
|
.getByTestId('selectable-list')
|
|
.getByTestId('searchbar')
|
|
.fill(domain.name);
|
|
await searchDomain;
|
|
await page.getByRole('listitem', { name: domain.displayName }).click();
|
|
|
|
await expect(page.getByTestId('domain-link')).toContainText(
|
|
domain.displayName
|
|
);
|
|
};
|
|
|
|
export const updateDomain = async (
|
|
page: Page,
|
|
domain: { name: string; displayName: string }
|
|
) => {
|
|
await page.getByTestId('add-domain').click();
|
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
|
await page.getByTestId('selectable-list').getByTestId('searchbar').clear();
|
|
const searchDomain = page.waitForResponse(
|
|
`/api/v1/search/query?q=*${encodeURIComponent(domain.name)}*`
|
|
);
|
|
await page
|
|
.getByTestId('selectable-list')
|
|
.getByTestId('searchbar')
|
|
.fill(domain.name);
|
|
await searchDomain;
|
|
await page.getByRole('listitem', { name: domain.displayName }).click();
|
|
|
|
await expect(page.getByTestId('domain-link')).toContainText(
|
|
domain.displayName
|
|
);
|
|
};
|
|
|
|
export const removeDomain = async (page: Page) => {
|
|
await page.getByTestId('add-domain').click();
|
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
|
|
|
await expect(page.getByTestId('remove-owner').locator('path')).toBeVisible();
|
|
|
|
await page.getByTestId('remove-owner').locator('svg').click();
|
|
|
|
await expect(page.getByTestId('no-domain-text')).toContainText('No Domain');
|
|
};
|
|
|
|
export const visitGlossaryPage = async (page: Page, glossaryName: string) => {
|
|
await redirectToHomePage(page);
|
|
const glossaryResponse = page.waitForResponse('/api/v1/glossaries?fields=*');
|
|
await sidebarClick(page, SidebarItem.GLOSSARY);
|
|
await glossaryResponse;
|
|
await page.getByRole('menuitem', { name: glossaryName }).click();
|
|
};
|
|
|
|
export const getRandomFirstName = () => {
|
|
return `${
|
|
adjectives[Math.floor(Math.random() * adjectives.length)]
|
|
}${uuid()}`;
|
|
};
|
|
export const getRandomLastName = () => {
|
|
return `${nouns[Math.floor(Math.random() * nouns.length)]}${uuid()}`;
|
|
};
|
|
|
|
export const generateRandomUsername = (prefix = '') => {
|
|
const firstName = `${prefix}${getRandomFirstName()}`;
|
|
const lastName = `${prefix}${getRandomLastName()}`;
|
|
|
|
return {
|
|
firstName,
|
|
lastName,
|
|
email: `${firstName}.${lastName}@example.com`,
|
|
password: 'User@OMD123',
|
|
};
|
|
};
|
|
|
|
export const verifyDomainPropagation = async (
|
|
page: Page,
|
|
domain: Domain['responseData'],
|
|
childFqnSearchTerm: string
|
|
) => {
|
|
await page.getByTestId('searchBox').fill(childFqnSearchTerm);
|
|
await page.getByTestId('searchBox').press('Enter');
|
|
|
|
await expect(
|
|
page
|
|
.getByTestId(`table-data-card_${childFqnSearchTerm}`)
|
|
.getByTestId('domain-link')
|
|
).toContainText(domain.displayName);
|
|
};
|
|
|
|
export const replaceAllSpacialCharWith_ = (text: string) => {
|
|
return text.replaceAll(/[&/\\#, +()$~%.'":*?<>{}]/g, '_');
|
|
};
|
|
|
|
// Since the tests run in parallel sometimes the error toast alert pops up
|
|
// Stating the domain or glossary does not exist since it's deleted in other test
|
|
// This error toast blocks the buttons at the top
|
|
// Below logic closes the alert if it's present to avoid flakiness in tests
|
|
export const closeFirstPopupAlert = async (page: Page) => {
|
|
const toastLocator = '.Toastify__toast-body[role="alert"]';
|
|
const toastElement = await page.$(toastLocator);
|
|
if (toastElement) {
|
|
await page
|
|
.locator('.Toastify__toast')
|
|
.getByLabel('close', { exact: true })
|
|
.first()
|
|
.click();
|
|
}
|
|
};
|