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 = '';
+ 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 = '
Text after
';
+ const result = extractTextFromHTML(htmlString);
+ expect(result).toBe('Text after');
+ });
+
+ it('should handle complex HTML structure', () => {
+ const htmlString = `
+
+ Page 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: [