mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-29 10:55:09 +00:00
Merge branch 'main' into search-refactoring-1
This commit is contained in:
commit
cfcaab7add
@ -1,23 +0,0 @@
|
||||
-- Migrate individual profile data to EntityProfile format
|
||||
UPDATE profiler_data_time_series pdts
|
||||
INNER JOIN table_entity te ON (
|
||||
pdts.entityFQNHash = te.fqnHash OR
|
||||
pdts.entityFQNHash LIKE CONCAT(te.fqnHash, '.%')
|
||||
)
|
||||
SET pdts.json = JSON_OBJECT(
|
||||
'id', UUID(),
|
||||
'entityReference', JSON_OBJECT(
|
||||
'id', te.json -> '$.id',
|
||||
'type', 'table',
|
||||
'fullyQualifiedName', te.json -> '$.fullyQualifiedName',
|
||||
'name', te.name
|
||||
),
|
||||
'timestamp', pdts.timestamp,
|
||||
'profileData', pdts.json,
|
||||
'profileType',
|
||||
(CASE pdts.extension
|
||||
WHEN 'table.tableProfile' THEN 'table'
|
||||
WHEN 'table.columnProfile' THEN 'column'
|
||||
WHEN 'table.systemProfile' THEN 'system'
|
||||
END)
|
||||
);
|
@ -1,24 +0,0 @@
|
||||
-- Migrate individual profile data to EntityProfile format
|
||||
UPDATE profiler_data_time_series pdts
|
||||
SET json = jsonb_build_object(
|
||||
'id', gen_random_uuid(),
|
||||
'entityReference', jsonb_build_object(
|
||||
'id', te.json ->> 'id',
|
||||
'type', 'table',
|
||||
'fullyQualifiedName', te.json ->> 'fullyQualifiedName',
|
||||
'name', te.name
|
||||
),
|
||||
'timestamp', pdts.timestamp,
|
||||
'profileData', pdts.json,
|
||||
'profileType',
|
||||
(CASE pdts.extension
|
||||
WHEN 'table.tableProfile' THEN 'table'
|
||||
WHEN 'table.columnProfile' THEN 'column'
|
||||
WHEN 'table.systemProfile' THEN 'system'
|
||||
END)
|
||||
)
|
||||
FROM table_entity te
|
||||
WHERE (
|
||||
pdts.entityFQNHash = te.fqnHash OR
|
||||
pdts.entityFQNHash LIKE CONCAT(te.fqnHash, '.%')
|
||||
);
|
@ -0,0 +1,97 @@
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX idx_pdts_entityFQNHash ON profiler_data_time_series(entityFQNHash);
|
||||
CREATE INDEX idx_pdts_extension ON profiler_data_time_series(extension);
|
||||
CREATE INDEX idx_te_fqnHash ON table_entity(fqnHash);
|
||||
|
||||
-- Add prefix index for LIKE queries (service.database.schema.table = 4 MD5 hashes + 3 dots = 132 chars)
|
||||
CREATE INDEX idx_pdts_entityFQNHash_prefix ON profiler_data_time_series(entityFQNHash(132));
|
||||
|
||||
-- Add composite index for better join performance
|
||||
CREATE INDEX idx_pdts_composite ON profiler_data_time_series(extension, entityFQNHash);
|
||||
|
||||
-- Analyze tables for query optimizer (MySQL 8.0+)
|
||||
ANALYZE TABLE profiler_data_time_series;
|
||||
ANALYZE TABLE table_entity;
|
||||
|
||||
-- Migrate table profiles (direct match)
|
||||
UPDATE profiler_data_time_series pdts
|
||||
INNER JOIN table_entity te ON pdts.entityFQNHash = te.fqnHash
|
||||
SET pdts.json = JSON_OBJECT(
|
||||
'id', UUID(),
|
||||
'entityReference', JSON_OBJECT(
|
||||
'id', te.json -> '$.id',
|
||||
'type', 'table',
|
||||
'fullyQualifiedName', te.json -> '$.fullyQualifiedName',
|
||||
'name', te.name
|
||||
),
|
||||
'timestamp', pdts.timestamp,
|
||||
'profileData', pdts.json,
|
||||
'profileType', 'table'
|
||||
)
|
||||
WHERE pdts.extension = 'table.tableProfile';
|
||||
|
||||
-- Migrate system profiles (direct match)
|
||||
UPDATE profiler_data_time_series pdts
|
||||
INNER JOIN table_entity te ON pdts.entityFQNHash = te.fqnHash
|
||||
SET pdts.json = JSON_OBJECT(
|
||||
'id', UUID(),
|
||||
'entityReference', JSON_OBJECT(
|
||||
'id', te.json -> '$.id',
|
||||
'type', 'table',
|
||||
'fullyQualifiedName', te.json -> '$.fullyQualifiedName',
|
||||
'name', te.name
|
||||
),
|
||||
'timestamp', pdts.timestamp,
|
||||
'profileData', pdts.json,
|
||||
'profileType', 'system'
|
||||
)
|
||||
WHERE pdts.extension = 'table.systemProfile';
|
||||
|
||||
-- Migrate column profiles using temporary mapping table for better performance
|
||||
-- Create temporary mapping table to extract table hash from column hash
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS column_to_table_mapping (
|
||||
column_hash VARCHAR(768) PRIMARY KEY,
|
||||
table_hash VARCHAR(768),
|
||||
INDEX idx_table_hash (table_hash)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- Populate mapping by extracting table hash (everything before the last dot)
|
||||
INSERT INTO column_to_table_mapping (column_hash, table_hash)
|
||||
SELECT DISTINCT
|
||||
pdts.entityFQNHash as column_hash,
|
||||
SUBSTRING_INDEX(pdts.entityFQNHash, '.', 4) as table_hash
|
||||
FROM profiler_data_time_series pdts
|
||||
WHERE pdts.extension = 'table.columnProfile'
|
||||
AND CHAR_LENGTH(pdts.entityFQNHash) - CHAR_LENGTH(REPLACE(pdts.entityFQNHash, '.', '')) >= 4;
|
||||
|
||||
-- Update column profiles using the mapping (much faster than LIKE)
|
||||
UPDATE profiler_data_time_series pdts
|
||||
INNER JOIN column_to_table_mapping ctm ON pdts.entityFQNHash = ctm.column_hash
|
||||
INNER JOIN table_entity te ON ctm.table_hash = te.fqnHash
|
||||
SET pdts.json = JSON_OBJECT(
|
||||
'id', UUID(),
|
||||
'entityReference', JSON_OBJECT(
|
||||
'id', te.json -> '$.id',
|
||||
'type', 'table',
|
||||
'fullyQualifiedName', te.json -> '$.fullyQualifiedName',
|
||||
'name', te.name
|
||||
),
|
||||
'timestamp', pdts.timestamp,
|
||||
'profileData', pdts.json,
|
||||
'profileType', 'column'
|
||||
)
|
||||
WHERE pdts.extension = 'table.columnProfile';
|
||||
|
||||
-- Clean up temporary table
|
||||
DROP TEMPORARY TABLE IF EXISTS column_to_table_mapping;
|
||||
|
||||
-- Drop temporary indexes after migration
|
||||
DROP INDEX idx_pdts_entityFQNHash ON profiler_data_time_series;
|
||||
DROP INDEX idx_pdts_entityFQNHash_prefix ON profiler_data_time_series;
|
||||
DROP INDEX idx_pdts_extension ON profiler_data_time_series;
|
||||
DROP INDEX idx_te_fqnHash ON table_entity;
|
||||
DROP INDEX idx_pdts_composite ON profiler_data_time_series;
|
||||
|
||||
-- Analyze tables after migration for updated statistics
|
||||
ANALYZE TABLE profiler_data_time_series;
|
||||
ANALYZE TABLE table_entity;
|
@ -0,0 +1,118 @@
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX IF NOT EXISTS idx_pdts_entityFQNHash ON profiler_data_time_series(entityFQNHash);
|
||||
CREATE INDEX IF NOT EXISTS idx_pdts_extension ON profiler_data_time_series(extension);
|
||||
CREATE INDEX IF NOT EXISTS idx_te_fqnHash ON table_entity(fqnHash);
|
||||
|
||||
-- Add prefix index for LIKE queries (service.database.schema.table = 4 MD5 hashes + 3 dots = 132 chars)
|
||||
CREATE INDEX IF NOT EXISTS idx_pdts_entityFQNHash_prefix ON profiler_data_time_series(substring(entityFQNHash, 1, 132));
|
||||
|
||||
-- Add composite index for better join performance
|
||||
CREATE INDEX IF NOT EXISTS idx_pdts_composite ON profiler_data_time_series(extension, entityFQNHash);
|
||||
|
||||
-- Analyze tables for query planner optimization
|
||||
ANALYZE profiler_data_time_series;
|
||||
ANALYZE table_entity;
|
||||
|
||||
-- Set work_mem higher temporarily for better sort performance (session level)
|
||||
SET LOCAL work_mem = '256MB';
|
||||
SET LOCAL maintenance_work_mem = '512MB';
|
||||
|
||||
-- Migrate table profiles (direct match)
|
||||
UPDATE profiler_data_time_series pdts
|
||||
SET json = jsonb_build_object(
|
||||
'id', gen_random_uuid(),
|
||||
'entityReference', jsonb_build_object(
|
||||
'id', te.json ->> 'id',
|
||||
'type', 'table',
|
||||
'fullyQualifiedName', te.json ->> 'fullyQualifiedName',
|
||||
'name', te.name
|
||||
),
|
||||
'timestamp', pdts.timestamp,
|
||||
'profileData', pdts.json,
|
||||
'profileType', 'table'
|
||||
)
|
||||
FROM table_entity te
|
||||
WHERE pdts.entityFQNHash = te.fqnHash
|
||||
AND pdts.extension = 'table.tableProfile';
|
||||
|
||||
-- Migrate system profiles (direct match)
|
||||
UPDATE profiler_data_time_series pdts
|
||||
SET json = jsonb_build_object(
|
||||
'id', gen_random_uuid(),
|
||||
'entityReference', jsonb_build_object(
|
||||
'id', te.json ->> 'id',
|
||||
'type', 'table',
|
||||
'fullyQualifiedName', te.json ->> 'fullyQualifiedName',
|
||||
'name', te.name
|
||||
),
|
||||
'timestamp', pdts.timestamp,
|
||||
'profileData', pdts.json,
|
||||
'profileType', 'system'
|
||||
)
|
||||
FROM table_entity te
|
||||
WHERE pdts.entityFQNHash = te.fqnHash
|
||||
AND pdts.extension = 'table.systemProfile';
|
||||
|
||||
-- Migrate column profiles using temporary mapping table for better performance
|
||||
-- Use UNLOGGED table for memory-like performance (no WAL writes)
|
||||
CREATE UNLOGGED TABLE IF NOT EXISTS column_to_table_mapping (
|
||||
column_hash VARCHAR(768) PRIMARY KEY,
|
||||
table_hash VARCHAR(768)
|
||||
);
|
||||
CREATE INDEX idx_ctm_table_hash ON column_to_table_mapping(table_hash);
|
||||
|
||||
-- Optimize for in-memory operations
|
||||
ALTER TABLE column_to_table_mapping SET (autovacuum_enabled = false);
|
||||
SET LOCAL temp_buffers = '256MB'; -- Increase temp buffer size
|
||||
SET LOCAL work_mem = '256MB'; -- Already set above but ensuring it's set
|
||||
|
||||
-- Populate mapping by extracting table hash (first 4 dot-separated parts)
|
||||
INSERT INTO column_to_table_mapping (column_hash, table_hash)
|
||||
SELECT DISTINCT
|
||||
pdts.entityFQNHash as column_hash,
|
||||
SPLIT_PART(pdts.entityFQNHash, '.', 1) || '.' ||
|
||||
SPLIT_PART(pdts.entityFQNHash, '.', 2) || '.' ||
|
||||
SPLIT_PART(pdts.entityFQNHash, '.', 3) || '.' ||
|
||||
SPLIT_PART(pdts.entityFQNHash, '.', 4) as table_hash
|
||||
FROM profiler_data_time_series pdts
|
||||
WHERE pdts.extension = 'table.columnProfile'
|
||||
AND ARRAY_LENGTH(STRING_TO_ARRAY(pdts.entityFQNHash, '.'), 1) >= 5;
|
||||
|
||||
-- Update column profiles using the mapping (much faster than LIKE)
|
||||
UPDATE profiler_data_time_series pdts
|
||||
SET json = jsonb_build_object(
|
||||
'id', gen_random_uuid(),
|
||||
'entityReference', jsonb_build_object(
|
||||
'id', te.json ->> 'id',
|
||||
'type', 'table',
|
||||
'fullyQualifiedName', te.json ->> 'fullyQualifiedName',
|
||||
'name', te.name
|
||||
),
|
||||
'timestamp', pdts.timestamp,
|
||||
'profileData', pdts.json,
|
||||
'profileType', 'column'
|
||||
)
|
||||
FROM column_to_table_mapping ctm
|
||||
INNER JOIN table_entity te ON ctm.table_hash = te.fqnHash
|
||||
WHERE pdts.entityFQNHash = ctm.column_hash
|
||||
AND pdts.extension = 'table.columnProfile';
|
||||
|
||||
-- Clean up temporary table
|
||||
DROP TABLE IF EXISTS column_to_table_mapping;
|
||||
|
||||
-- Reset temp buffers
|
||||
RESET temp_buffers;
|
||||
|
||||
-- Drop temporary indexes after migration
|
||||
DROP INDEX IF EXISTS idx_pdts_entityFQNHash;
|
||||
DROP INDEX IF EXISTS idx_pdts_entityFQNHash_prefix;
|
||||
DROP INDEX IF EXISTS idx_pdts_extension;
|
||||
DROP INDEX IF EXISTS idx_te_fqnHash;
|
||||
DROP INDEX IF EXISTS idx_pdts_composite;
|
||||
|
||||
-- Reset work_mem to default
|
||||
RESET work_mem;
|
||||
RESET maintenance_work_mem;
|
||||
|
||||
-- Analyze tables after migration for updated statistics
|
||||
ANALYZE profiler_data_time_series;
|
@ -1,10 +1,10 @@
|
||||
package org.openmetadata.service.migration.mysql.v198;
|
||||
package org.openmetadata.service.migration.mysql.v199;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.openmetadata.service.jdbi3.locator.ConnectionType;
|
||||
import org.openmetadata.service.migration.api.MigrationProcessImpl;
|
||||
import org.openmetadata.service.migration.utils.MigrationFile;
|
||||
import org.openmetadata.service.migration.utils.v198.MigrationUtil;
|
||||
import org.openmetadata.service.migration.utils.v199.MigrationUtil;
|
||||
|
||||
public class Migration extends MigrationProcessImpl {
|
||||
|
@ -1,10 +1,10 @@
|
||||
package org.openmetadata.service.migration.postgres.v198;
|
||||
package org.openmetadata.service.migration.postgres.v199;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.openmetadata.service.jdbi3.locator.ConnectionType;
|
||||
import org.openmetadata.service.migration.api.MigrationProcessImpl;
|
||||
import org.openmetadata.service.migration.utils.MigrationFile;
|
||||
import org.openmetadata.service.migration.utils.v198.MigrationUtil;
|
||||
import org.openmetadata.service.migration.utils.v199.MigrationUtil;
|
||||
|
||||
public class Migration extends MigrationProcessImpl {
|
||||
|
@ -1,4 +1,4 @@
|
||||
package org.openmetadata.service.migration.utils.v198;
|
||||
package org.openmetadata.service.migration.utils.v199;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
@ -34,7 +34,7 @@ public class MigrationUtil {
|
||||
LOG.info("Successfully completed user activity columns migration");
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Error running user activity columns migration", ex);
|
||||
throw new RuntimeException("Migration v198 failed", ex);
|
||||
throw new RuntimeException("Migration v199 failed", ex);
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,8 @@
|
||||
import { test as base, expect, Page } from '@playwright/test';
|
||||
import { SidebarItem } from '../../constant/sidebar';
|
||||
import { Domain } from '../../support/domain/Domain';
|
||||
import { AdminClass } from '../../support/user/AdminClass';
|
||||
import { performAdminLogin } from '../../utils/admin';
|
||||
import { getApiContext, redirectToHomePage } from '../../utils/common';
|
||||
import { redirectToHomePage } from '../../utils/common';
|
||||
import {
|
||||
addAssetsToDomain,
|
||||
addServicesToDomain,
|
||||
@ -31,21 +30,16 @@ const test = base.extend<{
|
||||
ingestionBotPage: Page;
|
||||
}>({
|
||||
page: async ({ browser }, use) => {
|
||||
const adminUser = new AdminClass();
|
||||
const adminPage = await browser.newPage();
|
||||
await adminUser.login(adminPage);
|
||||
await use(adminPage);
|
||||
await adminPage.close();
|
||||
const { afterAction, page } = await performAdminLogin(browser);
|
||||
|
||||
await use(page);
|
||||
await afterAction();
|
||||
},
|
||||
ingestionBotPage: async ({ browser }, use) => {
|
||||
const admin = new AdminClass();
|
||||
const { apiContext, afterAction } = await performAdminLogin(browser);
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// login with admin user
|
||||
await admin.login(page);
|
||||
await page.waitForURL('**/my-data');
|
||||
|
||||
const { apiContext } = await getApiContext(page);
|
||||
await page.goto('/');
|
||||
|
||||
const bot = await apiContext
|
||||
.get('/api/v1/bots/name/ingestion-bot')
|
||||
@ -55,10 +49,15 @@ const test = base.extend<{
|
||||
.then((response) => response.json());
|
||||
|
||||
await setToken(page, tokenData.config.JWTToken);
|
||||
await redirectToHomePage(page);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('loader', { state: 'hidden' });
|
||||
|
||||
await expect(page.getByTestId('nav-user-name')).toHaveText('ingestion-bot');
|
||||
|
||||
// await afterAction();
|
||||
await use(page);
|
||||
await page.close();
|
||||
await afterAction();
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -174,9 +174,8 @@ test.describe('Domains', () => {
|
||||
});
|
||||
|
||||
await test.step('Add assets to domain', async () => {
|
||||
await redirectToHomePage(page);
|
||||
await sidebarClick(page, SidebarItem.DOMAIN);
|
||||
await addAssetsToDomain(page, domain, assets);
|
||||
await page.getByTestId('assets').click();
|
||||
await addAssetsToDomain(page, domain, assets, false);
|
||||
});
|
||||
|
||||
await test.step('Delete domain using delete modal', async () => {
|
||||
|
@ -59,6 +59,7 @@ import {
|
||||
filterStatus,
|
||||
goToAssetsTab,
|
||||
openColumnDropdown,
|
||||
performExpandAll,
|
||||
renameGlossaryTerm,
|
||||
selectActiveGlossary,
|
||||
selectActiveGlossaryTerm,
|
||||
@ -823,11 +824,7 @@ test.describe('Glossary tests', () => {
|
||||
})
|
||||
).not.toBeVisible();
|
||||
|
||||
const termRes = page.waitForResponse('/api/v1/glossaryTerms?*');
|
||||
|
||||
// verify the term is moved under the parent term
|
||||
await page.getByTestId('expand-collapse-all-button').click();
|
||||
await termRes;
|
||||
await performExpandAll(page);
|
||||
|
||||
await expect(
|
||||
page.getByRole('cell', {
|
||||
@ -842,7 +839,7 @@ test.describe('Glossary tests', () => {
|
||||
await redirectToHomePage(page);
|
||||
await sidebarClick(page, SidebarItem.GLOSSARY);
|
||||
await selectActiveGlossary(page, glossary1.data.displayName);
|
||||
await page.getByTestId('expand-collapse-all-button').click();
|
||||
await performExpandAll(page);
|
||||
|
||||
await dragAndDropTerm(
|
||||
page,
|
||||
@ -918,11 +915,7 @@ test.describe('Glossary tests', () => {
|
||||
})
|
||||
).not.toBeVisible();
|
||||
|
||||
const termRes = page.waitForResponse('/api/v1/glossaryTerms?*');
|
||||
|
||||
// verify the term is moved under the parent term
|
||||
await page.getByTestId('expand-collapse-all-button').click();
|
||||
await termRes;
|
||||
await performExpandAll(page);
|
||||
|
||||
await expect(
|
||||
page.getByRole('cell', {
|
||||
@ -971,11 +964,7 @@ test.describe('Glossary tests', () => {
|
||||
})
|
||||
).not.toBeVisible();
|
||||
|
||||
const termRes = page.waitForResponse('/api/v1/glossaryTerms?*');
|
||||
|
||||
// verify the term is moved under the parent term
|
||||
await page.getByTestId('expand-collapse-all-button').click();
|
||||
await termRes;
|
||||
await performExpandAll(page);
|
||||
|
||||
await expect(
|
||||
page.getByRole('cell', {
|
||||
@ -1029,12 +1018,7 @@ test.describe('Glossary tests', () => {
|
||||
|
||||
await selectActiveGlossary(page, glossary2.data.displayName);
|
||||
|
||||
const termRes = page.waitForResponse(
|
||||
'/api/v1/glossaryTerms?directChildrenOf=*&fields=childrenCount%2Cowners%2Creviewers&limit=1000'
|
||||
);
|
||||
// verify the term is moved to the destination glossary
|
||||
await page.getByTestId('expand-collapse-all-button').click();
|
||||
await termRes;
|
||||
await performExpandAll(page);
|
||||
|
||||
await expect(
|
||||
page.getByRole('cell', {
|
||||
@ -1244,11 +1228,7 @@ test.describe('Glossary tests', () => {
|
||||
await selectActiveGlossaryTerm(page, glossaryTerm1.data.displayName);
|
||||
await page.getByTestId('terms').click();
|
||||
|
||||
const termRes = page.waitForResponse(
|
||||
'/api/v1/glossaryTerms?directChildrenOf=*&fields=childrenCount%2Cowners%2Creviewers&limit=1000'
|
||||
);
|
||||
await page.getByTestId('expand-collapse-all-button').click();
|
||||
await termRes;
|
||||
await performExpandAll(page);
|
||||
|
||||
await expect(
|
||||
page.getByRole('cell', { name: glossaryTerm2.data.displayName })
|
||||
@ -1440,11 +1420,7 @@ test.describe('Glossary tests', () => {
|
||||
await sidebarClick(page, SidebarItem.GLOSSARY);
|
||||
await selectActiveGlossary(page, glossary1.data.displayName);
|
||||
|
||||
const termRes = page.waitForResponse(
|
||||
'/api/v1/glossaryTerms?directChildrenOf=*&fields=childrenCount%2Cowners%2Creviewers&limit=1000'
|
||||
);
|
||||
await page.getByTestId('expand-collapse-all-button').click();
|
||||
await termRes;
|
||||
await performExpandAll(page);
|
||||
|
||||
await expect(
|
||||
page.getByRole('cell', { name: glossaryTerm1.data.displayName })
|
||||
|
@ -186,6 +186,10 @@ setup('authenticate all users', async ({ browser }) => {
|
||||
.storageState({ path: ownerFile, indexedDB: true });
|
||||
|
||||
await afterAction();
|
||||
|
||||
if (newAdminPage) {
|
||||
await newAdminPage.close();
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error during authentication setup:', error);
|
||||
|
@ -394,21 +394,6 @@ class ServiceBaseClass {
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector(`td:has-text("${ingestionType}")`);
|
||||
|
||||
const pipelineStatus = await page
|
||||
.locator(`[data-row-key*="${workflowData.name}"]`)
|
||||
.getByTestId('pipeline-status')
|
||||
.last()
|
||||
.textContent();
|
||||
// add logs to console for failed pipelines
|
||||
if (pipelineStatus?.toLowerCase() === 'failed') {
|
||||
const logsResponse = await apiContext
|
||||
.get(`/api/v1/services/ingestionPipelines/logs/${workflowData.id}/last`)
|
||||
.then((res) => res.json());
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(logsResponse);
|
||||
}
|
||||
|
||||
await expect(
|
||||
page
|
||||
.locator(`[data-row-key*="${workflowData.name}"]`)
|
||||
|
@ -570,7 +570,7 @@ export const addCustomPropertiesForEntity = async ({
|
||||
}: {
|
||||
page: Page;
|
||||
propertyName: string;
|
||||
customPropertyData: { description: string };
|
||||
customPropertyData: { description: string; entityApiType: string };
|
||||
customType: string;
|
||||
enumConfig?: { values: string[]; multiSelect: boolean };
|
||||
formatConfig?: string;
|
||||
@ -580,6 +580,20 @@ export const addCustomPropertiesForEntity = async ({
|
||||
// Add Custom property for selected entity
|
||||
await page.click('[data-testid="add-field-button"]');
|
||||
|
||||
// Assert that breadcrumb has correct link for the entity type
|
||||
// The second breadcrumb item should be "Custom Attributes" with the correct entity type in URL
|
||||
const customAttributesBreadcrumb = page.locator(
|
||||
'[data-testid="breadcrumb-link"]:nth-child(2) a'
|
||||
);
|
||||
|
||||
if (customPropertyData.entityApiType) {
|
||||
// Verify that the Custom Attributes breadcrumb link contains the correct entity type
|
||||
await expect(customAttributesBreadcrumb).toHaveAttribute(
|
||||
'href',
|
||||
`/settings/customProperties/${customPropertyData.entityApiType}`
|
||||
);
|
||||
}
|
||||
|
||||
// Trigger validation
|
||||
await page.click('[data-testid="create-button"]');
|
||||
|
||||
|
@ -10,18 +10,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/*
|
||||
* 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 test, { APIRequestContext, expect, Page } from '@playwright/test';
|
||||
import { get, isEmpty, isUndefined } from 'lodash';
|
||||
import { SidebarItem } from '../constant/sidebar';
|
||||
|
@ -1682,3 +1682,11 @@ export const setupGlossaryDenyPermissionTest = async (
|
||||
cleanup,
|
||||
};
|
||||
};
|
||||
|
||||
export const performExpandAll = async (page: Page) => {
|
||||
const termRes = page.waitForResponse(
|
||||
'/api/v1/glossaryTerms?directChildrenOf=*&fields=childrenCount%2Cowners%2Creviewers*'
|
||||
);
|
||||
await page.getByTestId('expand-collapse-all-button').click();
|
||||
await termRes;
|
||||
};
|
||||
|
@ -12,45 +12,45 @@
|
||||
*/
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { Form } from 'antd';
|
||||
import React from 'react';
|
||||
import { TestCase } from '../../../../generated/tests/testCase';
|
||||
import { TestSuite } from '../../../../generated/tests/testSuite';
|
||||
import { AddTestSuitePipelineProps } from '../AddDataQualityTest.interface';
|
||||
import AddTestSuitePipeline from './AddTestSuitePipeline';
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
const mockUseCustomLocation = jest.fn();
|
||||
const mockUseFqn = jest.fn();
|
||||
const mockScheduleInterval = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useNavigate: () => mockNavigate,
|
||||
}));
|
||||
|
||||
jest.mock('../../../../hooks/useCustomLocation/useCustomLocation', () =>
|
||||
jest.fn().mockImplementation(() => mockUseCustomLocation())
|
||||
);
|
||||
|
||||
jest.mock('../../../../hooks/useCustomLocation/useCustomLocation', () => {
|
||||
return jest.fn().mockImplementation(() => ({
|
||||
search: `?testSuiteId=test-suite-id`,
|
||||
}));
|
||||
});
|
||||
jest.mock('../../../../hooks/useFqn', () => ({
|
||||
useFqn: jest.fn().mockReturnValue({ fqn: 'test-suite-fqn' }),
|
||||
useFqn: jest.fn().mockImplementation(() => mockUseFqn()),
|
||||
}));
|
||||
|
||||
jest.mock('../../AddTestCaseList/AddTestCaseList.component', () => ({
|
||||
AddTestCaseList: jest
|
||||
.fn()
|
||||
.mockImplementation(() => <div>AddTestCaseList.component</div>),
|
||||
AddTestCaseList: () => <div>AddTestCaseList.component</div>,
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'../../../Settings/Services/AddIngestion/Steps/ScheduleInterval',
|
||||
() =>
|
||||
jest
|
||||
.fn()
|
||||
.mockImplementation(({ children, topChildren, onDeploy, onBack }) => (
|
||||
<div>
|
||||
ScheduleInterval
|
||||
{topChildren}
|
||||
{children}
|
||||
<div onClick={onDeploy}>submit</div>
|
||||
<div onClick={onBack}>cancel</div>
|
||||
</div>
|
||||
))
|
||||
() => jest.fn().mockImplementation((props) => mockScheduleInterval(props))
|
||||
);
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useNavigate: jest.fn().mockImplementation(() => mockNavigate),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../utils/SchedularUtils', () => ({
|
||||
getRaiseOnErrorFormField: jest.fn().mockReturnValue({}),
|
||||
getRaiseOnErrorFormField: () => ({
|
||||
name: 'raiseOnError',
|
||||
label: 'Raise On Error',
|
||||
type: 'switch',
|
||||
required: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockProps: AddTestSuitePipelineProps = {
|
||||
@ -59,6 +59,25 @@ const mockProps: AddTestSuitePipelineProps = {
|
||||
};
|
||||
|
||||
describe('AddTestSuitePipeline', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUseCustomLocation.mockReturnValue({
|
||||
search: '?testSuiteId=test-suite-id',
|
||||
});
|
||||
mockUseFqn.mockReturnValue({ ingestionFQN: '' });
|
||||
mockScheduleInterval.mockImplementation(
|
||||
({ children, topChildren, onDeploy, onBack }) => (
|
||||
<div>
|
||||
ScheduleInterval
|
||||
{topChildren}
|
||||
{children}
|
||||
<div onClick={onDeploy}>submit</div>
|
||||
<div onClick={onBack}>cancel</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('renders form fields', () => {
|
||||
render(
|
||||
<Form>
|
||||
@ -66,7 +85,6 @@ describe('AddTestSuitePipeline', () => {
|
||||
</Form>
|
||||
);
|
||||
|
||||
// Assert that the form fields are rendered
|
||||
expect(screen.getByTestId('pipeline-name')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('select-all-test-cases')).toBeInTheDocument();
|
||||
expect(screen.getByText('submit')).toBeInTheDocument();
|
||||
@ -90,7 +108,6 @@ describe('AddTestSuitePipeline', () => {
|
||||
fireEvent.click(screen.getByText('submit'));
|
||||
});
|
||||
|
||||
// Assert that onSubmit is called with the correct values
|
||||
expect(mockProps.onSubmit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -130,7 +147,7 @@ describe('AddTestSuitePipeline', () => {
|
||||
onClick={() =>
|
||||
onFormChange('', {
|
||||
forms: {
|
||||
['schedular-form']: {
|
||||
'schedular-form': {
|
||||
getFieldValue: jest.fn().mockImplementation(() => true),
|
||||
setFieldsValue: jest.fn(),
|
||||
},
|
||||
@ -147,15 +164,483 @@ describe('AddTestSuitePipeline', () => {
|
||||
</Form>
|
||||
);
|
||||
|
||||
// Assert that AddTestCaseList.component is now visible
|
||||
expect(screen.getByText('AddTestCaseList.component')).toBeInTheDocument();
|
||||
|
||||
// Click on the select-all-test-cases switch
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('select-all-test-cases'));
|
||||
});
|
||||
|
||||
// Assert that AddTestCaseList.component is not initially visible
|
||||
expect(screen.queryByText('AddTestCaseList.component')).toBeNull();
|
||||
});
|
||||
|
||||
describe('raiseOnError functionality', () => {
|
||||
it('includes raiseOnError field in form submission', async () => {
|
||||
const mockOnSubmit = jest.fn();
|
||||
render(
|
||||
<Form>
|
||||
<AddTestSuitePipeline {...mockProps} onSubmit={mockOnSubmit} />
|
||||
</Form>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('submit'));
|
||||
});
|
||||
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
raiseOnError: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('passes raiseOnError value from form to onSubmit', async () => {
|
||||
const mockOnSubmit = jest.fn();
|
||||
const initialData = {
|
||||
raiseOnError: true,
|
||||
selectAllTestCases: true,
|
||||
};
|
||||
|
||||
mockScheduleInterval.mockImplementationOnce(
|
||||
({
|
||||
children,
|
||||
onDeploy,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onDeploy: (values: unknown) => void;
|
||||
}) => (
|
||||
<div>
|
||||
{children}
|
||||
<div
|
||||
onClick={() =>
|
||||
onDeploy({
|
||||
raiseOnError: true,
|
||||
selectAllTestCases: true,
|
||||
})
|
||||
}>
|
||||
submit
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
render(
|
||||
<Form>
|
||||
<AddTestSuitePipeline
|
||||
{...mockProps}
|
||||
initialData={initialData}
|
||||
onSubmit={mockOnSubmit}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('submit'));
|
||||
});
|
||||
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
raiseOnError: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('testCase mapping logic', () => {
|
||||
it('maps TestCase objects to string names', async () => {
|
||||
const mockOnSubmit = jest.fn();
|
||||
const testCaseObject: TestCase = {
|
||||
name: 'test-case-object',
|
||||
id: '123',
|
||||
fullyQualifiedName: 'test.case.object',
|
||||
} as TestCase;
|
||||
|
||||
mockScheduleInterval.mockImplementationOnce(
|
||||
({
|
||||
children,
|
||||
onDeploy,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onDeploy: (values: unknown) => void;
|
||||
}) => (
|
||||
<div>
|
||||
{children}
|
||||
<div
|
||||
onClick={() =>
|
||||
onDeploy({
|
||||
testCases: [testCaseObject, 'test-case-string'],
|
||||
})
|
||||
}>
|
||||
submit
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
render(
|
||||
<Form>
|
||||
<AddTestSuitePipeline {...mockProps} onSubmit={mockOnSubmit} />
|
||||
</Form>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('submit'));
|
||||
});
|
||||
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
testCases: ['test-case-object', 'test-case-string'],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('handles undefined testCases array', async () => {
|
||||
const mockOnSubmit = jest.fn();
|
||||
|
||||
mockScheduleInterval.mockImplementationOnce(
|
||||
({
|
||||
children,
|
||||
onDeploy,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onDeploy: (values: unknown) => void;
|
||||
}) => (
|
||||
<div>
|
||||
{children}
|
||||
<div
|
||||
onClick={() =>
|
||||
onDeploy({
|
||||
testCases: undefined,
|
||||
selectAllTestCases: true,
|
||||
})
|
||||
}>
|
||||
submit
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
render(
|
||||
<Form>
|
||||
<AddTestSuitePipeline
|
||||
{...mockProps}
|
||||
initialData={{ selectAllTestCases: true }}
|
||||
onSubmit={mockOnSubmit}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('submit'));
|
||||
});
|
||||
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
testCases: undefined,
|
||||
selectAllTestCases: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('handles mixed array of TestCase objects and strings', async () => {
|
||||
const mockOnSubmit = jest.fn();
|
||||
const testCase1: TestCase = {
|
||||
name: 'test-case-1',
|
||||
id: '1',
|
||||
fullyQualifiedName: 'test.case.1',
|
||||
} as TestCase;
|
||||
const testCase2: TestCase = {
|
||||
name: 'test-case-2',
|
||||
id: '2',
|
||||
fullyQualifiedName: 'test.case.2',
|
||||
} as TestCase;
|
||||
|
||||
mockScheduleInterval.mockImplementationOnce(
|
||||
({
|
||||
children,
|
||||
onDeploy,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onDeploy: (values: unknown) => void;
|
||||
}) => (
|
||||
<div>
|
||||
{children}
|
||||
<div
|
||||
onClick={() =>
|
||||
onDeploy({
|
||||
testCases: [testCase1, 'string-test', testCase2],
|
||||
})
|
||||
}>
|
||||
submit
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
render(
|
||||
<Form>
|
||||
<AddTestSuitePipeline {...mockProps} onSubmit={mockOnSubmit} />
|
||||
</Form>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('submit'));
|
||||
});
|
||||
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
testCases: ['test-case-1', 'string-test', 'test-case-2'],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('testSuiteId extraction', () => {
|
||||
it('uses testSuiteId from testSuite prop when available', () => {
|
||||
const testSuite = { id: 'prop-test-suite-id' } as TestSuite;
|
||||
|
||||
render(
|
||||
<Form>
|
||||
<AddTestSuitePipeline {...mockProps} testSuite={testSuite} />
|
||||
</Form>
|
||||
);
|
||||
|
||||
expect(screen.getByText('AddTestCaseList.component')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('extracts testSuiteId from URL search params when testSuite prop is not provided', () => {
|
||||
mockUseCustomLocation.mockReturnValueOnce({
|
||||
search: '?testSuiteId=url-test-suite-id',
|
||||
});
|
||||
|
||||
render(
|
||||
<Form>
|
||||
<AddTestSuitePipeline {...mockProps} />
|
||||
</Form>
|
||||
);
|
||||
|
||||
expect(screen.getByText('AddTestCaseList.component')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles URL search params without question mark', () => {
|
||||
mockUseCustomLocation.mockReturnValueOnce({
|
||||
search: 'testSuiteId=no-question-mark-id',
|
||||
});
|
||||
|
||||
render(
|
||||
<Form>
|
||||
<AddTestSuitePipeline {...mockProps} />
|
||||
</Form>
|
||||
);
|
||||
|
||||
expect(screen.getByText('AddTestCaseList.component')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('prioritizes testSuite prop over URL params', () => {
|
||||
mockUseCustomLocation.mockReturnValueOnce({
|
||||
search: '?testSuiteId=url-id',
|
||||
});
|
||||
|
||||
const testSuite = { id: 'prop-id' } as TestSuite;
|
||||
|
||||
render(
|
||||
<Form>
|
||||
<AddTestSuitePipeline {...mockProps} testSuite={testSuite} />
|
||||
</Form>
|
||||
);
|
||||
|
||||
expect(screen.getByText('AddTestCaseList.component')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form state management', () => {
|
||||
it('clears testCases field when selectAllTestCases is enabled', async () => {
|
||||
const mockSetFieldsValue = jest.fn();
|
||||
const mockGetFieldValue = jest.fn().mockReturnValue(true);
|
||||
|
||||
jest.spyOn(Form, 'Provider').mockImplementation(
|
||||
jest.fn().mockImplementation(({ onFormChange, children }) => (
|
||||
<div>
|
||||
{children}
|
||||
<button
|
||||
data-testid="trigger-form-change"
|
||||
onClick={() =>
|
||||
onFormChange('', {
|
||||
forms: {
|
||||
'schedular-form': {
|
||||
getFieldValue: mockGetFieldValue,
|
||||
setFieldsValue: mockSetFieldsValue,
|
||||
},
|
||||
},
|
||||
})
|
||||
}>
|
||||
Trigger Change
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
|
||||
render(
|
||||
<Form>
|
||||
<AddTestSuitePipeline {...mockProps} />
|
||||
</Form>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('trigger-form-change'));
|
||||
});
|
||||
|
||||
expect(mockGetFieldValue).toHaveBeenCalledWith('selectAllTestCases');
|
||||
expect(mockSetFieldsValue).toHaveBeenCalledWith({ testCases: undefined });
|
||||
});
|
||||
|
||||
it('does not clear testCases when selectAllTestCases is false', async () => {
|
||||
const mockSetFieldsValue = jest.fn();
|
||||
const mockGetFieldValue = jest.fn().mockReturnValue(false);
|
||||
|
||||
jest.spyOn(Form, 'Provider').mockImplementation(
|
||||
jest.fn().mockImplementation(({ onFormChange, children }) => (
|
||||
<div>
|
||||
{children}
|
||||
<button
|
||||
data-testid="trigger-form-change"
|
||||
onClick={() =>
|
||||
onFormChange('', {
|
||||
forms: {
|
||||
'schedular-form': {
|
||||
getFieldValue: mockGetFieldValue,
|
||||
setFieldsValue: mockSetFieldsValue,
|
||||
},
|
||||
},
|
||||
})
|
||||
}>
|
||||
Trigger Change
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
|
||||
render(
|
||||
<Form>
|
||||
<AddTestSuitePipeline {...mockProps} />
|
||||
</Form>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('trigger-form-change'));
|
||||
});
|
||||
|
||||
expect(mockGetFieldValue).toHaveBeenCalledWith('selectAllTestCases');
|
||||
expect(mockSetFieldsValue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates selectAllTestCases state when form changes', async () => {
|
||||
const { rerender } = render(
|
||||
<Form>
|
||||
<AddTestSuitePipeline {...mockProps} />
|
||||
</Form>
|
||||
);
|
||||
|
||||
expect(screen.getByText('AddTestCaseList.component')).toBeInTheDocument();
|
||||
|
||||
const propsWithInitialData = {
|
||||
...mockProps,
|
||||
initialData: { selectAllTestCases: true },
|
||||
};
|
||||
|
||||
rerender(
|
||||
<Form>
|
||||
<AddTestSuitePipeline {...propsWithInitialData} />
|
||||
</Form>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
// Form state should reflect the initial data
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit mode behavior', () => {
|
||||
it('displays Save button in edit mode', () => {
|
||||
mockUseFqn.mockReturnValueOnce({ ingestionFQN: 'test-ingestion-fqn' });
|
||||
|
||||
render(
|
||||
<Form>
|
||||
<AddTestSuitePipeline {...mockProps} />
|
||||
</Form>
|
||||
);
|
||||
|
||||
expect(screen.getByText('ScheduleInterval')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays Create button when not in edit mode', () => {
|
||||
mockUseFqn.mockReturnValueOnce({ ingestionFQN: '' });
|
||||
|
||||
render(
|
||||
<Form>
|
||||
<AddTestSuitePipeline {...mockProps} />
|
||||
</Form>
|
||||
);
|
||||
|
||||
expect(screen.getByText('ScheduleInterval')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form submission with all fields', () => {
|
||||
it('submits form with all populated fields', async () => {
|
||||
const mockOnSubmit = jest.fn();
|
||||
const initialData = {
|
||||
name: 'Test Pipeline',
|
||||
cron: '0 0 * * *',
|
||||
enableDebugLog: true,
|
||||
selectAllTestCases: false,
|
||||
raiseOnError: true,
|
||||
};
|
||||
|
||||
mockScheduleInterval.mockImplementationOnce(
|
||||
({
|
||||
children,
|
||||
onDeploy,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onDeploy: (values: unknown) => void;
|
||||
}) => (
|
||||
<div>
|
||||
{children}
|
||||
<div
|
||||
onClick={() =>
|
||||
onDeploy({
|
||||
...initialData,
|
||||
testCases: ['test-1', 'test-2'],
|
||||
})
|
||||
}>
|
||||
submit
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
render(
|
||||
<Form>
|
||||
<AddTestSuitePipeline
|
||||
{...mockProps}
|
||||
initialData={initialData}
|
||||
onSubmit={mockOnSubmit}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('submit'));
|
||||
});
|
||||
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith({
|
||||
name: 'Test Pipeline',
|
||||
cron: '0 0 * * *',
|
||||
enableDebugLog: true,
|
||||
selectAllTestCases: false,
|
||||
testCases: ['test-1', 'test-2'],
|
||||
raiseOnError: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -11,14 +11,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
queryByAttribute,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { EntityReference } from '../../../generated/tests/testCase';
|
||||
import { act } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { EntityReference, TestCase } from '../../../generated/tests/testCase';
|
||||
import { getListTestCaseBySearch } from '../../../rest/testAPI';
|
||||
import { AddTestCaseList } from './AddTestCaseList.component';
|
||||
import { AddTestCaseModalProps } from './AddTestCaseList.interface';
|
||||
|
||||
@ -31,7 +33,15 @@ jest.mock('../../common/Loader/Loader', () => {
|
||||
});
|
||||
|
||||
jest.mock('../../common/SearchBarComponent/SearchBar.component', () => {
|
||||
return jest.fn().mockImplementation(() => <div>Search Bar Mock</div>);
|
||||
return jest.fn().mockImplementation(({ onSearch, searchValue }) => (
|
||||
<div>
|
||||
<input
|
||||
data-testid="search-bar"
|
||||
value={searchValue}
|
||||
onChange={(e) => onSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
});
|
||||
jest.mock('../../../utils/StringsUtils', () => {
|
||||
return {
|
||||
@ -60,12 +70,7 @@ jest.mock('../../../utils/CommonUtils', () => {
|
||||
});
|
||||
jest.mock('../../../rest/testAPI', () => {
|
||||
return {
|
||||
getListTestCaseBySearch: jest.fn().mockResolvedValue({
|
||||
data: [],
|
||||
paging: {
|
||||
total: 0,
|
||||
},
|
||||
}),
|
||||
getListTestCaseBySearch: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@ -85,23 +90,116 @@ const mockProps: AddTestCaseModalProps = {
|
||||
};
|
||||
|
||||
jest.mock('../../../utils/RouterUtils', () => ({
|
||||
getEntityDetailsPath: jest.fn(),
|
||||
getEntityDetailsPath: jest.fn().mockReturnValue('/path/to/entity'),
|
||||
}));
|
||||
|
||||
const mockGetListTestCaseBySearch =
|
||||
getListTestCaseBySearch as jest.MockedFunction<
|
||||
typeof getListTestCaseBySearch
|
||||
>;
|
||||
|
||||
const mockTestCases: TestCase[] = [
|
||||
{
|
||||
id: 'test-case-1',
|
||||
name: 'test_case_1',
|
||||
displayName: 'Test Case 1',
|
||||
entityLink: '<#E::table::sample.table>',
|
||||
testDefinition: {
|
||||
id: 'test-def-1',
|
||||
name: 'table_column_count_to_equal',
|
||||
displayName: 'Table Column Count To Equal',
|
||||
},
|
||||
} as TestCase,
|
||||
{
|
||||
id: 'test-case-2',
|
||||
name: 'test_case_2',
|
||||
displayName: 'Test Case 2',
|
||||
entityLink: '<#E::table::sample.table::columns::id>',
|
||||
testDefinition: {
|
||||
id: 'test-def-2',
|
||||
name: 'column_values_to_be_unique',
|
||||
displayName: 'Column Values To Be Unique',
|
||||
},
|
||||
} as TestCase,
|
||||
{
|
||||
id: 'test-case-3',
|
||||
name: 'test_case_3',
|
||||
displayName: 'Test Case 3',
|
||||
entityLink: '<#E::table::another.table>',
|
||||
testDefinition: {
|
||||
id: 'test-def-3',
|
||||
name: 'table_row_count_to_be_between',
|
||||
displayName: 'Table Row Count To Be Between',
|
||||
},
|
||||
} as TestCase,
|
||||
];
|
||||
|
||||
const renderWithRouter = (props: AddTestCaseModalProps) => {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<AddTestCaseList {...props} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
describe('AddTestCaseList', () => {
|
||||
it('renders the component', async () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockGetListTestCaseBySearch.mockResolvedValue({
|
||||
data: [],
|
||||
paging: {
|
||||
total: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the component with initial state', async () => {
|
||||
await act(async () => {
|
||||
render(<AddTestCaseList {...mockProps} />);
|
||||
renderWithRouter(mockProps);
|
||||
});
|
||||
|
||||
expect(screen.getByText('Search Bar Mock')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('search-bar')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('cancel')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('submit')).toBeInTheDocument();
|
||||
expect(mockGetListTestCaseBySearch).toHaveBeenCalledWith({
|
||||
q: '*',
|
||||
limit: 25,
|
||||
offset: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders empty state when no test cases are found', async () => {
|
||||
await act(async () => {
|
||||
renderWithRouter(mockProps);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Error Placeholder Mock')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders test cases when data is available', async () => {
|
||||
mockGetListTestCaseBySearch.mockResolvedValue({
|
||||
data: mockTestCases,
|
||||
paging: {
|
||||
total: 3,
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
renderWithRouter(mockProps);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('test_case_1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('test_case_2')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('test_case_3')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onCancel when cancel button is clicked', async () => {
|
||||
await act(async () => {
|
||||
render(<AddTestCaseList {...mockProps} />);
|
||||
renderWithRouter(mockProps);
|
||||
});
|
||||
fireEvent.click(screen.getByTestId('cancel'));
|
||||
|
||||
@ -110,7 +208,7 @@ describe('AddTestCaseList', () => {
|
||||
|
||||
it('calls onSubmit when submit button is clicked', async () => {
|
||||
await act(async () => {
|
||||
render(<AddTestCaseList {...mockProps} />);
|
||||
renderWithRouter(mockProps);
|
||||
});
|
||||
const submitBtn = screen.getByTestId('submit');
|
||||
fireEvent.click(submitBtn);
|
||||
@ -125,10 +223,483 @@ describe('AddTestCaseList', () => {
|
||||
|
||||
it('does not render submit and cancel buttons when showButton is false', async () => {
|
||||
await act(async () => {
|
||||
render(<AddTestCaseList {...mockProps} showButton={false} />);
|
||||
renderWithRouter({ ...mockProps, showButton: false });
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('cancel')).toBeNull();
|
||||
expect(screen.queryByTestId('submit')).toBeNull();
|
||||
});
|
||||
|
||||
describe('Search functionality', () => {
|
||||
it('triggers search when search term is entered', async () => {
|
||||
await act(async () => {
|
||||
renderWithRouter(mockProps);
|
||||
});
|
||||
|
||||
const searchBar = screen.getByTestId('search-bar');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(searchBar, { target: { value: 'test_search' } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetListTestCaseBySearch).toHaveBeenCalledWith({
|
||||
q: 'test_search',
|
||||
limit: 25,
|
||||
offset: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('applies filters when provided', async () => {
|
||||
const filters = 'testSuiteFullyQualifiedName:sample.test.suite';
|
||||
|
||||
await act(async () => {
|
||||
renderWithRouter({ ...mockProps, filters });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetListTestCaseBySearch).toHaveBeenCalledWith({
|
||||
q: `* && ${filters}`,
|
||||
limit: 25,
|
||||
offset: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('combines search term with filters', async () => {
|
||||
const filters = 'testSuiteFullyQualifiedName:sample.test.suite';
|
||||
|
||||
await act(async () => {
|
||||
renderWithRouter({ ...mockProps, filters });
|
||||
});
|
||||
|
||||
const searchBar = screen.getByTestId('search-bar');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(searchBar, { target: { value: 'column_test' } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetListTestCaseBySearch).toHaveBeenCalledWith({
|
||||
q: `column_test && ${filters}`,
|
||||
limit: 25,
|
||||
offset: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('passes testCaseParams to API call', async () => {
|
||||
const testCaseParams = {
|
||||
testSuiteId: 'test-suite-123',
|
||||
includeFields: ['testDefinition', 'testSuite'],
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<AddTestCaseList {...mockProps} testCaseParams={testCaseParams} />
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetListTestCaseBySearch).toHaveBeenCalledWith({
|
||||
q: '*',
|
||||
limit: 25,
|
||||
offset: 0,
|
||||
...testCaseParams,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test case selection', () => {
|
||||
beforeEach(() => {
|
||||
mockGetListTestCaseBySearch.mockResolvedValue({
|
||||
data: mockTestCases,
|
||||
paging: {
|
||||
total: 3,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('selects a test case when clicked', async () => {
|
||||
const onChange = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
renderWithRouter({ ...mockProps, onChange });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('test_case_1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const testCaseCard = screen
|
||||
.getByTestId('test_case_1')
|
||||
.closest('.cursor-pointer');
|
||||
|
||||
expect(testCaseCard).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(testCaseCard as Element);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledWith([mockTestCases[0]]);
|
||||
});
|
||||
|
||||
const checkbox = screen.getByTestId('checkbox-test_case_1');
|
||||
|
||||
expect(checkbox).toHaveProperty('checked', true);
|
||||
});
|
||||
|
||||
it('deselects a test case when clicked again', async () => {
|
||||
const onChange = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
renderWithRouter({ ...mockProps, onChange });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('test_case_1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const testCaseCard = screen
|
||||
.getByTestId('test_case_1')
|
||||
.closest('.cursor-pointer');
|
||||
|
||||
expect(testCaseCard).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(testCaseCard as Element);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(testCaseCard as Element);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenLastCalledWith([]);
|
||||
});
|
||||
|
||||
const checkbox = screen.getByTestId('checkbox-test_case_1');
|
||||
|
||||
expect(checkbox).toHaveProperty('checked', false);
|
||||
});
|
||||
|
||||
it('handles multiple test case selections', async () => {
|
||||
const onChange = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
renderWithRouter({ ...mockProps, onChange });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('test_case_1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const testCaseCard1 = screen
|
||||
.getByTestId('test_case_1')
|
||||
.closest('.cursor-pointer');
|
||||
const testCaseCard2 = screen
|
||||
.getByTestId('test_case_2')
|
||||
.closest('.cursor-pointer');
|
||||
|
||||
expect(testCaseCard1).not.toBeNull();
|
||||
expect(testCaseCard2).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(testCaseCard1 as Element);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(testCaseCard2 as Element);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenLastCalledWith([
|
||||
mockTestCases[0],
|
||||
mockTestCases[1],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('pre-selects test cases when selectedTest prop is provided', async () => {
|
||||
mockGetListTestCaseBySearch.mockResolvedValue({
|
||||
data: [mockTestCases[0], mockTestCases[1]],
|
||||
paging: {
|
||||
total: 2,
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
renderWithRouter({
|
||||
...mockProps,
|
||||
selectedTest: ['test_case_1', 'test_case_2'],
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const checkbox1 = screen.getByTestId('checkbox-test_case_1');
|
||||
const checkbox2 = screen.getByTestId('checkbox-test_case_2');
|
||||
|
||||
expect(checkbox1).toHaveProperty('checked', true);
|
||||
expect(checkbox2).toHaveProperty('checked', true);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles test cases without id gracefully', async () => {
|
||||
const testCasesWithoutId = [{ ...mockTestCases[0], id: undefined }];
|
||||
|
||||
mockGetListTestCaseBySearch.mockResolvedValue({
|
||||
data: testCasesWithoutId,
|
||||
paging: {
|
||||
total: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const onChange = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
renderWithRouter({ ...mockProps, onChange });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('test_case_1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const testCaseCard = screen
|
||||
.getByTestId('test_case_1')
|
||||
.closest('.cursor-pointer');
|
||||
|
||||
expect(testCaseCard).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(testCaseCard as Element);
|
||||
});
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pagination and virtual list', () => {
|
||||
it('fetches data with correct pagination parameters', async () => {
|
||||
mockGetListTestCaseBySearch.mockResolvedValue({
|
||||
data: mockTestCases.slice(0, 2),
|
||||
paging: {
|
||||
total: 3,
|
||||
},
|
||||
});
|
||||
|
||||
renderWithRouter(mockProps);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetListTestCaseBySearch).toHaveBeenCalledWith({
|
||||
q: '*',
|
||||
limit: 25,
|
||||
offset: 0,
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('test_case_1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('test_case_2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('maintains search term with API calls', async () => {
|
||||
mockGetListTestCaseBySearch.mockResolvedValue({
|
||||
data: [mockTestCases[0]],
|
||||
paging: {
|
||||
total: 2,
|
||||
},
|
||||
});
|
||||
|
||||
renderWithRouter(mockProps);
|
||||
|
||||
const searchBar = screen.getByTestId('search-bar');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(searchBar, { target: { value: 'specific_test' } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetListTestCaseBySearch).toHaveBeenCalledWith({
|
||||
q: 'specific_test',
|
||||
limit: 25,
|
||||
offset: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('uses virtual list for performance optimization', async () => {
|
||||
mockGetListTestCaseBySearch.mockResolvedValue({
|
||||
data: mockTestCases,
|
||||
paging: {
|
||||
total: 100,
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = renderWithRouter(mockProps);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('test_case_1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const virtualList = container.querySelector('.rc-virtual-list-holder');
|
||||
|
||||
expect(virtualList).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Submit functionality', () => {
|
||||
it('submits selected test cases', async () => {
|
||||
mockGetListTestCaseBySearch.mockResolvedValue({
|
||||
data: mockTestCases,
|
||||
paging: {
|
||||
total: 3,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
renderWithRouter({ ...mockProps, onSubmit });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('test_case_1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const testCaseCard1 = screen
|
||||
.getByTestId('test_case_1')
|
||||
.closest('.cursor-pointer');
|
||||
const testCaseCard2 = screen
|
||||
.getByTestId('test_case_2')
|
||||
.closest('.cursor-pointer');
|
||||
|
||||
expect(testCaseCard1).not.toBeNull();
|
||||
expect(testCaseCard2).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(testCaseCard1 as Element);
|
||||
fireEvent.click(testCaseCard2 as Element);
|
||||
});
|
||||
|
||||
const submitBtn = screen.getByTestId('submit');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(submitBtn);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmit).toHaveBeenCalledWith([
|
||||
mockTestCases[0],
|
||||
mockTestCases[1],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles async submit operations', async () => {
|
||||
mockGetListTestCaseBySearch.mockResolvedValue({
|
||||
data: mockTestCases,
|
||||
paging: {
|
||||
total: 3,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(resolve, 100))
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
renderWithRouter({ ...mockProps, onSubmit });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('test_case_1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const submitBtn = screen.getByTestId('submit');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(submitBtn);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const loader = queryByAttribute('aria-label', submitBtn, 'loading');
|
||||
|
||||
expect(loader).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Column test cases', () => {
|
||||
it('displays column information for column test cases', async () => {
|
||||
const columnTestCase: TestCase = {
|
||||
id: 'column-test',
|
||||
name: 'column_test',
|
||||
displayName: 'Column Test',
|
||||
entityLink: '<#E::table::sample.table::columns::user_id>',
|
||||
testDefinition: {
|
||||
id: 'test-def',
|
||||
name: 'column_values_to_be_unique',
|
||||
displayName: 'Column Values To Be Unique',
|
||||
},
|
||||
} as TestCase;
|
||||
|
||||
mockGetListTestCaseBySearch.mockResolvedValue({
|
||||
data: [columnTestCase],
|
||||
paging: {
|
||||
total: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
renderWithRouter(mockProps);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('column_test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('label.column:')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not display column information for table test cases', async () => {
|
||||
const tableTestCase: TestCase = {
|
||||
id: 'table-test',
|
||||
name: 'table_test',
|
||||
displayName: 'Table Test',
|
||||
entityLink: '<#E::table::sample.table>',
|
||||
testDefinition: {
|
||||
id: 'test-def',
|
||||
name: 'table_row_count_to_be_between',
|
||||
displayName: 'Table Row Count To Be Between',
|
||||
},
|
||||
} as TestCase;
|
||||
|
||||
mockGetListTestCaseBySearch.mockResolvedValue({
|
||||
data: [tableTestCase],
|
||||
paging: {
|
||||
total: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
renderWithRouter(mockProps);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('table_test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByText('label.column:')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -70,7 +70,9 @@ export const AddTestCaseList = ({
|
||||
setIsLoading(true);
|
||||
|
||||
const testCaseResponse = await getListTestCaseBySearch({
|
||||
q: filters ? `${searchText} && ${filters}` : searchText,
|
||||
q: filters
|
||||
? `${searchText || WILD_CARD_CHAR} && ${filters}`
|
||||
: searchText,
|
||||
limit: PAGE_SIZE_MEDIUM,
|
||||
offset: (page - 1) * PAGE_SIZE_MEDIUM,
|
||||
...(testCaseParams ?? {}),
|
||||
|
@ -264,6 +264,12 @@ export const PAGE_HEADERS = {
|
||||
entity: i18n.t('label.metric-plural'),
|
||||
}),
|
||||
},
|
||||
CHARTS_CUSTOM_ATTRIBUTES: {
|
||||
header: i18n.t('label.chart-plural'),
|
||||
subHeader: i18n.t('message.define-custom-property-for-entity', {
|
||||
entity: i18n.t('label.chart-plural'),
|
||||
}),
|
||||
},
|
||||
PLATFORM_LINEAGE: {
|
||||
header: i18n.t('label.lineage'),
|
||||
subHeader: i18n.t('message.page-sub-header-for-platform-lineage'),
|
||||
|
@ -12,11 +12,15 @@
|
||||
*/
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { ENTITY_PATH } from '../../constants/constants';
|
||||
import { PAGE_HEADERS } from '../../constants/PageHeaders.constant';
|
||||
import { EntityTabs } from '../../enums/entity.enum';
|
||||
import { Type } from '../../generated/entity/type';
|
||||
import CustomEntityDetailV1 from './CustomPropertiesPageV1';
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
const mockTab = jest.fn().mockReturnValue('tables');
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useNavigate: jest.fn().mockImplementation(() => mockNavigate),
|
||||
@ -25,6 +29,10 @@ jest.mock('react-router-dom', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../utils/useRequiredParams', () => ({
|
||||
useRequiredParams: jest.fn(() => ({ tab: mockTab() })),
|
||||
}));
|
||||
|
||||
jest.mock('../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder', () =>
|
||||
jest.fn().mockImplementation(() => <div>ErrorPlaceHolder</div>)
|
||||
);
|
||||
@ -161,4 +169,104 @@ describe('CustomPropertiesPageV1 component', () => {
|
||||
|
||||
await waitFor(() => expect(mockShowErrorToast).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
|
||||
describe('customPageHeader mapping', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(React, 'useMemo').mockImplementation((fn) => fn());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const testCases = [
|
||||
{ tab: 'tables', expected: PAGE_HEADERS.TABLES_CUSTOM_ATTRIBUTES },
|
||||
{ tab: 'topics', expected: PAGE_HEADERS.TOPICS_CUSTOM_ATTRIBUTES },
|
||||
{ tab: 'dashboards', expected: PAGE_HEADERS.DASHBOARD_CUSTOM_ATTRIBUTES },
|
||||
{
|
||||
tab: 'dashboardDataModels',
|
||||
expected: PAGE_HEADERS.DASHBOARD_DATA_MODEL_CUSTOM_ATTRIBUTES,
|
||||
},
|
||||
{
|
||||
tab: 'dataProducts',
|
||||
expected: PAGE_HEADERS.DATA_PRODUCT_CUSTOM_ATTRIBUTES,
|
||||
},
|
||||
{ tab: 'metrics', expected: PAGE_HEADERS.METRIC_CUSTOM_ATTRIBUTES },
|
||||
{ tab: 'pipelines', expected: PAGE_HEADERS.PIPELINES_CUSTOM_ATTRIBUTES },
|
||||
{ tab: 'mlmodels', expected: PAGE_HEADERS.ML_MODELS_CUSTOM_ATTRIBUTES },
|
||||
{ tab: 'containers', expected: PAGE_HEADERS.CONTAINER_CUSTOM_ATTRIBUTES },
|
||||
{
|
||||
tab: 'searchIndexes',
|
||||
expected: PAGE_HEADERS.SEARCH_INDEX_CUSTOM_ATTRIBUTES,
|
||||
},
|
||||
{
|
||||
tab: 'storedProcedures',
|
||||
expected: PAGE_HEADERS.STORED_PROCEDURE_CUSTOM_ATTRIBUTES,
|
||||
},
|
||||
{ tab: 'domains', expected: PAGE_HEADERS.DOMAIN_CUSTOM_ATTRIBUTES },
|
||||
{
|
||||
tab: 'glossaryTerm',
|
||||
expected: PAGE_HEADERS.GLOSSARY_TERM_CUSTOM_ATTRIBUTES,
|
||||
},
|
||||
{ tab: 'databases', expected: PAGE_HEADERS.DATABASE_CUSTOM_ATTRIBUTES },
|
||||
{
|
||||
tab: 'databaseSchemas',
|
||||
expected: PAGE_HEADERS.DATABASE_SCHEMA_CUSTOM_ATTRIBUTES,
|
||||
},
|
||||
{
|
||||
tab: 'apiEndpoints',
|
||||
expected: PAGE_HEADERS.API_ENDPOINT_CUSTOM_ATTRIBUTES,
|
||||
},
|
||||
{
|
||||
tab: 'apiCollections',
|
||||
expected: PAGE_HEADERS.API_COLLECTION_CUSTOM_ATTRIBUTES,
|
||||
},
|
||||
{ tab: 'charts', expected: PAGE_HEADERS.CHARTS_CUSTOM_ATTRIBUTES },
|
||||
];
|
||||
|
||||
it.each(testCases)(
|
||||
'should return correct header for $tab',
|
||||
async ({ tab }) => {
|
||||
mockTab.mockReturnValue(tab);
|
||||
|
||||
render(<CustomEntityDetailV1 />);
|
||||
|
||||
// Wait for component to render
|
||||
await waitFor(() => {
|
||||
expect(mockGetTypeByFQN).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Verify the correct header is used based on the tab
|
||||
// The actual header would be passed to PageHeader component
|
||||
// Since we're mocking PageHeader, we can't directly test the prop
|
||||
// but the logic is tested through the tab parameter
|
||||
expect(mockTab).toHaveBeenCalled();
|
||||
expect(ENTITY_PATH[tab as keyof typeof ENTITY_PATH]).toBeDefined();
|
||||
}
|
||||
);
|
||||
|
||||
it('should return TABLES_CUSTOM_ATTRIBUTES as default for unknown tab', async () => {
|
||||
mockTab.mockReturnValue('unknownTab');
|
||||
|
||||
render(<CustomEntityDetailV1 />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetTypeByFQN).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// For unknown tabs, it should default to tables
|
||||
expect(mockTab).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should have all supported custom property entities covered', () => {
|
||||
const supportedEntities = Object.keys(ENTITY_PATH).filter(
|
||||
() => (key: string) => testCases.some((tc) => tc.tab === key)
|
||||
);
|
||||
|
||||
// Verify we have test cases for most entities (excluding some that don't have custom properties)
|
||||
expect(testCases).toHaveLength(18);
|
||||
expect(supportedEntities.length).toBeGreaterThanOrEqual(18);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -189,6 +189,9 @@ const CustomEntityDetailV1 = () => {
|
||||
case ENTITY_PATH.apiCollections:
|
||||
return PAGE_HEADERS.API_COLLECTION_CUSTOM_ATTRIBUTES;
|
||||
|
||||
case ENTITY_PATH.charts:
|
||||
return PAGE_HEADERS.CHARTS_CUSTOM_ATTRIBUTES;
|
||||
|
||||
default:
|
||||
return PAGE_HEADERS.TABLES_CUSTOM_ATTRIBUTES;
|
||||
}
|
||||
|
@ -0,0 +1,224 @@
|
||||
/*
|
||||
* 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 { GlobalSettingOptions } from '../constants/GlobalSettings.constants';
|
||||
import { EntityType } from '../enums/entity.enum';
|
||||
import { getSettingOptionByEntityType } from './GlobalSettingsUtils';
|
||||
|
||||
describe('GlobalSettingsUtils', () => {
|
||||
describe('getSettingOptionByEntityType', () => {
|
||||
it('should return TABLES for EntityType.TABLE', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.TABLE)).toBe(
|
||||
GlobalSettingOptions.TABLES
|
||||
);
|
||||
});
|
||||
|
||||
it('should return TOPICS for EntityType.TOPIC', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.TOPIC)).toBe(
|
||||
GlobalSettingOptions.TOPICS
|
||||
);
|
||||
});
|
||||
|
||||
it('should return DASHBOARDS for EntityType.DASHBOARD', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.DASHBOARD)).toBe(
|
||||
GlobalSettingOptions.DASHBOARDS
|
||||
);
|
||||
});
|
||||
|
||||
it('should return PIPELINES for EntityType.PIPELINE', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.PIPELINE)).toBe(
|
||||
GlobalSettingOptions.PIPELINES
|
||||
);
|
||||
});
|
||||
|
||||
it('should return MLMODELS for EntityType.MLMODEL', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.MLMODEL)).toBe(
|
||||
GlobalSettingOptions.MLMODELS
|
||||
);
|
||||
});
|
||||
|
||||
it('should return CONTAINERS for EntityType.CONTAINER', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.CONTAINER)).toBe(
|
||||
GlobalSettingOptions.CONTAINERS
|
||||
);
|
||||
});
|
||||
|
||||
it('should return DATABASE for EntityType.DATABASE', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.DATABASE)).toBe(
|
||||
GlobalSettingOptions.DATABASES
|
||||
);
|
||||
});
|
||||
|
||||
it('should return DATABASE_SCHEMA for EntityType.DATABASE_SCHEMA', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.DATABASE_SCHEMA)).toBe(
|
||||
GlobalSettingOptions.DATABASE_SCHEMA
|
||||
);
|
||||
});
|
||||
|
||||
it('should return GLOSSARY_TERM for EntityType.GLOSSARY_TERM', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.GLOSSARY_TERM)).toBe(
|
||||
GlobalSettingOptions.GLOSSARY_TERM
|
||||
);
|
||||
});
|
||||
|
||||
it('should return CHARTS for EntityType.CHART', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.CHART)).toBe(
|
||||
GlobalSettingOptions.CHARTS
|
||||
);
|
||||
});
|
||||
|
||||
it('should return DOMAINS for EntityType.DOMAIN', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.DOMAIN)).toBe(
|
||||
GlobalSettingOptions.DOMAINS
|
||||
);
|
||||
});
|
||||
|
||||
it('should return STORED_PROCEDURES for EntityType.STORED_PROCEDURE', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.STORED_PROCEDURE)).toBe(
|
||||
GlobalSettingOptions.STORED_PROCEDURES
|
||||
);
|
||||
});
|
||||
|
||||
it('should return SEARCH_INDEXES for EntityType.SEARCH_INDEX', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.SEARCH_INDEX)).toBe(
|
||||
GlobalSettingOptions.SEARCH_INDEXES
|
||||
);
|
||||
});
|
||||
|
||||
it('should return DASHBOARD_DATA_MODEL for EntityType.DASHBOARD_DATA_MODEL', () => {
|
||||
expect(
|
||||
getSettingOptionByEntityType(EntityType.DASHBOARD_DATA_MODEL)
|
||||
).toBe(GlobalSettingOptions.DASHBOARD_DATA_MODEL);
|
||||
});
|
||||
|
||||
it('should return API_ENDPOINTS for EntityType.API_ENDPOINT', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.API_ENDPOINT)).toBe(
|
||||
GlobalSettingOptions.API_ENDPOINTS
|
||||
);
|
||||
});
|
||||
|
||||
it('should return API_COLLECTIONS for EntityType.API_COLLECTION', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.API_COLLECTION)).toBe(
|
||||
GlobalSettingOptions.API_COLLECTIONS
|
||||
);
|
||||
});
|
||||
|
||||
it('should return DATA_PRODUCT for EntityType.DATA_PRODUCT', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.DATA_PRODUCT)).toBe(
|
||||
GlobalSettingOptions.DATA_PRODUCT
|
||||
);
|
||||
});
|
||||
|
||||
it('should return METRICS for EntityType.METRIC', () => {
|
||||
expect(getSettingOptionByEntityType(EntityType.METRIC)).toBe(
|
||||
GlobalSettingOptions.METRICS
|
||||
);
|
||||
});
|
||||
|
||||
it('should return TABLES as default for unknown entity types', () => {
|
||||
expect(
|
||||
getSettingOptionByEntityType('unknownEntityType' as EntityType)
|
||||
).toBe(GlobalSettingOptions.TABLES);
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle undefined gracefully and return TABLES', () => {
|
||||
expect(
|
||||
getSettingOptionByEntityType(undefined as unknown as EntityType)
|
||||
).toBe(GlobalSettingOptions.TABLES);
|
||||
});
|
||||
|
||||
it('should handle null gracefully and return TABLES', () => {
|
||||
expect(
|
||||
getSettingOptionByEntityType(null as unknown as EntityType)
|
||||
).toBe(GlobalSettingOptions.TABLES);
|
||||
});
|
||||
|
||||
it('should handle empty string and return TABLES', () => {
|
||||
expect(getSettingOptionByEntityType('' as EntityType)).toBe(
|
||||
GlobalSettingOptions.TABLES
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('all supported custom property entities', () => {
|
||||
const supportedEntities = [
|
||||
{ entity: EntityType.TABLE, option: GlobalSettingOptions.TABLES },
|
||||
{ entity: EntityType.TOPIC, option: GlobalSettingOptions.TOPICS },
|
||||
{
|
||||
entity: EntityType.DASHBOARD,
|
||||
option: GlobalSettingOptions.DASHBOARDS,
|
||||
},
|
||||
{
|
||||
entity: EntityType.PIPELINE,
|
||||
option: GlobalSettingOptions.PIPELINES,
|
||||
},
|
||||
{ entity: EntityType.MLMODEL, option: GlobalSettingOptions.MLMODELS },
|
||||
{
|
||||
entity: EntityType.CONTAINER,
|
||||
option: GlobalSettingOptions.CONTAINERS,
|
||||
},
|
||||
{
|
||||
entity: EntityType.DATABASE,
|
||||
option: GlobalSettingOptions.DATABASES,
|
||||
},
|
||||
{
|
||||
entity: EntityType.DATABASE_SCHEMA,
|
||||
option: GlobalSettingOptions.DATABASE_SCHEMA,
|
||||
},
|
||||
{
|
||||
entity: EntityType.GLOSSARY_TERM,
|
||||
option: GlobalSettingOptions.GLOSSARY_TERM,
|
||||
},
|
||||
{ entity: EntityType.CHART, option: GlobalSettingOptions.CHARTS },
|
||||
{ entity: EntityType.DOMAIN, option: GlobalSettingOptions.DOMAINS },
|
||||
{
|
||||
entity: EntityType.STORED_PROCEDURE,
|
||||
option: GlobalSettingOptions.STORED_PROCEDURES,
|
||||
},
|
||||
{
|
||||
entity: EntityType.SEARCH_INDEX,
|
||||
option: GlobalSettingOptions.SEARCH_INDEXES,
|
||||
},
|
||||
{
|
||||
entity: EntityType.DASHBOARD_DATA_MODEL,
|
||||
option: GlobalSettingOptions.DASHBOARD_DATA_MODEL,
|
||||
},
|
||||
{
|
||||
entity: EntityType.API_ENDPOINT,
|
||||
option: GlobalSettingOptions.API_ENDPOINTS,
|
||||
},
|
||||
{
|
||||
entity: EntityType.API_COLLECTION,
|
||||
option: GlobalSettingOptions.API_COLLECTIONS,
|
||||
},
|
||||
{
|
||||
entity: EntityType.DATA_PRODUCT,
|
||||
option: GlobalSettingOptions.DATA_PRODUCT,
|
||||
},
|
||||
{ entity: EntityType.METRIC, option: GlobalSettingOptions.METRICS },
|
||||
];
|
||||
|
||||
it.each(supportedEntities)(
|
||||
'should map $entity to $option correctly',
|
||||
({ entity, option }) => {
|
||||
expect(getSettingOptionByEntityType(entity)).toBe(option);
|
||||
}
|
||||
);
|
||||
|
||||
it('should have all entities that support custom properties covered', () => {
|
||||
expect(supportedEntities).toHaveLength(18);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -55,13 +55,29 @@ export const getSettingOptionByEntityType = (entityType: EntityType) => {
|
||||
case EntityType.CONTAINER:
|
||||
return GlobalSettingOptions.CONTAINERS;
|
||||
case EntityType.DATABASE:
|
||||
return GlobalSettingOptions.DATABASE;
|
||||
return GlobalSettingOptions.DATABASES;
|
||||
case EntityType.DATABASE_SCHEMA:
|
||||
return GlobalSettingOptions.DATABASE_SCHEMA;
|
||||
case EntityType.GLOSSARY_TERM:
|
||||
return GlobalSettingOptions.GLOSSARY_TERM;
|
||||
case EntityType.CHART:
|
||||
return GlobalSettingOptions.CHARTS;
|
||||
case EntityType.DOMAIN:
|
||||
return GlobalSettingOptions.DOMAINS;
|
||||
case EntityType.STORED_PROCEDURE:
|
||||
return GlobalSettingOptions.STORED_PROCEDURES;
|
||||
case EntityType.SEARCH_INDEX:
|
||||
return GlobalSettingOptions.SEARCH_INDEXES;
|
||||
case EntityType.DASHBOARD_DATA_MODEL:
|
||||
return GlobalSettingOptions.DASHBOARD_DATA_MODEL;
|
||||
case EntityType.API_ENDPOINT:
|
||||
return GlobalSettingOptions.API_ENDPOINTS;
|
||||
case EntityType.API_COLLECTION:
|
||||
return GlobalSettingOptions.API_COLLECTIONS;
|
||||
case EntityType.DATA_PRODUCT:
|
||||
return GlobalSettingOptions.DATA_PRODUCT;
|
||||
case EntityType.METRIC:
|
||||
return GlobalSettingOptions.METRICS;
|
||||
|
||||
case EntityType.TABLE:
|
||||
default:
|
||||
|
@ -58,9 +58,16 @@ Object.defineProperty(global, 'navigator', {
|
||||
});
|
||||
|
||||
// Mock MessageChannel
|
||||
interface MockMessageEvent {
|
||||
data: {
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const mockMessageChannel = {
|
||||
port1: {
|
||||
onmessage: null as ((event: any) => void) | null,
|
||||
onmessage: null as ((event: MockMessageEvent) => void) | null,
|
||||
},
|
||||
port2: {},
|
||||
};
|
||||
@ -245,7 +252,7 @@ describe('SwMessenger', () => {
|
||||
});
|
||||
|
||||
it('should resolve when service worker is ready', async () => {
|
||||
let messageHandler: (event: any) => void;
|
||||
let messageHandler: (event: MockMessageEvent) => void;
|
||||
|
||||
Object.defineProperty(mockMessageChannel.port1, 'onmessage', {
|
||||
set: (handler) => {
|
||||
@ -303,7 +310,10 @@ describe('SwMessenger', () => {
|
||||
});
|
||||
|
||||
it('should send message and return response', async () => {
|
||||
const testMessage = { type: 'get', key: 'test-key' };
|
||||
const testMessage: { type: 'get'; key: string } = {
|
||||
type: 'get',
|
||||
key: 'test-key',
|
||||
};
|
||||
const expectedResponse = 'test-value';
|
||||
|
||||
Object.defineProperty(mockMessageChannel.port1, 'onmessage', {
|
||||
@ -317,7 +327,7 @@ describe('SwMessenger', () => {
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const result = await sendMessageToServiceWorker(testMessage as any);
|
||||
const result = await sendMessageToServiceWorker(testMessage);
|
||||
|
||||
expect(mockController.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@ -330,7 +340,10 @@ describe('SwMessenger', () => {
|
||||
});
|
||||
|
||||
it('should handle service worker errors', async () => {
|
||||
const testMessage = { type: 'get', key: 'test-key' };
|
||||
const testMessage: { type: 'get'; key: string } = {
|
||||
type: 'get',
|
||||
key: 'test-key',
|
||||
};
|
||||
const errorMessage = 'Service worker error';
|
||||
|
||||
Object.defineProperty(mockMessageChannel.port1, 'onmessage', {
|
||||
@ -344,13 +357,13 @@ describe('SwMessenger', () => {
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
await expect(
|
||||
sendMessageToServiceWorker(testMessage as any)
|
||||
).rejects.toThrow(errorMessage);
|
||||
await expect(sendMessageToServiceWorker(testMessage)).rejects.toThrow(
|
||||
errorMessage
|
||||
);
|
||||
});
|
||||
|
||||
it('should increment request counter for unique request IDs', async () => {
|
||||
const testMessage = { type: 'ping' };
|
||||
const testMessage: { type: 'ping' } = { type: 'ping' };
|
||||
|
||||
Object.defineProperty(mockMessageChannel.port1, 'onmessage', {
|
||||
set: (handler) => {
|
||||
@ -362,8 +375,8 @@ describe('SwMessenger', () => {
|
||||
});
|
||||
|
||||
// Send two messages
|
||||
await sendMessageToServiceWorker(testMessage as any);
|
||||
await sendMessageToServiceWorker(testMessage as any);
|
||||
await sendMessageToServiceWorker(testMessage);
|
||||
await sendMessageToServiceWorker(testMessage);
|
||||
|
||||
// Check that different request IDs were used
|
||||
const calls = mockController.postMessage.mock.calls;
|
||||
@ -402,7 +415,9 @@ describe('SwMessenger', () => {
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const result = await sendMessageToServiceWorker({ type: 'ping' } as any);
|
||||
const result = await sendMessageToServiceWorker({
|
||||
type: 'ping',
|
||||
} as const);
|
||||
|
||||
expect(result).toBe('success');
|
||||
});
|
||||
|
@ -17,8 +17,8 @@ import { swTokenStorage } from './SwTokenStorage';
|
||||
const mockSendMessageToServiceWorker = jest.fn();
|
||||
|
||||
jest.mock('./SwMessenger', () => ({
|
||||
sendMessageToServiceWorker: (...args: any[]) =>
|
||||
mockSendMessageToServiceWorker(...args),
|
||||
sendMessageToServiceWorker: (message: unknown) =>
|
||||
mockSendMessageToServiceWorker(message),
|
||||
}));
|
||||
|
||||
describe('SwTokenStorage', () => {
|
||||
|
@ -25,13 +25,21 @@ const mockGetItem = jest.fn();
|
||||
|
||||
jest.mock('./SwTokenStorage', () => ({
|
||||
swTokenStorage: {
|
||||
setItem: (...args: any[]) => mockSetItem(...args),
|
||||
getItem: (...args: any[]) => mockGetItem(...args),
|
||||
setItem: (key: string, value: string) => mockSetItem(key, value),
|
||||
getItem: (key: string) => mockGetItem(key),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock navigator and localStorage for browser environment simulation
|
||||
const mockNavigator = {
|
||||
interface MockNavigator {
|
||||
serviceWorker?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface MockWindow {
|
||||
indexedDB?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const mockNavigator: MockNavigator = {
|
||||
serviceWorker: {},
|
||||
};
|
||||
|
||||
@ -53,7 +61,7 @@ Object.defineProperty(global, 'localStorage', {
|
||||
Object.defineProperty(global, 'window', {
|
||||
value: {
|
||||
indexedDB: {},
|
||||
},
|
||||
} as MockWindow,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
@ -65,7 +73,7 @@ describe('SwTokenStorageUtils', () => {
|
||||
describe('isServiceWorkerAvailable', () => {
|
||||
it('should return true when both serviceWorker and indexedDB are available', () => {
|
||||
mockNavigator.serviceWorker = {};
|
||||
(global as any).window.indexedDB = {};
|
||||
(global.window as unknown as MockWindow).indexedDB = {};
|
||||
|
||||
const result = isServiceWorkerAvailable();
|
||||
|
||||
@ -73,8 +81,8 @@ describe('SwTokenStorageUtils', () => {
|
||||
});
|
||||
|
||||
it('should return false when serviceWorker is not available', () => {
|
||||
delete (mockNavigator as any).serviceWorker;
|
||||
(global as any).window.indexedDB = {};
|
||||
delete mockNavigator.serviceWorker;
|
||||
(global.window as unknown as MockWindow).indexedDB = {};
|
||||
|
||||
const result = isServiceWorkerAvailable();
|
||||
|
||||
@ -83,7 +91,7 @@ describe('SwTokenStorageUtils', () => {
|
||||
|
||||
it('should return false when indexedDB is not available', () => {
|
||||
mockNavigator.serviceWorker = {};
|
||||
delete (global as any).window.indexedDB;
|
||||
delete (global.window as unknown as MockWindow).indexedDB;
|
||||
|
||||
const result = isServiceWorkerAvailable();
|
||||
|
||||
@ -91,8 +99,8 @@ describe('SwTokenStorageUtils', () => {
|
||||
});
|
||||
|
||||
it('should return false when neither serviceWorker nor indexedDB are available', () => {
|
||||
delete (mockNavigator as any).serviceWorker;
|
||||
delete (global as any).window.indexedDB;
|
||||
delete mockNavigator.serviceWorker;
|
||||
delete (global.window as unknown as MockWindow).indexedDB;
|
||||
|
||||
const result = isServiceWorkerAvailable();
|
||||
|
||||
@ -104,7 +112,7 @@ describe('SwTokenStorageUtils', () => {
|
||||
beforeEach(() => {
|
||||
// Reset environment for each test
|
||||
mockNavigator.serviceWorker = {};
|
||||
(global as any).window.indexedDB = {};
|
||||
(global.window as unknown as MockWindow).indexedDB = {};
|
||||
});
|
||||
|
||||
it('should return token from service worker when available', async () => {
|
||||
@ -136,7 +144,7 @@ describe('SwTokenStorageUtils', () => {
|
||||
});
|
||||
|
||||
it('should fallback to localStorage when service worker is not available', async () => {
|
||||
delete (mockNavigator as any).serviceWorker;
|
||||
delete mockNavigator.serviceWorker;
|
||||
const mockToken = 'test-oidc-token';
|
||||
const mockAppState = JSON.stringify({ primary: mockToken });
|
||||
mockLocalStorage.getItem.mockReturnValue(mockAppState);
|
||||
@ -167,7 +175,7 @@ describe('SwTokenStorageUtils', () => {
|
||||
describe('setOidcToken', () => {
|
||||
beforeEach(() => {
|
||||
mockNavigator.serviceWorker = {};
|
||||
(global as any).window.indexedDB = {};
|
||||
(global.window as unknown as MockWindow).indexedDB = {};
|
||||
});
|
||||
|
||||
it('should set token in service worker when available', async () => {
|
||||
@ -200,7 +208,7 @@ describe('SwTokenStorageUtils', () => {
|
||||
});
|
||||
|
||||
it('should fallback to localStorage when service worker is not available', async () => {
|
||||
delete (mockNavigator as any).serviceWorker;
|
||||
delete mockNavigator.serviceWorker;
|
||||
const mockToken = 'new-oidc-token';
|
||||
const expectedState = JSON.stringify({ primary: mockToken });
|
||||
|
||||
@ -224,7 +232,7 @@ describe('SwTokenStorageUtils', () => {
|
||||
describe('getRefreshToken', () => {
|
||||
beforeEach(() => {
|
||||
mockNavigator.serviceWorker = {};
|
||||
(global as any).window.indexedDB = {};
|
||||
(global.window as unknown as MockWindow).indexedDB = {};
|
||||
});
|
||||
|
||||
it('should return refresh token from service worker when available', async () => {
|
||||
@ -248,7 +256,7 @@ describe('SwTokenStorageUtils', () => {
|
||||
});
|
||||
|
||||
it('should fallback to localStorage when service worker is not available', async () => {
|
||||
delete (mockNavigator as any).serviceWorker;
|
||||
delete mockNavigator.serviceWorker;
|
||||
const mockToken = 'test-refresh-token';
|
||||
const mockAppState = JSON.stringify({ secondary: mockToken });
|
||||
mockLocalStorage.getItem.mockReturnValue(mockAppState);
|
||||
@ -271,7 +279,7 @@ describe('SwTokenStorageUtils', () => {
|
||||
describe('setRefreshToken', () => {
|
||||
beforeEach(() => {
|
||||
mockNavigator.serviceWorker = {};
|
||||
(global as any).window.indexedDB = {};
|
||||
(global.window as unknown as MockWindow).indexedDB = {};
|
||||
});
|
||||
|
||||
it('should set refresh token in service worker when available', async () => {
|
||||
@ -304,7 +312,7 @@ describe('SwTokenStorageUtils', () => {
|
||||
});
|
||||
|
||||
it('should fallback to localStorage when service worker is not available', async () => {
|
||||
delete (mockNavigator as any).serviceWorker;
|
||||
delete mockNavigator.serviceWorker;
|
||||
const mockToken = 'new-refresh-token';
|
||||
const expectedState = JSON.stringify({ secondary: mockToken });
|
||||
|
||||
@ -328,7 +336,7 @@ describe('SwTokenStorageUtils', () => {
|
||||
describe('integration scenarios', () => {
|
||||
beforeEach(() => {
|
||||
mockNavigator.serviceWorker = {};
|
||||
(global as any).window.indexedDB = {};
|
||||
(global.window as unknown as MockWindow).indexedDB = {};
|
||||
});
|
||||
|
||||
it('should maintain both tokens when updating one', async () => {
|
||||
@ -363,7 +371,7 @@ describe('SwTokenStorageUtils', () => {
|
||||
it('should handle mixed environment gracefully (some features available)', async () => {
|
||||
// Simulate environment where serviceWorker exists but indexedDB doesn't
|
||||
mockNavigator.serviceWorker = {};
|
||||
delete (global as any).window.indexedDB;
|
||||
delete (global.window as unknown as MockWindow).indexedDB;
|
||||
|
||||
const result = await getOidcToken();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user