diff --git a/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx b/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx index e94230de2e4..87cb0abb6e2 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx +++ b/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx @@ -24,6 +24,7 @@ import { removeHTMLTags, isJsonString, getParagraphContents, + extractTextFromHTML, } from './html'; describe('sanitizeHtml', () => { @@ -204,3 +205,101 @@ describe('getParagraphContents', () => { }); }); }); + +describe('extractTextFromHTML', () => { + it('should extract text from HTML div tags', () => { + const htmlString = '
Hello World
'; + const result = extractTextFromHTML(htmlString); + expect(result).toBe('Hello World'); + }); + + it('should extract text from nested HTML tags', () => { + const htmlString = '

Hello World

'; + const result = extractTextFromHTML(htmlString); + expect(result).toBe('Hello World'); + }); + + it('should extract text from multiple HTML elements', () => { + const htmlString = '

Title

Content

Footer'; + const result = extractTextFromHTML(htmlString); + expect(result).toBe('TitleContentFooter'); + }); + + it('should return original string when input is not HTML', () => { + const plainText = 'Just plain text'; + const result = extractTextFromHTML(plainText); + expect(result).toBe('Just plain text'); + }); + + it('should return original value when input is not a string', () => { + const numberValue = 12345; + const result = extractTextFromHTML(numberValue); + expect(result).toBe(12345); + + const nullValue = null; + const nullResult = extractTextFromHTML(nullValue); + expect(nullResult).toBe(null); + + const booleanValue = true; + const booleanResult = extractTextFromHTML(booleanValue); + expect(booleanResult).toBe(true); + }); + + it('should handle empty HTML tags', () => { + const htmlString = '
'; + const result = extractTextFromHTML(htmlString); + expect(result).toBe(''); + }); + + it('should handle HTML with only whitespace', () => { + const htmlString = '
'; + const result = extractTextFromHTML(htmlString); + expect(result).toBe(' '); + }); + + it('should extract text from HTML with attributes', () => { + const htmlString = '
Hello World
'; + const result = extractTextFromHTML(htmlString); + expect(result).toBe('Hello World'); + }); + + it('should handle self-closing tags', () => { + const htmlString = 'Image

Text after

'; + const result = extractTextFromHTML(htmlString); + expect(result).toBe('Text after'); + }); + + it('should handle complex HTML structure', () => { + const htmlString = ` + + Page Title + +

Main Title

+
+

First paragraph with emphasis.

+ +
+ + + `; + const result = extractTextFromHTML(htmlString); + expect(result).toContain('Page Title'); + expect(result).toContain('Main Title'); + expect(result).toContain('First paragraph with emphasis.'); + expect(result).toContain('Item 1'); + expect(result).toContain('Item 2'); + }); + + it('should not extract text from strings that look like HTML but are not', () => { + const fakeHtmlString = ''; + const result = extractTextFromHTML(fakeHtmlString); + expect(result).toBe(''); + + const mathExpression = 'x < 5 and y > 10'; + const mathResult = extractTextFromHTML(mathExpression); + expect(mathResult).toBe('x < 5 and y > 10'); + }); +}); diff --git a/superset-frontend/packages/superset-ui-core/src/utils/html.tsx b/superset-frontend/packages/superset-ui-core/src/utils/html.tsx index e1514976985..3055108f0ba 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/html.tsx +++ b/superset-frontend/packages/superset-ui-core/src/utils/html.tsx @@ -17,6 +17,7 @@ * under the License. */ import { FilterXSS, getDefaultWhiteList } from 'xss'; +import { DataRecordValue } from '../types'; const xssFilter = new FilterXSS({ whiteList: { @@ -169,7 +170,10 @@ export function safeHtmlSpan(possiblyHtmlString: string) { } export function removeHTMLTags(str: string): string { - return str.replace(/<[^>]*>/g, ''); + const doc = new DOMParser().parseFromString(str, 'text/html'); + const bodyText = doc.body?.textContent || ''; + const headText = doc.head?.textContent || ''; + return headText + bodyText; } export function isJsonString(str: string): boolean { @@ -204,3 +208,10 @@ export function getParagraphContents( return paragraphContents; } + +export function extractTextFromHTML(value: DataRecordValue): DataRecordValue { + if (typeof value === 'string' && isProbablyHTML(value)) { + return removeHTMLTags(value); + } + return value; +} diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx index b182ad03bf9..784e33de288 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx @@ -52,6 +52,7 @@ import { tn, useTheme, SupersetTheme, + extractTextFromHTML, } from '@superset-ui/core'; import { GenericDataType } from '@apache-superset/core/api/core'; import { @@ -514,7 +515,9 @@ export default function TableChart( const drillToDetailFilters: BinaryQueryObjectFilterClause[] = []; filteredColumnsMeta.forEach(col => { if (!col.isMetric) { - const dataRecordValue = value[col.key]; + let dataRecordValue = value[col.key]; + dataRecordValue = extractTextFromHTML(dataRecordValue); + drillToDetailFilters.push({ col: col.key, op: '==', @@ -535,7 +538,7 @@ export default function TableChart( { col: cellPoint.key, op: '==', - val: cellPoint.value as string | number | boolean, + val: extractTextFromHTML(cellPoint.value), }, ], groupbyFieldName: 'groupby', diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx index af57bee7597..17385970c5f 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx @@ -310,7 +310,12 @@ test('render time filter types as disabled if there are no temporal columns in t test('validates the name', async () => { defaultRender(); userEvent.click(screen.getByRole('button', { name: SAVE_REGEX })); - expect(await screen.findByText(NAME_REQUIRED_REGEX)).toBeInTheDocument(); + await waitFor( + async () => { + expect(await screen.findByText(NAME_REQUIRED_REGEX)).toBeInTheDocument(); + }, + { timeout: 10000 }, + ); }); test('validates the column', async () => { @@ -352,9 +357,12 @@ test('validates the pre-filter value', async () => { jest.useRealTimers(); // Wait for validation to complete after timer switch - await waitFor(() => { - expect(screen.getByText(PRE_FILTER_REQUIRED_REGEX)).toBeInTheDocument(); - }); + await waitFor( + () => { + expect(screen.getByText(PRE_FILTER_REQUIRED_REGEX)).toBeInTheDocument(); + }, + { timeout: 15000 }, + ); }, 50000); // Slow-running test, increase timeout to 50 seconds. // eslint-disable-next-line jest/no-disabled-tests diff --git a/superset-frontend/src/explore/components/PropertiesModal/PropertiesModal.test.tsx b/superset-frontend/src/explore/components/PropertiesModal/PropertiesModal.test.tsx index 4a194b1e7de..8ee41ea4909 100644 --- a/superset-frontend/src/explore/components/PropertiesModal/PropertiesModal.test.tsx +++ b/superset-frontend/src/explore/components/PropertiesModal/PropertiesModal.test.tsx @@ -91,7 +91,7 @@ fetchMock.get('glob:*/api/v1/chart/318*', { certification_details: 'Test certification details', certified_by: 'Test certified by', description: 'Test description', - cache_timeout: '1000', + cache_timeout: 1000, slice_name: 'Test chart new name', }, show_columns: [