Add unit test for table entity page (#3730)

* Add unit test for table entity page

* Add unit test for description component

* Add unit for EntityPageInfo component

* Add unit test for EntityTable component
This commit is contained in:
Sachin Chaurasiya 2022-03-30 15:48:43 +05:30 committed by GitHub
parent 9fb78ed2c1
commit 227d81951c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1187 additions and 41 deletions

View File

@ -11,7 +11,13 @@
* limitations under the License.
*/
import { findByText, getByTestId, render } from '@testing-library/react';
import {
findByTestId,
findByText,
getByTestId,
queryByTestId,
render,
} from '@testing-library/react';
import { LeafNodes, LoadingNodeState } from 'Models';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
@ -58,6 +64,40 @@ const mockUserTeam = [
type: 'type',
},
];
const mockThreads = [
{
id: '465b2dfb-300e-45f5-a1a6-e19c6225e9e7',
href: 'http://localhost:8585/api/v1/feed/465b2dfb-300e-45f5-a1a6-e19c6225e9e7',
threadTs: 1647434125848,
about: '<#E/table/bigquery_gcp.shopify.raw_product_catalog/description>',
entityId: 'f1ebcfdf-d4b8-43bd-add2-1789e25ddde3',
createdBy: 'aaron_johnson0',
updatedAt: 1647434125848,
updatedBy: 'anonymous',
resolved: false,
message: 'New thread.',
postsCount: 0,
posts: [],
relativeDay: 'Today',
},
{
id: '40c2faec-0159-4d86-9b15-c17f3e1c081b',
href: 'http://localhost:8585/api/v1/feed/40c2faec-0159-4d86-9b15-c17f3e1c081b',
threadTs: 1647411418056,
about: '<#E/table/bigquery_gcp.shopify.raw_product_catalog/description>',
entityId: 'f1ebcfdf-d4b8-43bd-add2-1789e25ddde3',
createdBy: 'sachin.c',
updatedAt: 1647434031435,
updatedBy: 'anonymous',
resolved: false,
message: 'New thread.',
postsCount: 0,
posts: [],
relativeDay: 'Today',
},
];
const DatasetDetailsProps = {
activeTab: 1,
columns: [],
@ -93,7 +133,7 @@ const DatasetDetailsProps = {
removeLineageHandler: jest.fn(),
entityLineageHandler: jest.fn(),
tableQueries: [],
entityThread: [],
entityThread: mockThreads,
isentityThreadLoading: false,
postFeedHandler: jest.fn(),
feedCount: 0,
@ -115,15 +155,17 @@ const DatasetDetailsProps = {
tagUpdateHandler: jest.fn(),
};
jest.mock('../ManageTab/ManageTab.component', () => {
return jest.fn().mockReturnValue(<p>ManageTab</p>);
return jest.fn().mockReturnValue(<p data-testid="manage">ManageTab</p>);
});
jest.mock('../EntityLineage/EntityLineage.component', () => {
return jest.fn().mockReturnValue(<p>Lineage</p>);
return jest.fn().mockReturnValue(<p data-testid="lineage">Lineage</p>);
});
jest.mock('../TableProfiler/TableProfiler.component', () => {
return jest.fn().mockReturnValue(<p>ProfilerTable</p>);
return jest
.fn()
.mockReturnValue(<p data-testid="TableProfiler">TableProfiler</p>);
});
jest.mock('../common/description/Description', () => {
@ -138,12 +180,8 @@ jest.mock('../common/entityPageInfo/EntityPageInfo', () => {
return jest.fn().mockReturnValue(<p>EntityPageInfo</p>);
});
jest.mock('../common/TabsPane/TabsPane', () => {
return jest.fn().mockReturnValue(<p>TabsPane</p>);
});
jest.mock('../ActivityFeed/ActivityFeedList/ActivityFeedList.tsx', () => {
return jest.fn().mockReturnValue(<p>FeedCards</p>);
return jest.fn().mockReturnValue(<p>ActivityFeedList</p>);
});
jest.mock('../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.tsx', () => {
@ -155,9 +193,9 @@ jest.mock('../ActivityFeed/ActivityFeedEditor/ActivityFeedEditor.tsx', () => {
jest.mock('../../utils/CommonUtils', () => ({
addToRecentViewed: jest.fn(),
getCountBadge: jest.fn(),
getCurrentUserId: jest.fn().mockReturnValue('CurrentUserId'),
getPartialNameFromFQN: jest.fn().mockReturnValue('PartialNameFromFQN'),
// getTableFQNFromColumnFQN: jest.fn().mockReturnValue('TableFQNFromColumnFQN'),
getUserTeams: () => mockUserTeam,
}));
@ -169,10 +207,123 @@ describe('Test MyDataDetailsPage page', () => {
const relatedTables = getByTestId(container, 'related-tables-container');
const EntityPageInfo = await findByText(container, /EntityPageInfo/i);
const TabsPane = await findByText(container, /TabsPane/i);
const description = await findByText(container, /Description/i);
const tabs = await findByTestId(container, 'tabs');
const schemaTab = await findByTestId(tabs, 'Schema');
const activityFeedTab = await findByTestId(tabs, 'Activity Feed');
const sampleDataTab = await findByTestId(tabs, 'Sample Data');
const queriesTab = await findByTestId(tabs, 'Queries');
const profilerTab = await findByTestId(tabs, 'Profiler');
const dataQualityTab = await findByTestId(tabs, 'Data Quality');
const lineageTab = await findByTestId(tabs, 'Lineage');
const manageTab = await findByTestId(tabs, 'Manage');
const dbtTab = queryByTestId(tabs, 'DBT');
expect(relatedTables).toBeInTheDocument();
expect(EntityPageInfo).toBeInTheDocument();
expect(TabsPane).toBeInTheDocument();
expect(description).toBeInTheDocument();
expect(tabs).toBeInTheDocument();
expect(schemaTab).toBeInTheDocument();
expect(activityFeedTab).toBeInTheDocument();
expect(sampleDataTab).toBeInTheDocument();
expect(queriesTab).toBeInTheDocument();
expect(profilerTab).toBeInTheDocument();
expect(dataQualityTab).toBeInTheDocument();
expect(lineageTab).toBeInTheDocument();
expect(manageTab).toBeInTheDocument();
expect(dbtTab).not.toBeInTheDocument();
});
it('Check if active tab is schema', async () => {
const { container } = render(<DatasetDetails {...DatasetDetailsProps} />, {
wrapper: MemoryRouter,
});
const schema = await findByText(container, /SchemaTab/i);
expect(schema).toBeInTheDocument();
});
it('Check if active tab is activity feed', async () => {
const { container } = render(
<DatasetDetails {...DatasetDetailsProps} activeTab={2} />,
{
wrapper: MemoryRouter,
}
);
const activityFeedList = await findByText(container, /ActivityFeedList/i);
expect(activityFeedList).toBeInTheDocument();
});
it('Check if active tab is sample data', async () => {
const { container } = render(
<DatasetDetails {...DatasetDetailsProps} activeTab={3} />,
{
wrapper: MemoryRouter,
}
);
const sampleData = await findByTestId(container, 'sample-data');
expect(sampleData).toBeInTheDocument();
});
it('Check if active tab is queries', async () => {
const { container } = render(
<DatasetDetails {...DatasetDetailsProps} activeTab={4} />,
{
wrapper: MemoryRouter,
}
);
const tableQueries = await findByTestId(container, 'table-queries');
expect(tableQueries).toBeInTheDocument();
});
it('Check if active tab is profiler', async () => {
const { container } = render(
<DatasetDetails {...DatasetDetailsProps} activeTab={5} />,
{
wrapper: MemoryRouter,
}
);
const tableProfiler = await findByTestId(container, 'TableProfiler');
expect(tableProfiler).toBeInTheDocument();
});
it('Check if active tab is data quality', async () => {
const { container } = render(
<DatasetDetails {...DatasetDetailsProps} activeTab={6} />,
{
wrapper: MemoryRouter,
}
);
const dataQuality = await findByTestId(container, 'data-quality-tab');
expect(dataQuality).toBeInTheDocument();
});
it('Check if active tab is lineage', async () => {
const { container } = render(
<DatasetDetails {...DatasetDetailsProps} activeTab={7} />,
{
wrapper: MemoryRouter,
}
);
const lineage = await findByTestId(container, 'lineage');
expect(lineage).toBeInTheDocument();
});
it('Check if active tab is manage', async () => {
const { container } = render(
<DatasetDetails {...DatasetDetailsProps} activeTab={9} />,
{
wrapper: MemoryRouter,
}
);
const manage = await findByTestId(container, 'manage');
expect(manage).toBeInTheDocument();
});
});

View File

@ -61,7 +61,7 @@ import TagsContainer from '../tags-container/tags-container';
import TagsViewer from '../tags-viewer/tags-viewer';
import Tags from '../tags/tags';
type Props = {
interface Props {
owner: Table['owner'];
tableColumns: ModifiedTableColumn[];
joins: Array<ColumnJoins>;
@ -74,7 +74,7 @@ type Props = {
onUpdate?: (columns: ModifiedTableColumn[]) => void;
onThreadLinkSelect?: (value: string) => void;
onEntityFieldSelect?: (value: string) => void;
};
}
const EntityTable = ({
tableColumns,
@ -341,8 +341,11 @@ const EntityTable = ({
return (
<div className="tw-table-responsive" id="schemaTable">
<table className="tw-w-full" {...getTableProps()}>
<thead>
<table
className="tw-w-full"
{...getTableProps()}
data-testid="entity-table">
<thead data-testid="table-header">
{/* eslint-disable-next-line */}
{headerGroups.map((headerGroup: any, index: number) => (
<tr
@ -356,6 +359,7 @@ const EntityTable = ({
'tw-w-60':
column.id === 'tags' || column.id === 'columnTests',
})}
data-testid={column.id}
key={index}
{...column.getHeaderProps()}>
{column.render('Header')}
@ -365,7 +369,7 @@ const EntityTable = ({
))}
</thead>
<tbody {...getTableBodyProps()}>
<tbody {...getTableBodyProps()} data-testid="table-body">
{/* eslint-disable-next-line */}
{rows.map((row: any, index: number) => {
prepareRow(row);
@ -373,6 +377,7 @@ const EntityTable = ({
return (
<tr
className={classNames('tableBody-row')}
data-testid="row"
key={index}
{...row.getRowProps()}>
{/* eslint-disable-next-line */}

View File

@ -0,0 +1,309 @@
/*
* Copyright 2021 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 {
findAllByTestId,
findByTestId,
fireEvent,
queryByTestId,
render,
} from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Table } from '../../generated/entity/data/table';
import { ModifiedTableColumn } from '../../interface/dataQuality.interface';
import EntityTable from './EntityTable.component';
const mockTableheader = [
{
Header: 'Name',
accessor: 'name',
},
{
Header: 'Type',
accessor: 'dataTypeDisplay',
},
{
Header: 'Data Quality',
accessor: 'columnTests',
},
{
Header: 'Description',
accessor: 'description',
},
{
Header: 'Tags',
accessor: 'tags',
},
];
const mockEntityFieldThreads = [
{
entityLink:
'<#E/table/bigquery_gcp.shopify.raw_product_catalog/columns/products/description>',
count: 1,
entityField: 'columns/products/description',
},
];
const onEntityFieldSelect = jest.fn();
const onThreadLinkSelect = jest.fn();
const mockEntityTableProp = {
tableColumns: [
{
name: 'comments',
dataType: 'STRING',
dataLength: 1,
dataTypeDisplay: 'string',
fullyQualifiedName: 'bigquery_gcp.shopify.raw_product_catalog.comments',
tags: [],
constraint: 'NULL',
ordinalPosition: 1,
},
{
name: 'products',
dataType: 'ARRAY',
arrayDataType: 'STRUCT',
dataLength: 1,
dataTypeDisplay:
'array<struct<product_id:character varying(24),price:int,onsale:boolean,tax:int,weight:int,others:int,vendor:character varying(64), stock:int>>',
fullyQualifiedName: 'bigquery_gcp.shopify.raw_product_catalog.products',
tags: [],
constraint: 'NULL',
ordinalPosition: 2,
},
{
name: 'platform',
dataType: 'STRING',
dataLength: 1,
dataTypeDisplay: 'string',
fullyQualifiedName: 'bigquery_gcp.shopify.raw_product_catalog.platform',
tags: [],
constraint: 'NULL',
ordinalPosition: 3,
},
{
name: 'store_address',
dataType: 'ARRAY',
arrayDataType: 'STRUCT',
dataLength: 1,
dataTypeDisplay:
'array<struct<name:character varying(32),street_address:character varying(128),city:character varying(32),postcode:character varying(8)>>',
fullyQualifiedName:
'bigquery_gcp.shopify.raw_product_catalog.store_address',
tags: [],
constraint: 'NULL',
ordinalPosition: 4,
},
{
name: 'first_order_date',
dataType: 'TIMESTAMP',
dataTypeDisplay: 'timestamp',
description:
'The date (ISO 8601) and time (UTC) when the customer placed their first order. The format is YYYY-MM-DD HH:mm:ss (for example, 2016-02-05 17:04:01).',
fullyQualifiedName:
'bigquery_gcp.shopify.raw_product_catalog.first_order_date',
tags: [],
ordinalPosition: 5,
},
{
name: 'last_order_date',
dataType: 'TIMESTAMP',
dataTypeDisplay: 'timestamp',
description:
'The date (ISO 8601) and time (UTC) when the customer placed their most recent order. The format is YYYY-MM-DD HH:mm:ss (for example, 2016-02-05 17:04:01).',
fullyQualifiedName:
'bigquery_gcp.shopify.raw_product_catalog.last_order_date',
tags: [],
ordinalPosition: 6,
},
] as ModifiedTableColumn[],
searchText: '',
hasEditAccess: false,
joins: [],
entityFieldThreads: [],
isReadOnly: false,
entityFqn: 'bigquery_gcp.shopify.raw_product_catalog',
owner: {} as Table['owner'],
columnName: '',
onEntityFieldSelect,
onThreadLinkSelect,
};
jest.mock('@fortawesome/react-fontawesome', () => ({
FontAwesomeIcon: jest.fn().mockReturnValue(<i>Icon</i>),
}));
jest.mock('../common/non-admin-action/NonAdminAction', () => {
return jest.fn().mockReturnValue(<p>NonAdminAction</p>);
});
jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => {
return jest.fn().mockReturnValue(<p>RichTextEditorPreviewer</p>);
});
jest.mock('../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor', () => ({
ModalWithMarkdownEditor: jest.fn().mockReturnValue(<p>EditorModal</p>),
}));
jest.mock('../tags-container/tags-container', () => {
return jest.fn().mockReturnValue(<p>TagContainer</p>);
});
jest.mock('../tags-viewer/tags-viewer', () => {
return jest.fn().mockReturnValue(<p>TagViewer</p>);
});
jest.mock('../tags/tags', () => {
return jest.fn().mockReturnValue(<p>Tag</p>);
});
describe('Test EntityTable Component', () => {
it('Check if it has all child elements', async () => {
const { container } = render(<EntityTable {...mockEntityTableProp} />, {
wrapper: MemoryRouter,
});
const entityTable = await findByTestId(container, 'entity-table');
expect(entityTable).toBeInTheDocument();
const tableHeader = await findByTestId(container, 'table-header');
expect(tableHeader).toBeInTheDocument();
for (let index = 0; index < mockTableheader.length; index++) {
const headerValue = mockTableheader[index];
const header = await findByTestId(tableHeader, `${headerValue.accessor}`);
expect(header).toBeInTheDocument();
}
const tableBody = await findByTestId(container, 'table-body');
expect(tableBody).toBeInTheDocument();
const tableRows = await findAllByTestId(tableBody, 'row');
expect(tableRows).toHaveLength(mockEntityTableProp.tableColumns.length);
});
it('should render request description button', async () => {
const { container } = render(<EntityTable {...mockEntityTableProp} />, {
wrapper: MemoryRouter,
});
const entityTable = await findByTestId(container, 'entity-table');
expect(entityTable).toBeInTheDocument();
const tableBody = await findByTestId(container, 'table-body');
expect(tableBody).toBeInTheDocument();
const tableRows = await findAllByTestId(tableBody, 'row');
const requestDescriptionButton = await findByTestId(
tableRows[0],
'request-description'
);
expect(requestDescriptionButton).toBeInTheDocument();
fireEvent.click(
requestDescriptionButton,
new MouseEvent('click', { bubbles: true, cancelable: true })
);
expect(onEntityFieldSelect).toBeCalled();
const descriptionThread = queryByTestId(tableRows[0], 'field-thread');
const startDescriptionThread = queryByTestId(
tableRows[0],
'start-field-thread'
);
// should not be in the document, as request description button is present
expect(descriptionThread).not.toBeInTheDocument();
expect(startDescriptionThread).not.toBeInTheDocument();
});
it('Should render start thread button', async () => {
const { container } = render(<EntityTable {...mockEntityTableProp} />, {
wrapper: MemoryRouter,
});
const entityTable = await findByTestId(container, 'entity-table');
expect(entityTable).toBeInTheDocument();
const tableBody = await findByTestId(container, 'table-body');
expect(tableBody).toBeInTheDocument();
const tableRows = await findAllByTestId(tableBody, 'row');
const startThreadButton = await findByTestId(
tableRows[4],
'start-field-thread'
);
expect(startThreadButton).toBeInTheDocument();
fireEvent.click(
startThreadButton,
new MouseEvent('click', { bubbles: true, cancelable: true })
);
expect(onThreadLinkSelect).toBeCalled();
});
it('Should render thread button with count', async () => {
const { container } = render(
<EntityTable
{...mockEntityTableProp}
entityFieldThreads={mockEntityFieldThreads}
/>,
{
wrapper: MemoryRouter,
}
);
const entityTable = await findByTestId(container, 'entity-table');
expect(entityTable).toBeInTheDocument();
const tableBody = await findByTestId(container, 'table-body');
expect(tableBody).toBeInTheDocument();
const tableRows = await findAllByTestId(tableBody, 'row');
const threadButton = await findByTestId(tableRows[1], 'field-thread');
expect(threadButton).toBeInTheDocument();
fireEvent.click(
threadButton,
new MouseEvent('click', { bubbles: true, cancelable: true })
);
expect(onThreadLinkSelect).toBeCalled();
const threadCount = await findByTestId(threadButton, 'field-thread-count');
expect(threadCount).toBeInTheDocument();
expect(threadCount).toHaveTextContent(
String(mockEntityFieldThreads[0].count)
);
});
});

View File

@ -11,6 +11,11 @@
* limitations under the License.
*/
import {
faChevronLeft,
faChevronRight,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import { lowerCase } from 'lodash';
import React, {
@ -23,11 +28,6 @@ import React, {
import { TableData } from '../../generated/entity/data/table';
import { withLoader } from '../../hoc/withLoader';
import { isEven } from '../../utils/CommonUtils';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faChevronLeft,
faChevronRight,
} from '@fortawesome/free-solid-svg-icons';
export type SampleColumns = { name: string; dataType: string };
@ -70,6 +70,7 @@ const SampleDataTable: FunctionComponent<Props> = ({ sampleData }: Props) => {
return (
<div
className="tw-relative tw-flex tw-justify-between"
data-testid="sample-data"
onScrollCapture={() => {
setScrollOffSet(tableRef.current?.scrollLeft ?? 0);
}}>

View File

@ -23,7 +23,7 @@ interface TableQueriesProp extends HTMLAttributes<HTMLDivElement> {
const TableQueries: FC<TableQueriesProp> = ({ queries, className }) => {
return (
<div className={className}>
<div className={className} data-testid="table-queries">
<div className="tw-my-6" data-testid="queries-container">
{!isUndefined(queries) && queries.length > 0 ? (
queries.map((query, index) => <QueryCard key={index} query={query} />)

View File

@ -53,6 +53,7 @@ const TabsPane = ({
<div className={classNames('tw-bg-transparent tw--mx-6', className)}>
<nav
className="tw-flex tw-items-center tw-justify-between tw-gh-tabs-container tw-px-7"
data-testid="tabs"
id="tabs">
<div className="tw-flex tw-flex-grow">
{tabs.map((tab) =>
@ -64,7 +65,7 @@ const TabsPane = ({
title={TITLE_FOR_NON_OWNER_ACTION}>
<button
className={getTabClasses(tab.position, activeTab)}
data-testid="tab"
data-testid={tab.name}
id={camelCase(tab.name)}
onClick={() => setActiveTab?.(tab.position)}>
{tab.name}
@ -73,7 +74,7 @@ const TabsPane = ({
) : (
<button
className={getTabClasses(tab.position, activeTab)}
data-testid="tab"
data-testid={tab.name}
id={camelCase(tab.name)}
key={tab.position}
onClick={() => setActiveTab?.(tab.position)}>

View File

@ -0,0 +1,220 @@
/*
* Copyright 2021 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 { findByTestId, queryByTestId, render } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import Description from './Description';
const mockEntityFieldThreads = [
{
entityLink:
'<#E/table/bigquery_gcp.shopify.raw_product_catalog/description>',
count: 1,
entityField: 'description',
},
];
const mockDescriptionProp = {
description: 'description',
isEdit: false,
isReadOnly: false,
blurWithBodyBG: false,
removeBlur: false,
entityName: 'entity1',
entityFieldThreads: [],
entityType: 'xyz',
entityFqn: 'x.y.z',
onCancel: jest.fn(),
onDescriptionUpdate: jest.fn(),
onThreadLinkSelect: jest.fn(),
onEntityFieldSelect: jest.fn(),
};
jest.mock('../../../utils/CommonUtils', () => ({
getHtmlForNonAdminAction: jest.fn(),
}));
jest.mock('../../../utils/EntityUtils', () => ({
getEntityFeedLink: jest.fn(),
}));
jest.mock(
'../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor',
() => ({
ModalWithMarkdownEditor: jest
.fn()
.mockReturnValue(
<p data-testid="editor-modal">ModalWithMarkdownEditor</p>
),
})
);
jest.mock('../rich-text-editor/RichTextEditorPreviewer', () => {
return jest
.fn()
.mockReturnValue(
<p data-testid="rich-text-previewer">RichTextPreviewer</p>
);
});
jest.mock('../non-admin-action/NonAdminAction', () => {
return jest
.fn()
.mockReturnValue(<p data-testid="edit-description">NonAdminAction</p>);
});
describe('Test Description Component', () => {
it('Check if it has all child elements', async () => {
const { container } = render(<Description {...mockDescriptionProp} />, {
wrapper: MemoryRouter,
});
const descriptionContainer = await findByTestId(container, 'description');
const editDescriptionButton = await findByTestId(
container,
'edit-description'
);
expect(descriptionContainer).toBeInTheDocument();
expect(editDescriptionButton).toBeInTheDocument();
});
it('Check if it has isReadOnly as true', async () => {
const { container } = render(
<Description {...mockDescriptionProp} isReadOnly />,
{
wrapper: MemoryRouter,
}
);
const descriptionContainer = await findByTestId(container, 'description');
const editDescriptionButton = queryByTestId(container, 'edit-description');
expect(descriptionContainer).toBeInTheDocument();
expect(editDescriptionButton).not.toBeInTheDocument();
});
it('Check if it has isEdit as true', async () => {
const { container } = render(
<Description {...mockDescriptionProp} isEdit />,
{
wrapper: MemoryRouter,
}
);
const descriptionContainer = await findByTestId(container, 'description');
const editorModal = await findByTestId(container, 'editor-modal');
expect(descriptionContainer).toBeInTheDocument();
expect(editorModal).toBeInTheDocument();
});
it('Check if it has isEdit as false', async () => {
const { container } = render(
<Description {...mockDescriptionProp} isEdit={false} />,
{
wrapper: MemoryRouter,
}
);
const descriptionContainer = await findByTestId(container, 'description');
const editorModal = queryByTestId(container, 'editor-modal');
expect(descriptionContainer).toBeInTheDocument();
expect(editorModal).not.toBeInTheDocument();
});
it('Check if it has entityFieldThreads', async () => {
const { container } = render(
<Description
{...mockDescriptionProp}
entityFieldThreads={mockEntityFieldThreads}
/>,
{
wrapper: MemoryRouter,
}
);
const descriptionContainer = await findByTestId(container, 'description');
const descriptionThread = await findByTestId(
container,
'description-thread'
);
const descriptionThreadCount = await findByTestId(
descriptionThread,
'description-thread-count'
);
expect(descriptionContainer).toBeInTheDocument();
expect(descriptionThread).toBeInTheDocument();
expect(descriptionThreadCount).toBeInTheDocument();
// check for thread count
expect(descriptionThreadCount).toHaveTextContent(
String(mockEntityFieldThreads[0].count)
);
});
it('Check if it has entityFieldThreads as empty list', async () => {
const { container } = render(
<Description {...mockDescriptionProp} entityFieldThreads={[]} />,
{
wrapper: MemoryRouter,
}
);
const descriptionContainer = await findByTestId(container, 'description');
const descriptionThread = queryByTestId(container, 'description-thread');
const startDescriptionThread = await findByTestId(
container,
'start-description-thread'
);
expect(descriptionContainer).toBeInTheDocument();
expect(descriptionThread).not.toBeInTheDocument();
// should render startDescription thread button, as description thread is empty value
expect(startDescriptionThread).toBeInTheDocument();
});
it('Check if it has entityFieldThreads as empty list, description as empty string', async () => {
const { container } = render(
<Description
{...mockDescriptionProp}
description=""
entityFieldThreads={[]}
/>,
{
wrapper: MemoryRouter,
}
);
const descriptionContainer = await findByTestId(container, 'description');
const descriptionThread = queryByTestId(container, 'description-thread');
const startDescriptionThread = queryByTestId(
container,
'start-description-thread'
);
const requestDescription = await findByTestId(
container,
'request-description'
);
expect(descriptionContainer).toBeInTheDocument();
expect(descriptionThread).not.toBeInTheDocument();
expect(startDescriptionThread).not.toBeInTheDocument();
// should render requestDescription, as description thread and description are empty value
expect(requestDescription).toBeInTheDocument();
});
});

View File

@ -25,7 +25,7 @@ import NonAdminAction from '../non-admin-action/NonAdminAction';
import PopOver from '../popover/PopOver';
import RichTextEditorPreviewer from '../rich-text-editor/RichTextEditorPreviewer';
type Props = {
interface Props {
entityName?: string;
owner?: Table['owner'];
hasEditAccess?: boolean;
@ -43,7 +43,7 @@ type Props = {
onDescriptionUpdate?: (value: string) => void;
onSuggest?: (value: string) => void;
onEntityFieldSelect?: (value: string) => void;
};
}
const Description = ({
owner,
@ -143,12 +143,18 @@ const Description = ({
{!isUndefined(descriptionThread) ? (
<p
className="link-text tw-ml-2 tw-w-8 tw-h-8 tw-flex-none"
data-testid="description-thread"
onClick={() =>
onThreadLinkSelect?.(descriptionThread.entityLink)
}>
<span className="tw-flex">
<SVGIcons alt="comments" icon={Icons.COMMENT} width="20px" />{' '}
<span className="tw-ml-1"> {descriptionThread.count}</span>
<span
className="tw-ml-1"
data-testid="description-thread-count">
{' '}
{descriptionThread.count}
</span>
</span>
</p>
) : (
@ -156,6 +162,7 @@ const Description = ({
{description?.trim() && onThreadLinkSelect ? (
<p
className="link-text tw-flex-none tw-ml-2"
data-testid="start-description-thread"
onClick={() =>
onThreadLinkSelect?.(
getEntityFeedLink(entityType, entityFqn, 'description')

View File

@ -0,0 +1,434 @@
/*
* Copyright 2021 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 {
findByTestId,
findByText,
fireEvent,
queryByTestId,
render,
} from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { TagLabel } from '../../../generated/type/tagLabel';
import EntityPageInfo from './EntityPageInfo';
const mockEntityFieldThreads = [
{
entityLink: '<#E/table/bigquery_gcp.shopify.raw_product_catalog/tags>',
count: 1,
entityField: 'tags',
},
];
const followHandler = jest.fn();
const versionHandler = jest.fn();
const onThreadLinkSelect = jest.fn();
const mockTier = {
tagFQN: 'Tier.Tier1',
description: '',
source: 'Tag',
labelType: 'Manual',
state: 'Confirmed',
};
const mockInfoTags = [
{
tagFQN: 'User.Biometric',
source: 'Tag',
labelType: 'Manual',
state: 'Confirmed',
},
];
const mockEntityInfoProp = {
titleLinks: [
{
name: 'bigquery_gcp',
url: '/service/databaseServices/bigquery_gcp',
imgSrc: '/service-icon-query.png',
},
{
name: 'shopify',
url: '/database/bigquery_gcp.shopify',
},
{
name: 'raw_product_catalog',
url: '',
activeTitle: true,
},
],
hasEditAccess: false,
isFollowing: false,
isTagEditable: false,
isVersionSelected: undefined,
deleted: false,
followHandler,
followers: 0,
extraInfo: [
{
key: 'Owner',
value: '',
placeholderText: '',
isLink: false,
openInNewTab: false,
},
{
key: 'Tier',
value: '',
},
{
value: 'Usage - 0th pctile',
},
{
value: '0 queries',
},
{
key: 'Columns',
value: '6 columns',
},
],
tier: {} as TagLabel,
tags: mockInfoTags,
owner: undefined,
tagsHandler: jest.fn(),
followersList: [],
entityFqn: 'bigquery_gcp.shopify.raw_product_catalog',
entityName: 'raw_product_catalog',
entityType: 'table',
version: '0.3',
versionHandler,
entityFieldThreads: [],
onThreadLinkSelect,
};
jest.mock('../../../utils/CommonUtils', () => ({
getHtmlForNonAdminAction: jest.fn(),
}));
jest.mock('../../../utils/EntityUtils', () => ({
getEntityFeedLink: jest.fn(),
getInfoElements: jest.fn(),
}));
jest.mock('../../../utils/GlossaryUtils', () => ({
fetchGlossaryTerms: jest.fn(),
getGlossaryTermlist: jest.fn(),
}));
jest.mock('../../../utils/TableUtils', () => ({
getFollowerDetail: jest.fn(),
}));
jest.mock('../../../utils/TagsUtils', () => ({
getTagCategories: jest.fn(),
getTaglist: jest.fn(),
}));
jest.mock('../non-admin-action/NonAdminAction', () => {
return jest
.fn()
.mockReturnValue(<p data-testid="tag-action">NonAdminAction</p>);
});
jest.mock('../../tags-container/tags-container', () => {
return jest.fn().mockReturnValue(<p>TagContainer</p>);
});
jest.mock('../../tags-viewer/tags-viewer', () => {
return jest.fn().mockReturnValue(<p data-testid="info-tags">TagViewer</p>);
});
jest.mock('../../tags/tags', () => {
return jest.fn().mockReturnValue(<p data-testid="tier-tag">Tag</p>);
});
jest.mock('../avatar/Avatar', () => {
return jest.fn().mockReturnValue(<p>Avatar</p>);
});
jest.mock('./FollowersModal', () => {
return jest.fn().mockReturnValue(<p>FollowModal</p>);
});
jest.mock('../title-breadcrumb/title-breadcrumb.component', () => {
return jest.fn().mockReturnValue(<p>TitleBreadCrumb</p>);
});
describe('Test EntityPageInfo component', () => {
it('Check if it has all child elements', async () => {
const { container } = render(<EntityPageInfo {...mockEntityInfoProp} />, {
wrapper: MemoryRouter,
});
const entityPageInfoContainer = await findByTestId(
container,
'entity-page-info'
);
expect(entityPageInfoContainer).toBeInTheDocument();
const titleBreadCrumb = await findByText(
entityPageInfoContainer,
/TitleBreadCrumb/i
);
expect(titleBreadCrumb).toBeInTheDocument();
const versionButton = await findByTestId(
entityPageInfoContainer,
'version-button'
);
expect(versionButton).toBeInTheDocument();
const versionValue = await findByTestId(
entityPageInfoContainer,
'version-value'
);
expect(versionValue).toBeInTheDocument();
expect(versionValue).toHaveTextContent(mockEntityInfoProp.version);
const deleteBadge = queryByTestId(entityPageInfoContainer, 'deleted-badge');
expect(deleteBadge).not.toBeInTheDocument();
const followButton = await findByTestId(
entityPageInfoContainer,
'follow-button'
);
expect(followButton).toBeInTheDocument();
const followerValue = await findByTestId(
entityPageInfoContainer,
'follower-value'
);
expect(followerValue).toBeInTheDocument();
expect(followerValue).toHaveTextContent(
String(mockEntityInfoProp.followers)
);
const extraInfo = await findByTestId(entityPageInfoContainer, 'extrainfo');
expect(extraInfo).toBeInTheDocument();
});
it('Should call version handler on version button click', async () => {
const { container } = render(<EntityPageInfo {...mockEntityInfoProp} />, {
wrapper: MemoryRouter,
});
const entityPageInfoContainer = await findByTestId(
container,
'entity-page-info'
);
expect(entityPageInfoContainer).toBeInTheDocument();
const versionButton = await findByTestId(
entityPageInfoContainer,
'version-button'
);
expect(versionButton).toBeInTheDocument();
fireEvent.click(
versionButton,
new MouseEvent('click', { bubbles: true, cancelable: true })
);
expect(versionHandler).toBeCalled();
});
it('Should call follow handler on follow button click', async () => {
const { container } = render(<EntityPageInfo {...mockEntityInfoProp} />, {
wrapper: MemoryRouter,
});
const entityPageInfoContainer = await findByTestId(
container,
'entity-page-info'
);
expect(entityPageInfoContainer).toBeInTheDocument();
const followButton = await findByTestId(
entityPageInfoContainer,
'follow-button'
);
expect(followButton).toBeInTheDocument();
fireEvent.click(
followButton,
new MouseEvent('click', { bubbles: true, cancelable: true })
);
expect(followHandler).toBeCalled();
});
it('Should render all the extra info', async () => {
const { container } = render(<EntityPageInfo {...mockEntityInfoProp} />, {
wrapper: MemoryRouter,
});
const entityPageInfoContainer = await findByTestId(
container,
'entity-page-info'
);
expect(entityPageInfoContainer).toBeInTheDocument();
const extraInfo = await findByTestId(entityPageInfoContainer, 'extrainfo');
expect(extraInfo).toBeInTheDocument();
for (let index = 0; index < mockEntityInfoProp.extraInfo.length; index++) {
const info = mockEntityInfoProp.extraInfo[index];
const key = await findByTestId(
extraInfo,
`${info.key ? info.key : `info${index}`}`
);
expect(key).toBeInTheDocument();
}
});
it('Should render all the tags including tier tag', async () => {
const { container } = render(
<EntityPageInfo {...mockEntityInfoProp} tier={mockTier as TagLabel} />,
{
wrapper: MemoryRouter,
}
);
const entityPageInfoContainer = await findByTestId(
container,
'entity-page-info'
);
expect(entityPageInfoContainer).toBeInTheDocument();
const entityTags = await findByTestId(
entityPageInfoContainer,
'entity-tags'
);
expect(entityTags).toBeInTheDocument();
const tierTag = await findByTestId(entityTags, 'tier-tag');
expect(tierTag).toBeInTheDocument();
const infoTags = await findByTestId(entityTags, 'info-tags');
expect(infoTags).toBeInTheDocument();
});
it('Check if it has isTagEditable as true and deleted as false value', async () => {
const { container } = render(
<EntityPageInfo {...mockEntityInfoProp} isTagEditable />,
{
wrapper: MemoryRouter,
}
);
const entityPageInfoContainer = await findByTestId(
container,
'entity-page-info'
);
expect(entityPageInfoContainer).toBeInTheDocument();
const tagAction = await findByTestId(entityPageInfoContainer, 'tag-action');
// should render tag action either add tag or edit tag
expect(tagAction).toBeInTheDocument();
});
it('Should render start thread button', async () => {
const { container } = render(
<EntityPageInfo {...mockEntityInfoProp} isTagEditable />,
{
wrapper: MemoryRouter,
}
);
const entityPageInfoContainer = await findByTestId(
container,
'entity-page-info'
);
expect(entityPageInfoContainer).toBeInTheDocument();
const startThreadButton = await findByTestId(
entityPageInfoContainer,
'start-tag-thread'
);
expect(startThreadButton).toBeInTheDocument();
fireEvent.click(
startThreadButton,
new MouseEvent('click', { bubbles: true, cancelable: true })
);
expect(onThreadLinkSelect).toBeCalled();
});
it('Should render tag thread button with count', async () => {
const { container } = render(
<EntityPageInfo
{...mockEntityInfoProp}
isTagEditable
entityFieldThreads={mockEntityFieldThreads}
/>,
{
wrapper: MemoryRouter,
}
);
const entityPageInfoContainer = await findByTestId(
container,
'entity-page-info'
);
expect(entityPageInfoContainer).toBeInTheDocument();
const tagThreadButton = await findByTestId(
entityPageInfoContainer,
'tag-thread'
);
expect(tagThreadButton).toBeInTheDocument();
const tagThreadCount = await findByTestId(
tagThreadButton,
'tag-thread-count'
);
expect(tagThreadCount).toBeInTheDocument();
expect(tagThreadCount).toHaveTextContent(
String(mockEntityFieldThreads[0].count)
);
fireEvent.click(
tagThreadButton,
new MouseEvent('click', { bubbles: true, cancelable: true })
);
expect(onThreadLinkSelect).toBeCalled();
});
});

View File

@ -47,7 +47,7 @@ import TitleBreadcrumb from '../title-breadcrumb/title-breadcrumb.component';
import { TitleBreadcrumbProps } from '../title-breadcrumb/title-breadcrumb.interface';
import FollowersModal from './FollowersModal';
type Props = {
interface Props {
titleLinks: TitleBreadcrumbProps['titleLinks'];
isFollowing?: boolean;
deleted?: boolean;
@ -69,7 +69,7 @@ type Props = {
followHandler?: () => void;
tagsHandler?: (selectedTags?: Array<EntityTags>) => void;
versionHandler?: () => void;
};
}
const EntityPageInfo = ({
titleLinks,
@ -189,7 +189,10 @@ const EntityPageInfo = ({
const getVersionButton = (version: string) => {
return (
<div className="tw-flex tw-h-6 tw-ml-2 tw-mt-2" onClick={versionHandler}>
<div
className="tw-flex tw-h-6 tw-ml-2 tw-mt-2"
data-testid="version"
onClick={versionHandler}>
<span
className={classNames(
'tw-flex tw-border tw-border-primary tw-rounded',
@ -214,7 +217,7 @@ const EntityPageInfo = ({
<span
className="tw-text-xs tw-border-l-0 tw-font-medium tw-py-1 tw-px-2 tw-rounded-r tw-cursor-pointer hover:tw-underline"
data-testid="getversions">
data-testid="version-value">
{parseFloat(version).toFixed(1)}
</span>
</span>
@ -257,14 +260,16 @@ const EntityPageInfo = ({
}, [followersList]);
return (
<div>
<div data-testid="entity-page-info">
<div className="tw-flex tw-flex-col">
<div className="tw-flex tw-flex-initial tw-justify-between tw-items-start">
<div className="tw-flex tw-items-center">
<TitleBreadcrumb titleLinks={titleLinks} />
{deleted && (
<>
<div className="tw-rounded tw-bg-error-lite tw-text-error tw-font-medium tw-h-6 tw-px-2 tw-py-0.5 tw-ml-2">
<div
className="tw-rounded tw-bg-error-lite tw-text-error tw-font-medium tw-h-6 tw-px-2 tw-py-0.5 tw-ml-2"
data-testid="deleted-badge">
<FontAwesomeIcon
className="tw-mr-1"
icon={faExclamationCircle}
@ -331,7 +336,7 @@ const EntityPageInfo = ({
trigger="click">
<span
className="tw-text-xs tw-border-l-0 tw-font-medium tw-py-1 tw-px-2 tw-rounded-r tw-cursor-pointer hover:tw-underline"
data-testid="getFollowerDetail">
data-testid="follower-value">
{followers}
</span>
</PopOver>
@ -341,9 +346,14 @@ const EntityPageInfo = ({
</div>
</div>
</div>
<div className="tw-flex tw-gap-1 tw-mb-2 tw-mt-1 tw-ml-7 tw-flex-wrap tw-items-center">
<div
className="tw-flex tw-gap-1 tw-mb-2 tw-mt-1 tw-ml-7 tw-flex-wrap tw-items-center"
data-testid="extrainfo">
{extraInfo.map((info, index) => (
<span className="tw-flex tw-items-center" key={index}>
<span
className="tw-flex tw-items-center"
data-testid={info.key || `info${index}`}
key={index}>
{getInfoElements(info)}
{extraInfo.length !== 1 && index < extraInfo.length - 1 ? (
<span className="tw-mx-1.5 tw-inline-block tw-text-gray-400">
@ -355,7 +365,7 @@ const EntityPageInfo = ({
</div>
<div
className="tw-flex tw-flex-wrap tw-pt-1 tw-ml-7 tw-group"
data-testid="breadcrumb-tags">
data-testid="entity-tags">
{(!isEditable || !isTagEditable || deleted) && (
<>
{(tags.length > 0 || !isEmpty(tier)) && (
@ -435,15 +445,19 @@ const EntityPageInfo = ({
{!isUndefined(tagThread) ? (
<p
className="link-text tw-ml-1 tw-w-8 tw-flex-none"
data-testid="tag-thread"
onClick={() => onThreadLinkSelect?.(tagThread.entityLink)}>
<span className="tw-flex">
<SVGIcons alt="comments" icon={Icons.COMMENT} width="20px" />
<span className="tw-ml-1">{tagThread.count}</span>
<span className="tw-ml-1" data-testid="tag-thread-count">
{tagThread.count}
</span>
</span>
</p>
) : (
<p
className="link-text tw-self-start tw-w-8 tw-opacity-0 tw-ml-1 group-hover:tw-opacity-100 tw-flex-none"
data-testid="start-tag-thread"
onClick={() =>
onThreadLinkSelect?.(
getEntityFeedLink(entityType, entityFqn, 'tags')

View File

@ -41,6 +41,7 @@ export const getFieldThreadElement = (
return !isEmpty(threadValue) ? (
<p
className="link-text tw-w-8 tw-h-8 tw-flex-none"
data-testid="field-thread"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@ -48,7 +49,9 @@ export const getFieldThreadElement = (
}}>
<span className="tw-flex">
<SVGIcons alt="comments" icon={Icons.COMMENT} width="20px" />
<span className="tw-ml-1">{threadValue.count}</span>
<span className="tw-ml-1" data-testid="field-thread-count">
{threadValue.count}
</span>
</span>
</p>
) : (
@ -56,6 +59,7 @@ export const getFieldThreadElement = (
{entityType && entityFqn && entityField && flag ? (
<p
className="link-text tw-self-start tw-w-8 tw-h-8 tw-opacity-0 tw-ml-1 group-hover:tw-opacity-100 tw-flex-none"
data-testid="start-field-thread"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();