Compare commits

...

4 Commits

Author SHA1 Message Date
Evan Rusackas
f3c848278d test(e2e): read antd6 Select value from content element, not the input
antd v6 moved the Select `aria-label` onto the combobox <input>, so
getByLabel('Dataset') resolved to the empty input (no text content) instead of
the value container. Scope getDatasetSelectContainer to
`.ant-select-content-has-value` (which wraps the input and holds the selected
value text), fixing the create-dataset wizard assertion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 18:06:32 -07:00
Evan Rusackas
f03aa32ab0 test(labels): add antd6 Tag icon-color regression test; fix Tooltip story
- Add a direct regression test in Label.test.tsx asserting a Label with a
  colored icon keeps its color (guards the shared Label/index.tsx fix for antd
  v6's Tag overwriting the icon's inline style).
- Update the Tooltip story argType onVisibleChange -> onOpenChange so the
  generated docs show the current antd v6 prop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:39:18 -07:00
Evan Rusackas
c65f38a6fc test(e2e): update Cypress/Playwright selectors for antd v6
E2E specs query antd's internal Select DOM, which v6 renamed. Fixes the
Cypress + Playwright failures on the antd6 upgrade:
- .ant-select-selection-search-input -> .ant-select-input (central Playwright
  Select helper + Cypress directories/utils; drives create-dataset, dataset-list
  modals, sqllab DB selection, and the "Select a dashboard" chart flow)
- .ant-select-selector -> .ant-select-content
- .ant-select-selection-placeholder -> .ant-select-placeholder
- .ant-select-selection-item -> combined .ant-select-content-has-value,
  .ant-select-selection-item (single + multiple modes)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:34:17 -07:00
Evan Rusackas
e35c7ae536 feat(frontend): upgrade Ant Design from v5 to v6
Upgrades antd 5.26 -> 6.5 (and @ant-design/icons -> 6) across the frontend.

Theme-safe: the getDesignToken -> allowedAntdTokens -> ConfigProvider/Emotion
bridge works on v6 with no token loss (only cosmetic box-shadow recomputation).
Adds theme characterization tests that lock the full computed token set for
light and dark so any future token rename/removal fails loudly.

Key changes:
- Theme bridge: coerce boolean `cssVar` (removed from v6 ThemeConfig) to the
  object form.
- Wrappers/app: Select (dropdownAlign removed; showSearch/tokenSeparators union
  handling), Tooltip/Popover/DropdownContainer styles.body -> styles.container,
  Steps.Step -> items, Popover/Tooltip/Dropdown visible/onVisibleChange ->
  open/onOpenChange, Dropdown overlay -> menu, Pagination size="default"
  removed, .ant-tooltip-inner -> .ant-tooltip-container and
  .ant-select-selector -> .ant-select-content in styled CSS.
- Fixes a real regression the tests caught: antd v6 Tag overwrites its `icon`
  prop's inline style, dropping Label icon colors; fixed by wrapping the icon.
- Deps: remove unused @rjsf/antd (pinned antd 5); bump @rjsf/core/utils/
  validator-ajv8 to v6; pin jsonforms-antd-renderers to antd 6 via overrides.
- Test env: replace the `MessageChannel = undefined` stub with a polyfill
  (antd v6's scheduler needs the constructor).
- Update test-DOM assertions across suites for v6 class/structure renames.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 01:47:11 -07:00
66 changed files with 2675 additions and 724 deletions

View File

@@ -471,7 +471,7 @@ export function applyAdvancedTimeRangeFilterOnDashboard(
endRange?: string,
) {
cy.get('.control-label').contains('Range type').should('be.visible');
cy.get('.ant-popover-content .ant-select-selector')
cy.get('.ant-popover-content .ant-select-content')
.should('be.visible')
.click();
cy.get(`[label="Advanced"]`).should('be.visible').click();
@@ -514,15 +514,14 @@ export function inputNativeFilterDefaultValue(
)
.eq(1)
.within(() => {
cy.get('.ant-select-selection-search-input').type(
`${defaultValue}{enter}`,
{ force: true },
);
cy.get('.ant-select-input').type(`${defaultValue}{enter}`, {
force: true,
});
});
});
} else {
cy.getBySel('default-input').within(() => {
cy.get('.ant-select-selection-search-input').click();
cy.get('.ant-select-input').click();
cy.get('.ant-select-item-option-content').contains(defaultValue).click();
});
}

View File

@@ -53,9 +53,7 @@ export function saveChartToDashboard(chartName: string, dashboardName: string) {
.then($modal => {
cy.wait(500);
cy.wrap($modal)
.find(
'.ant-select-selection-search-input[aria-label*="Select a dashboard"]',
)
.find('.ant-select-input[aria-label*="Select a dashboard"]')
.type(dashboardName, { force: true });
cy.wrap($modal)
.find(`.ant-select-item-option[title="${dashboardName}"]`)

View File

@@ -91,7 +91,7 @@ export const databasesPage = {
preferredBlockSheets: '.preferred > :nth-child(6)',
supportedDatabasesText: '.control-label',
orChoose: '.available-label',
dbDropdown: '[class="ant-select-selection-search-input"]',
dbDropdown: '.ant-select-input',
dbDropdownMenu: '.rc-virtual-list-holder-inner',
dbDropdownMenuItem: '[class="ant-select-item-option-content"]',
infoAlert: '.ant-alert',
@@ -277,7 +277,7 @@ export const chartListView = {
header: {
cardView: '[aria-label="card-view"]',
listView: '[aria-label="list-view"]',
sort: '[class="ant-select-selection-search-input"][aria-label="Sort"]',
sort: '.ant-select-input[aria-label="Sort"]',
sortRecentlyModifiedMenuOption: '[label="Recently modified"]',
sortAlphabeticalMenuOption: '[label="Alphabetical"]',
sortDropdown: '.Select__menu',
@@ -338,20 +338,20 @@ export const nativeFilters = {
filtersPanel: {
filterName: dataTestLocator('filters-config-modal__name-input'),
datasetName: dataTestLocator('filters-config-modal__datasource-input'),
filterInfoInput: '.ant-select-selection-search-input',
filterInfoInput: '.ant-select-input',
inputDropdown: '.ant-select-item-option-content',
columnEmptyInput: '.ant-select-selection-placeholder',
columnEmptyInput: '.ant-select-placeholder',
filterTypeInput: dataTestLocator('filters-config-modal__filter-type'),
fieldInput: dataTestLocator('field-input'),
filterTypeItem: '.ant-select-selection-item',
filterTypeItem: '.ant-select-content-has-value, .ant-select-selection-item',
filterGear: dataTestLocator('filterbar-orientation-icon'),
},
filterFromDashboardView: {
filterValueInput: '[class="ant-select-selection-search-input"]',
filterValueInput: '.ant-select-input',
expand: dataTestLocator('filter-bar__expand-button'),
collapse: dataTestLocator('filter-bar__collapse-button'),
filterName: dataTestLocator('filter-control-name'),
filterContent: '.ant-select-selection-item',
filterContent: '.ant-select-content-has-value, .ant-select-selection-item',
createFilterButton: dataTestLocator('filter-bar__create-filter'),
timeRangeFilterContent: dataTestLocator('time-range-trigger'),
},
@@ -365,7 +365,7 @@ export const nativeFilters = {
checkedCheckbox: '.ant-checkbox-wrapper-checked',
infoTooltip: '[aria-label="Show info tooltip"]',
parentFilterInput: dataTestLocator('parent-filter-input'),
filterPlaceholder: '.ant-select-selection-placeholder',
filterPlaceholder: '.ant-select-placeholder',
collapsedSectionContainer: '[class="ant-collapse-content-box"]',
},
filtersList: {
@@ -376,7 +376,7 @@ export const nativeFilters = {
removeIcon: '[aria-label="delete"]',
},
filterItem: dataTestLocator('form-item-value'),
filterItemDropdown: '.ant-select-selection-search',
filterItemDropdown: '.ant-select-input',
applyFilter: dataTestLocator('filter-bar__apply-button'),
defaultInput: dataTestLocator('default-input'),
filterIcon: dataTestLocator('filter-icon'),
@@ -484,7 +484,7 @@ export const exploreView = {
saveModal: {
modal: '.ant-modal-content',
chartNameInput: dataTestLocator('new-chart-name'),
dashboardNameInput: '.ant-select-selection-search-input',
dashboardNameInput: '.ant-select-input',
addToDashboardInput: dataTestLocator(
'save-chart-modal-select-dashboard-form',
),
@@ -594,7 +594,7 @@ export const exploreView = {
},
};
export const createChartView = {
chooseDatasetInput: '.ant-select-selection-search-input',
chooseDatasetInput: '.ant-select-input',
chooseDatasetOption: '.ant-select-item-option-content',
chooseDatasetList: '.rc-virtual-list-holder-inner',
tableVizType: "[alt='Table']",
@@ -641,7 +641,7 @@ export const dashboardView = {
secondTabSalesDashboard: dataTestLocator('dragdroppable-object'),
},
timeRangeModal: {
rangeTypeField: '.ant-select-selection-item',
rangeTypeField: '.ant-select-content-has-value, .ant-select-selection-item',
startTimeInputNumber: '.ant-input-number-input',
datePicker: '.ant-picker-input',
applyButton: dataTestLocator('date-filter-control__apply-button'),

View File

@@ -72,7 +72,7 @@ module.exports = {
],
coverageReporters: ['lcov', 'json-summary', 'html', 'text'],
transformIgnorePatterns: [
'node_modules/(?!@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued|storybook/*.|json-stringify-pretty-compact)',
'node_modules/(?!@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|@x0k/.*|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued|storybook/*.|json-stringify-pretty-compact)',
],
preset: 'ts-jest',
transform: {

File diff suppressed because it is too large Load Diff

View File

@@ -127,10 +127,9 @@
"@luma.gl/shadertools": "~9.2.5",
"@luma.gl/webgl": "~9.2.5",
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/antd": "^5.24.13",
"@rjsf/core": "^5.24.13",
"@rjsf/utils": "^5.24.3",
"@rjsf/validator-ajv8": "^5.24.13",
"@rjsf/core": "^6.6.2",
"@rjsf/utils": "^6.6.2",
"@rjsf/validator-ajv8": "^6.6.2",
"@scarf/scarf": "^1.4.0",
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
"@superset-ui/core": "file:./packages/superset-ui-core",
@@ -166,7 +165,7 @@
"@visx/xychart": "^4.0.0",
"ag-grid-community": "35.3.1",
"ag-grid-react": "35.3.1",
"antd": "^5.26.0",
"antd": "^6.0.0",
"chrono-node": "^2.9.1",
"classnames": "^2.2.5",
"content-disposition": "^2.0.1",
@@ -388,6 +387,9 @@
},
"overrides": {
"uuid": "$uuid",
"@great-expectations/jsonforms-antd-renderers": {
"antd": "$antd"
},
"core-js": "^3.38.1",
"dompurify": "^3.4.11",
"esbuild": "^0.28.1",

View File

@@ -110,7 +110,7 @@
"react-loadable": "^5.5.0",
"tinycolor2": "*",
"lodash": "^4.18.1",
"antd": "^5.26.0",
"antd": "^6.0.0",
"jed": "^1.1.1"
},
"scripts": {

View File

@@ -18,7 +18,34 @@
*/
import { theme as antdThemeImport } from 'antd';
import { Theme } from './Theme';
import { AnyThemeConfig, ThemeAlgorithm } from './types';
import { AnyThemeConfig, ThemeAlgorithm, allowedAntdTokens } from './types';
/**
* A fixed, Superset-representative base theme used by the characterization
* tests below. Mirrors the real THEME_DEFAULT token set so the snapshots are
* stable and meaningful. Do NOT churn this casually — it is the baseline the
* Ant Design v6 upgrade is measured against.
*/
const SUPERSET_BASE_THEME: AnyThemeConfig = {
token: {
colorPrimary: '#2893B3',
colorError: '#e04355',
colorWarning: '#fcc700',
colorSuccess: '#5ac189',
colorInfo: '#66bcfe',
fontFamily: "'Inter', Helvetica, Arial",
fontFamilyCode: "'IBM Plex Mono', 'Courier New', monospace",
},
};
/** Collect the computed value of every allow-listed antd token, sorted. */
function pickAllowedTokens(theme: Theme): Record<string, unknown> {
const out: Record<string, unknown> = {};
[...allowedAntdTokens].sort().forEach(key => {
out[key] = (theme.theme as unknown as Record<string, unknown>)[key];
});
return out;
}
// Mock emotion's cache to avoid actual DOM operations
jest.mock('@emotion/cache', () => ({
@@ -338,10 +365,10 @@ test('Theme edge cases correctly applies base theme tokens in dark mode', () =>
algorithm: antdThemeImport.defaultAlgorithm,
};
const baseThemeDark: AnyThemeConfig = {
const baseThemeDark = {
...baseTheme,
algorithm: antdThemeImport.darkAlgorithm,
};
} as AnyThemeConfig;
// Simulate light mode with base theme
const lightTheme = Theme.fromConfig({}, baseTheme);
@@ -473,10 +500,10 @@ test('Theme base theme integration handles base theme with dark algorithm correc
},
};
const baseThemeDark: AnyThemeConfig = {
const baseThemeDark = {
...baseTheme,
algorithm: antdThemeImport.darkAlgorithm,
};
} as AnyThemeConfig;
const userDarkTheme: AnyThemeConfig = {
algorithm: antdThemeImport.darkAlgorithm,
@@ -525,7 +552,7 @@ test('Theme base theme integration works with real-world Superset base theme con
const darkTheme = Theme.fromConfig(themeDark, {
...supersetBaseTheme,
algorithm: antdThemeImport.darkAlgorithm,
});
} as AnyThemeConfig);
expect(darkTheme.theme.fontFamily).toBe("'Inter', Helvetica, Arial");
const darkSerialized = darkTheme.toSerializedConfig();
@@ -604,7 +631,9 @@ test('Theme base theme integration handles cssVar, hashed and inherit properties
// User properties override/add to base
expect(serialized.inherit).toBe(true);
expect(serialized.cssVar).toBe(true);
// Ant Design v6 dropped boolean `cssVar`; Superset normalizes `true` to the
// object form `{}` at the antd boundary (CSS variables are on by default).
expect(serialized.cssVar).toEqual({});
expect(serialized.hashed).toBe(false);
// Tokens are still merged
@@ -909,3 +938,81 @@ test('colorLink is preserved in setConfig when explicitly set', () => {
expect(theme.theme.colorPrimary).toBe('#f759ab');
expect(theme.theme.colorLink).toBe('#ff0000');
});
// ---------------------------------------------------------------------------
// Characterization tests — theming safety net for the Ant Design v6 upgrade.
//
// These lock the bridge between Superset's theme and Ant Design's design
// tokens (getDesignToken -> allowedAntdTokens -> SupersetTheme). When antd
// renames or removes a token, the completeness tests fail loudly instead of
// letting a token silently resolve to `undefined`, and the snapshots make any
// value drift reviewable. See packages/superset-core/src/theme/types.ts.
// ---------------------------------------------------------------------------
test('every allowed antd token resolves to a defined value in light mode', () => {
const theme = Theme.fromConfig(
{ algorithm: antdThemeImport.defaultAlgorithm },
SUPERSET_BASE_THEME,
);
const missing = allowedAntdTokens.filter(
key =>
(theme.theme as unknown as Record<string, unknown>)[key] === undefined,
);
// A non-empty list means antd dropped/renamed a token we still allow-list.
expect(missing).toEqual([]);
});
test('every allowed antd token resolves to a defined value in dark mode', () => {
const theme = Theme.fromConfig(
{ algorithm: antdThemeImport.darkAlgorithm },
SUPERSET_BASE_THEME,
);
const missing = allowedAntdTokens.filter(
key =>
(theme.theme as unknown as Record<string, unknown>)[key] === undefined,
);
expect(missing).toEqual([]);
});
test('allowed antd token key set is stable', () => {
// Locks the *set* of tokens Superset exposes (independent of their values),
// so additions/removals to allowedAntdTokens are an explicit, reviewed diff.
expect([...allowedAntdTokens].sort()).toMatchSnapshot();
});
test('computed light-mode token values match snapshot', () => {
const theme = Theme.fromConfig(
{ algorithm: antdThemeImport.defaultAlgorithm },
SUPERSET_BASE_THEME,
);
expect(pickAllowedTokens(theme)).toMatchSnapshot();
});
test('computed dark-mode token values match snapshot', () => {
const theme = Theme.fromConfig(
{ algorithm: antdThemeImport.darkAlgorithm },
SUPERSET_BASE_THEME,
);
expect(pickAllowedTokens(theme)).toMatchSnapshot();
});
test('dark mode actually diverges from light mode for background tokens', () => {
const light = Theme.fromConfig(
{ algorithm: antdThemeImport.defaultAlgorithm },
SUPERSET_BASE_THEME,
);
const dark = Theme.fromConfig(
{ algorithm: antdThemeImport.darkAlgorithm },
SUPERSET_BASE_THEME,
);
// Guards the dark algorithm wiring: base surface and text must invert.
expect(dark.theme.colorBgBase).not.toBe(light.theme.colorBgBase);
expect(dark.theme.colorTextBase).not.toBe(light.theme.colorTextBase);
});

View File

@@ -0,0 +1,730 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`allowed antd token key set is stable 1`] = `
[
"borderRadius",
"borderRadiusLG",
"borderRadiusOuter",
"borderRadiusSM",
"borderRadiusXS",
"boxShadow",
"boxShadowDrawerLeft",
"boxShadowDrawerRight",
"boxShadowDrawerUp",
"boxShadowPopoverArrow",
"boxShadowSecondary",
"boxShadowTabsOverflowBottom",
"boxShadowTabsOverflowLeft",
"boxShadowTabsOverflowRight",
"boxShadowTabsOverflowTop",
"boxShadowTertiary",
"colorBgBase",
"colorBgBlur",
"colorBgContainer",
"colorBgContainerDisabled",
"colorBgElevated",
"colorBgLayout",
"colorBgMask",
"colorBgSpotlight",
"colorBgTextActive",
"colorBgTextHover",
"colorBorder",
"colorBorderBg",
"colorBorderSecondary",
"colorError",
"colorErrorActive",
"colorErrorBg",
"colorErrorBgActive",
"colorErrorBgHover",
"colorErrorBorder",
"colorErrorBorderHover",
"colorErrorHover",
"colorErrorOutline",
"colorErrorText",
"colorErrorTextActive",
"colorErrorTextHover",
"colorFill",
"colorFillAlter",
"colorFillContent",
"colorFillContentHover",
"colorFillQuaternary",
"colorFillSecondary",
"colorFillTertiary",
"colorHighlight",
"colorIcon",
"colorIconHover",
"colorInfo",
"colorInfoActive",
"colorInfoBg",
"colorInfoBgHover",
"colorInfoBorder",
"colorInfoBorderHover",
"colorInfoHover",
"colorInfoText",
"colorInfoTextActive",
"colorInfoTextHover",
"colorLink",
"colorLinkActive",
"colorLinkHover",
"colorPrimary",
"colorPrimaryActive",
"colorPrimaryBg",
"colorPrimaryBgHover",
"colorPrimaryBorder",
"colorPrimaryBorderHover",
"colorPrimaryHover",
"colorPrimaryText",
"colorPrimaryTextActive",
"colorPrimaryTextHover",
"colorSplit",
"colorSuccess",
"colorSuccessActive",
"colorSuccessBg",
"colorSuccessBgHover",
"colorSuccessBorder",
"colorSuccessBorderHover",
"colorSuccessHover",
"colorSuccessText",
"colorSuccessTextActive",
"colorSuccessTextHover",
"colorText",
"colorTextBase",
"colorTextDescription",
"colorTextDisabled",
"colorTextHeading",
"colorTextLabel",
"colorTextLightSolid",
"colorTextPlaceholder",
"colorTextQuaternary",
"colorTextSecondary",
"colorTextTertiary",
"colorWarning",
"colorWarningActive",
"colorWarningBg",
"colorWarningBgHover",
"colorWarningBorder",
"colorWarningBorderHover",
"colorWarningHover",
"colorWarningOutline",
"colorWarningText",
"colorWarningTextActive",
"colorWarningTextHover",
"colorWhite",
"controlHeight",
"controlHeightLG",
"controlHeightSM",
"controlHeightXS",
"controlInteractiveSize",
"controlItemBgActive",
"controlItemBgActiveDisabled",
"controlItemBgActiveHover",
"controlItemBgHover",
"controlOutline",
"controlOutlineWidth",
"controlPaddingHorizontal",
"controlPaddingHorizontalSM",
"controlTmpOutline",
"fontFamily",
"fontFamilyCode",
"fontHeight",
"fontHeightLG",
"fontHeightSM",
"fontSize",
"fontSizeHeading1",
"fontSizeHeading2",
"fontSizeHeading3",
"fontSizeHeading4",
"fontSizeHeading5",
"fontSizeIcon",
"fontSizeLG",
"fontSizeSM",
"fontSizeXL",
"fontWeightStrong",
"lineHeight",
"lineHeightHeading1",
"lineHeightHeading2",
"lineHeightHeading3",
"lineHeightHeading4",
"lineHeightHeading5",
"lineHeightLG",
"lineHeightSM",
"lineType",
"lineWidth",
"lineWidthBold",
"lineWidthFocus",
"linkDecoration",
"linkFocusDecoration",
"linkHoverDecoration",
"margin",
"marginLG",
"marginMD",
"marginSM",
"marginXL",
"marginXS",
"marginXXL",
"marginXXS",
"motion",
"motionBase",
"motionDurationFast",
"motionDurationMid",
"motionDurationSlow",
"motionEaseInBack",
"motionEaseInOut",
"motionEaseInOutCirc",
"motionEaseInQuint",
"motionEaseOut",
"motionEaseOutBack",
"motionEaseOutCirc",
"motionEaseOutQuint",
"motionUnit",
"opacityImage",
"opacityLoading",
"padding",
"paddingContentHorizontal",
"paddingContentHorizontalLG",
"paddingContentHorizontalSM",
"paddingContentVertical",
"paddingContentVerticalLG",
"paddingContentVerticalSM",
"paddingLG",
"paddingMD",
"paddingSM",
"paddingXL",
"paddingXS",
"paddingXXS",
"screenLG",
"screenLGMax",
"screenLGMin",
"screenMD",
"screenMDMax",
"screenMDMin",
"screenSM",
"screenSMMax",
"screenSMMin",
"screenXL",
"screenXLMax",
"screenXLMin",
"screenXS",
"screenXSMax",
"screenXSMin",
"screenXXL",
"screenXXLMin",
"size",
"sizeLG",
"sizeMD",
"sizeMS",
"sizePopupArrow",
"sizeSM",
"sizeStep",
"sizeUnit",
"sizeXL",
"sizeXS",
"sizeXXL",
"sizeXXS",
"wireframe",
"zIndexBase",
"zIndexPopupBase",
]
`;
exports[`computed dark-mode token values match snapshot 1`] = `
{
"borderRadius": 6,
"borderRadiusLG": 8,
"borderRadiusOuter": 4,
"borderRadiusSM": 4,
"borderRadiusXS": 2,
"boxShadow": "
0 6px 16px 0 rgba(255,255,255,0.016),
0 3px 6px -4px rgba(255,255,255,0.024),
0 9px 28px 8px rgba(255,255,255,0.010000000000000002)
",
"boxShadowDrawerLeft": "
6px 0 16px 0 rgba(255,255,255,0.016),
3px 0 6px -4px rgba(255,255,255,0.024),
9px 0 28px 8px rgba(255,255,255,0.010000000000000002)
",
"boxShadowDrawerRight": "
-6px 0 16px 0 rgba(255,255,255,0.016),
-3px 0 6px -4px rgba(255,255,255,0.024),
-9px 0 28px 8px rgba(255,255,255,0.010000000000000002)
",
"boxShadowDrawerUp": "
0 6px 16px 0 rgba(255,255,255,0.016),
0 3px 6px -4px rgba(255,255,255,0.024),
0 9px 28px 8px rgba(255,255,255,0.010000000000000002)
",
"boxShadowPopoverArrow": "2px 2px 5px rgba(255,255,255,0.010000000000000002)",
"boxShadowSecondary": "
0 6px 16px 0 rgba(255,255,255,0.016),
0 3px 6px -4px rgba(255,255,255,0.024),
0 9px 28px 8px rgba(255,255,255,0.010000000000000002)
",
"boxShadowTabsOverflowBottom": "inset 0 -10px 8px -8px rgba(255,255,255,0.016)",
"boxShadowTabsOverflowLeft": "inset 10px 0 8px -8px rgba(255,255,255,0.016)",
"boxShadowTabsOverflowRight": "inset -10px 0 8px -8px rgba(255,255,255,0.016)",
"boxShadowTabsOverflowTop": "inset 0 10px 8px -8px rgba(255,255,255,0.016)",
"boxShadowTertiary": "
0 1px 2px 0 rgba(255,255,255,0.010000000000000002),
0 1px 6px -1px rgba(255,255,255,0.006),
0 2px 4px 0 rgba(255,255,255,0.006)
",
"colorBgBase": "#000",
"colorBgBlur": "rgba(255,255,255,0.04)",
"colorBgContainer": "#141414",
"colorBgContainerDisabled": "rgba(255,255,255,0.08)",
"colorBgElevated": "#1f1f1f",
"colorBgLayout": "#000000",
"colorBgMask": "rgba(0,0,0,0.45)",
"colorBgSpotlight": "#424242",
"colorBgTextActive": "rgba(255,255,255,0.18)",
"colorBgTextHover": "rgba(255,255,255,0.12)",
"colorBorder": "#424242",
"colorBorderBg": "#141414",
"colorBorderSecondary": "#303030",
"colorError": "#e04355",
"colorErrorActive": "#99333e",
"colorErrorBg": "#271619",
"colorErrorBgActive": "#512228",
"colorErrorBgHover": "#3e1b20",
"colorErrorBorder": "#512228",
"colorErrorBorderHover": "#702931",
"colorErrorHover": "#d7646e",
"colorErrorOutline": "rgba(231,42,76,0.09)",
"colorErrorText": "#c13c4b",
"colorErrorTextActive": "#99333e",
"colorErrorTextHover": "#d7646e",
"colorFill": "rgba(255,255,255,0.18)",
"colorFillAlter": "rgba(255,255,255,0.04)",
"colorFillContent": "rgba(255,255,255,0.12)",
"colorFillContentHover": "rgba(255,255,255,0.18)",
"colorFillQuaternary": "rgba(255,255,255,0.04)",
"colorFillSecondary": "rgba(255,255,255,0.12)",
"colorFillTertiary": "rgba(255,255,255,0.08)",
"colorHighlight": "#c13c4b",
"colorIcon": "rgba(255,255,255,0.45)",
"colorIconHover": "rgba(255,255,255,0.85)",
"colorInfo": "#66bcfe",
"colorInfoActive": "#4981ac",
"colorInfoBg": "#19222c",
"colorInfoBgHover": "#223545",
"colorInfoBorder": "#2d465a",
"colorInfoBorderHover": "#39607d",
"colorInfoHover": "#39607d",
"colorInfoText": "#5aa3db",
"colorInfoTextActive": "#4981ac",
"colorInfoTextHover": "#83bfe8",
"colorLink": "#2893B3",
"colorLinkActive": "#21677b",
"colorLinkHover": "#1d4d5c",
"colorPrimary": "#2893B3",
"colorPrimaryActive": "#21677b",
"colorPrimaryBg": "#1a3a44",
"colorPrimaryBgHover": "#1d4d5c",
"colorPrimaryBorder": "#1a3a44",
"colorPrimaryBorderHover": "#1d4d5c",
"colorPrimaryHover": "#4499ae",
"colorPrimaryText": "#25809b",
"colorPrimaryTextActive": "#21677b",
"colorPrimaryTextHover": "#4499ae",
"colorSplit": "rgba(253,253,253,0.12)",
"colorSuccess": "#5ac189",
"colorSuccessActive": "#428460",
"colorSuccessBg": "#17231d",
"colorSuccessBgHover": "#1f362a",
"colorSuccessBorder": "#294837",
"colorSuccessBorderHover": "#346249",
"colorSuccessHover": "#346249",
"colorSuccessText": "#50a777",
"colorSuccessTextActive": "#428460",
"colorSuccessTextHover": "#77bc94",
"colorText": "rgba(255,255,255,0.85)",
"colorTextBase": "#fff",
"colorTextDescription": "rgba(255,255,255,0.45)",
"colorTextDisabled": "rgba(255,255,255,0.25)",
"colorTextHeading": "rgba(255,255,255,0.85)",
"colorTextLabel": "rgba(255,255,255,0.65)",
"colorTextLightSolid": "#fff",
"colorTextPlaceholder": "rgba(255,255,255,0.25)",
"colorTextQuaternary": "rgba(255,255,255,0.25)",
"colorTextSecondary": "rgba(255,255,255,0.65)",
"colorTextTertiary": "rgba(255,255,255,0.45)",
"colorWarning": "#fcc700",
"colorWarningActive": "#ab8807",
"colorWarningBg": "#2b2411",
"colorWarningBgHover": "#45370f",
"colorWarningBorder": "#5a4a0e",
"colorWarningBorderHover": "#7c650b",
"colorWarningHover": "#7c650b",
"colorWarningOutline": "rgba(173,127,0,0.15)",
"colorWarningText": "#d9ac03",
"colorWarningTextActive": "#ab8807",
"colorWarningTextHover": "#e8c427",
"colorWhite": "#fff",
"controlHeight": 32,
"controlHeightLG": 40,
"controlHeightSM": 24,
"controlHeightXS": 16,
"controlInteractiveSize": 16,
"controlItemBgActive": "#1a3a44",
"controlItemBgActiveDisabled": "rgba(255,255,255,0.18)",
"controlItemBgActiveHover": "#1d4d5c",
"controlItemBgHover": "rgba(255,255,255,0.08)",
"controlOutline": "rgba(49,201,249,0.21)",
"controlOutlineWidth": 2,
"controlPaddingHorizontal": 12,
"controlPaddingHorizontalSM": 8,
"controlTmpOutline": "rgba(255,255,255,0.04)",
"fontFamily": "'Inter', Helvetica, Arial",
"fontFamilyCode": "'IBM Plex Mono', 'Courier New', monospace",
"fontHeight": 22,
"fontHeightLG": 24,
"fontHeightSM": 20,
"fontSize": 14,
"fontSizeHeading1": 38,
"fontSizeHeading2": 30,
"fontSizeHeading3": 24,
"fontSizeHeading4": 20,
"fontSizeHeading5": 16,
"fontSizeIcon": 12,
"fontSizeLG": 16,
"fontSizeSM": 12,
"fontSizeXL": 20,
"fontWeightStrong": 600,
"lineHeight": 1.5714285714285714,
"lineHeightHeading1": 1.2105263157894737,
"lineHeightHeading2": 1.2666666666666666,
"lineHeightHeading3": 1.3333333333333333,
"lineHeightHeading4": 1.4,
"lineHeightHeading5": 1.5,
"lineHeightLG": 1.5,
"lineHeightSM": 1.6666666666666667,
"lineType": "solid",
"lineWidth": 1,
"lineWidthBold": 2,
"lineWidthFocus": 3,
"linkDecoration": "none",
"linkFocusDecoration": "none",
"linkHoverDecoration": "none",
"margin": 16,
"marginLG": 24,
"marginMD": 20,
"marginSM": 12,
"marginXL": 32,
"marginXS": 8,
"marginXXL": 48,
"marginXXS": 4,
"motion": true,
"motionBase": 0,
"motionDurationFast": "0.1s",
"motionDurationMid": "0.2s",
"motionDurationSlow": "0.3s",
"motionEaseInBack": "cubic-bezier(0.71, -0.46, 0.88, 0.6)",
"motionEaseInOut": "cubic-bezier(0.645, 0.045, 0.355, 1)",
"motionEaseInOutCirc": "cubic-bezier(0.78, 0.14, 0.15, 0.86)",
"motionEaseInQuint": "cubic-bezier(0.755, 0.05, 0.855, 0.06)",
"motionEaseOut": "cubic-bezier(0.215, 0.61, 0.355, 1)",
"motionEaseOutBack": "cubic-bezier(0.12, 0.4, 0.29, 1.46)",
"motionEaseOutCirc": "cubic-bezier(0.08, 0.82, 0.17, 1)",
"motionEaseOutQuint": "cubic-bezier(0.23, 1, 0.32, 1)",
"motionUnit": 0.1,
"opacityImage": 1,
"opacityLoading": 0.65,
"padding": 16,
"paddingContentHorizontal": 16,
"paddingContentHorizontalLG": 24,
"paddingContentHorizontalSM": 16,
"paddingContentVertical": 12,
"paddingContentVerticalLG": 16,
"paddingContentVerticalSM": 8,
"paddingLG": 24,
"paddingMD": 20,
"paddingSM": 12,
"paddingXL": 32,
"paddingXS": 8,
"paddingXXS": 4,
"screenLG": 992,
"screenLGMax": 1199,
"screenLGMin": 992,
"screenMD": 768,
"screenMDMax": 991,
"screenMDMin": 768,
"screenSM": 576,
"screenSMMax": 767,
"screenSMMin": 576,
"screenXL": 1200,
"screenXLMax": 1599,
"screenXLMin": 1200,
"screenXS": 480,
"screenXSMax": 575,
"screenXSMin": 480,
"screenXXL": 1600,
"screenXXLMin": 1600,
"size": 16,
"sizeLG": 24,
"sizeMD": 20,
"sizeMS": 16,
"sizePopupArrow": 16,
"sizeSM": 12,
"sizeStep": 4,
"sizeUnit": 4,
"sizeXL": 32,
"sizeXS": 8,
"sizeXXL": 48,
"sizeXXS": 4,
"wireframe": false,
"zIndexBase": 0,
"zIndexPopupBase": 1000,
}
`;
exports[`computed light-mode token values match snapshot 1`] = `
{
"borderRadius": 6,
"borderRadiusLG": 8,
"borderRadiusOuter": 4,
"borderRadiusSM": 4,
"borderRadiusXS": 2,
"boxShadow": "
0 6px 16px 0 rgba(0,0,0,0.08),
0 3px 6px -4px rgba(0,0,0,0.12),
0 9px 28px 8px rgba(0,0,0,0.05)
",
"boxShadowDrawerLeft": "
6px 0 16px 0 rgba(0,0,0,0.08),
3px 0 6px -4px rgba(0,0,0,0.12),
9px 0 28px 8px rgba(0,0,0,0.05)
",
"boxShadowDrawerRight": "
-6px 0 16px 0 rgba(0,0,0,0.08),
-3px 0 6px -4px rgba(0,0,0,0.12),
-9px 0 28px 8px rgba(0,0,0,0.05)
",
"boxShadowDrawerUp": "
0 6px 16px 0 rgba(0,0,0,0.08),
0 3px 6px -4px rgba(0,0,0,0.12),
0 9px 28px 8px rgba(0,0,0,0.05)
",
"boxShadowPopoverArrow": "2px 2px 5px rgba(0,0,0,0.05)",
"boxShadowSecondary": "
0 6px 16px 0 rgba(0,0,0,0.08),
0 3px 6px -4px rgba(0,0,0,0.12),
0 9px 28px 8px rgba(0,0,0,0.05)
",
"boxShadowTabsOverflowBottom": "inset 0 -10px 8px -8px rgba(0,0,0,0.08)",
"boxShadowTabsOverflowLeft": "inset 10px 0 8px -8px rgba(0,0,0,0.08)",
"boxShadowTabsOverflowRight": "inset -10px 0 8px -8px rgba(0,0,0,0.08)",
"boxShadowTabsOverflowTop": "inset 0 10px 8px -8px rgba(0,0,0,0.08)",
"boxShadowTertiary": "
0 1px 2px 0 rgba(0,0,0,0.05),
0 1px 6px -1px rgba(0,0,0,0.03),
0 2px 4px 0 rgba(0,0,0,0.03)
",
"colorBgBase": "#fff",
"colorBgBlur": "transparent",
"colorBgContainer": "#ffffff",
"colorBgContainerDisabled": "rgba(0,0,0,0.04)",
"colorBgElevated": "#ffffff",
"colorBgLayout": "#f5f5f5",
"colorBgMask": "rgba(0,0,0,0.45)",
"colorBgSpotlight": "rgba(0,0,0,0.85)",
"colorBgTextActive": "rgba(0,0,0,0.15)",
"colorBgTextHover": "rgba(0,0,0,0.06)",
"colorBorder": "#d9d9d9",
"colorBorderBg": "#ffffff",
"colorBorderSecondary": "#f0f0f0",
"colorError": "#e04355",
"colorErrorActive": "#ba2f43",
"colorErrorBg": "#fff0f0",
"colorErrorBgActive": "#ffc7c8",
"colorErrorBgHover": "#fff0f0",
"colorErrorBorder": "#ffc7c8",
"colorErrorBorderHover": "#fa9ba0",
"colorErrorHover": "#ed6d78",
"colorErrorOutline": "rgba(255,5,5,0.06)",
"colorErrorText": "#e04355",
"colorErrorTextActive": "#ba2f43",
"colorErrorTextHover": "#ed6d78",
"colorFill": "rgba(0,0,0,0.15)",
"colorFillAlter": "rgba(0,0,0,0.02)",
"colorFillContent": "rgba(0,0,0,0.06)",
"colorFillContentHover": "rgba(0,0,0,0.15)",
"colorFillQuaternary": "rgba(0,0,0,0.02)",
"colorFillSecondary": "rgba(0,0,0,0.06)",
"colorFillTertiary": "rgba(0,0,0,0.04)",
"colorHighlight": "#e04355",
"colorIcon": "rgba(0,0,0,0.45)",
"colorIconHover": "rgba(0,0,0,0.88)",
"colorInfo": "#66bcfe",
"colorInfoActive": "#4c97d9",
"colorInfoBg": "#f0fbff",
"colorInfoBgHover": "#f0faff",
"colorInfoBorder": "#e0f5ff",
"colorInfoBorderHover": "#b8e5ff",
"colorInfoHover": "#b8e5ff",
"colorInfoText": "#66bcfe",
"colorInfoTextActive": "#4c97d9",
"colorInfoTextHover": "#8fd2ff",
"colorLink": "#2893B3",
"colorLinkActive": "#186d8c",
"colorLinkHover": "#6ebccc",
"colorPrimary": "#2893B3",
"colorPrimaryActive": "#186d8c",
"colorPrimaryBg": "#e4f1f2",
"colorPrimaryBgHover": "#c5e2e6",
"colorPrimaryBorder": "#98d0d9",
"colorPrimaryBorderHover": "#6ebccc",
"colorPrimaryHover": "#49a8bf",
"colorPrimaryText": "#2893b3",
"colorPrimaryTextActive": "#186d8c",
"colorPrimaryTextHover": "#49a8bf",
"colorSplit": "rgba(5,5,5,0.06)",
"colorSuccess": "#5ac189",
"colorSuccessActive": "#419c6d",
"colorSuccessBg": "#f0fff4",
"colorSuccessBgHover": "#e6f5eb",
"colorSuccessBorder": "#dae8df",
"colorSuccessBorderHover": "#addbbf",
"colorSuccessHover": "#addbbf",
"colorSuccessText": "#5ac189",
"colorSuccessTextActive": "#419c6d",
"colorSuccessTextHover": "#82cfa2",
"colorText": "rgba(0,0,0,0.88)",
"colorTextBase": "#000",
"colorTextDescription": "rgba(0,0,0,0.45)",
"colorTextDisabled": "rgba(0,0,0,0.25)",
"colorTextHeading": "rgba(0,0,0,0.88)",
"colorTextLabel": "rgba(0,0,0,0.65)",
"colorTextLightSolid": "#fff",
"colorTextPlaceholder": "rgba(0,0,0,0.25)",
"colorTextQuaternary": "rgba(0,0,0,0.25)",
"colorTextSecondary": "rgba(0,0,0,0.65)",
"colorTextTertiary": "rgba(0,0,0,0.45)",
"colorWarning": "#fcc700",
"colorWarningActive": "#d6a100",
"colorWarningBg": "#fffee6",
"colorWarningBgHover": "#fff7a3",
"colorWarningBorder": "#fff07a",
"colorWarningBorderHover": "#ffe552",
"colorWarningHover": "#ffe552",
"colorWarningOutline": "rgba(255,245,5,0.1)",
"colorWarningText": "#fcc700",
"colorWarningTextActive": "#d6a100",
"colorWarningTextHover": "#ffd829",
"colorWhite": "#fff",
"controlHeight": 32,
"controlHeightLG": 40,
"controlHeightSM": 24,
"controlHeightXS": 16,
"controlInteractiveSize": 16,
"controlItemBgActive": "#e4f1f2",
"controlItemBgActiveDisabled": "rgba(0,0,0,0.15)",
"controlItemBgActiveHover": "#c5e2e6",
"controlItemBgHover": "rgba(0,0,0,0.04)",
"controlOutline": "rgba(10,128,137,0.11)",
"controlOutlineWidth": 2,
"controlPaddingHorizontal": 12,
"controlPaddingHorizontalSM": 8,
"controlTmpOutline": "rgba(0,0,0,0.02)",
"fontFamily": "'Inter', Helvetica, Arial",
"fontFamilyCode": "'IBM Plex Mono', 'Courier New', monospace",
"fontHeight": 22,
"fontHeightLG": 24,
"fontHeightSM": 20,
"fontSize": 14,
"fontSizeHeading1": 38,
"fontSizeHeading2": 30,
"fontSizeHeading3": 24,
"fontSizeHeading4": 20,
"fontSizeHeading5": 16,
"fontSizeIcon": 12,
"fontSizeLG": 16,
"fontSizeSM": 12,
"fontSizeXL": 20,
"fontWeightStrong": 600,
"lineHeight": 1.5714285714285714,
"lineHeightHeading1": 1.2105263157894737,
"lineHeightHeading2": 1.2666666666666666,
"lineHeightHeading3": 1.3333333333333333,
"lineHeightHeading4": 1.4,
"lineHeightHeading5": 1.5,
"lineHeightLG": 1.5,
"lineHeightSM": 1.6666666666666667,
"lineType": "solid",
"lineWidth": 1,
"lineWidthBold": 2,
"lineWidthFocus": 3,
"linkDecoration": "none",
"linkFocusDecoration": "none",
"linkHoverDecoration": "none",
"margin": 16,
"marginLG": 24,
"marginMD": 20,
"marginSM": 12,
"marginXL": 32,
"marginXS": 8,
"marginXXL": 48,
"marginXXS": 4,
"motion": true,
"motionBase": 0,
"motionDurationFast": "0.1s",
"motionDurationMid": "0.2s",
"motionDurationSlow": "0.3s",
"motionEaseInBack": "cubic-bezier(0.71, -0.46, 0.88, 0.6)",
"motionEaseInOut": "cubic-bezier(0.645, 0.045, 0.355, 1)",
"motionEaseInOutCirc": "cubic-bezier(0.78, 0.14, 0.15, 0.86)",
"motionEaseInQuint": "cubic-bezier(0.755, 0.05, 0.855, 0.06)",
"motionEaseOut": "cubic-bezier(0.215, 0.61, 0.355, 1)",
"motionEaseOutBack": "cubic-bezier(0.12, 0.4, 0.29, 1.46)",
"motionEaseOutCirc": "cubic-bezier(0.08, 0.82, 0.17, 1)",
"motionEaseOutQuint": "cubic-bezier(0.23, 1, 0.32, 1)",
"motionUnit": 0.1,
"opacityImage": 1,
"opacityLoading": 0.65,
"padding": 16,
"paddingContentHorizontal": 16,
"paddingContentHorizontalLG": 24,
"paddingContentHorizontalSM": 16,
"paddingContentVertical": 12,
"paddingContentVerticalLG": 16,
"paddingContentVerticalSM": 8,
"paddingLG": 24,
"paddingMD": 20,
"paddingSM": 12,
"paddingXL": 32,
"paddingXS": 8,
"paddingXXS": 4,
"screenLG": 992,
"screenLGMax": 1199,
"screenLGMin": 992,
"screenMD": 768,
"screenMDMax": 991,
"screenMDMin": 768,
"screenSM": 576,
"screenSMMax": 767,
"screenSMMin": 576,
"screenXL": 1200,
"screenXLMax": 1599,
"screenXLMin": 1200,
"screenXS": 480,
"screenXSMax": 575,
"screenXSMin": 480,
"screenXXL": 1600,
"screenXXLMin": 1600,
"size": 16,
"sizeLG": 24,
"sizeMD": 20,
"sizeMS": 16,
"sizePopupArrow": 16,
"sizeSM": 12,
"sizeStep": 4,
"sizeUnit": 4,
"sizeXL": 32,
"sizeXS": 8,
"sizeXXL": 48,
"sizeXXS": 4,
"wireframe": false,
"zIndexBase": 0,
"zIndexPopupBase": 1000,
}
`;

View File

@@ -48,7 +48,7 @@ export function isSerializableConfig(
export function deserializeThemeConfig(
config: SerializableThemeConfig,
): AntdThemeConfig {
const { algorithm, ...rest } = config;
const { algorithm, cssVar, ...rest } = config;
const algorithmMap: Record<string, any> = {
default: antdThemeImport.defaultAlgorithm,
dark: antdThemeImport.darkAlgorithm,
@@ -74,9 +74,17 @@ export function deserializeThemeConfig(
resolvedAlgorithm = antdThemeImport.defaultAlgorithm;
}
// Ant Design v6 dropped `boolean` from ThemeConfig['cssVar'] (it is now
// object-only, and CSS variables are enabled by default). Superset keeps a
// boolean-friendly serializable API, so coerce at the antd boundary:
// `true` -> `{}` (defaults), `false`/undefined -> omit (antd default).
const normalizedCssVar =
typeof cssVar === 'boolean' ? (cssVar ? {} : undefined) : cssVar;
return {
...rest,
algorithm: resolvedAlgorithm,
...(normalizedCssVar !== undefined ? { cssVar: normalizedCssVar } : {}),
};
}

View File

@@ -31,7 +31,7 @@
"lodash-es": "^4.17.21"
},
"peerDependencies": {
"@ant-design/icons": "^5.6.1",
"@ant-design/icons": "^5.6.1 || ^6.0.0",
"@emotion/react": "^11.4.1",
"@superset-ui/core": "*",
"@testing-library/dom": "^9.3.4",

View File

@@ -100,7 +100,7 @@
"@types/react-loadable": "*",
"@types/react-window": "^1.8.8",
"@types/tinycolor2": "*",
"antd": "^5.26.0",
"antd": "^6.0.0",
"nanoid": "^5.0.9",
"react": "^18.3.0",
"react-dom": "^18.3.0",

View File

@@ -82,8 +82,10 @@ test('collapses on click', async () => {
await userEvent.click(screen.getAllByRole('button')[0]);
expect(screen.getByText('Content 1').parentNode).toHaveClass(
'ant-collapse-content-hidden',
// antd v6 moved the hidden state to the panel wrapper element
// (ant-collapse-panel-hidden) instead of the content wrapper.
expect(screen.getByText('Content 1').parentElement).toHaveClass(
'ant-collapse-panel-hidden',
);
expect(screen.queryByText('Content 2')).not.toBeInTheDocument();
});

View File

@@ -175,7 +175,7 @@ export const CronPicker = styled((props: CronProps) => (
.react-js-cron-select.ant-select {
width: 100%;
.ant-select-selector {
.ant-select-content {
flex-wrap: nowrap;
}
}

View File

@@ -361,7 +361,8 @@ export const DropdownContainer = forwardRef(
<Popover
styles={{
body: {
// antd v6 renamed the inner content slot `body` -> `container`
container: {
maxHeight: `${MAX_HEIGHT}px`,
overflow: showOverflow ? 'auto' : 'visible',
},

View File

@@ -18,6 +18,7 @@
*/
import { fireEvent, render } from '@superset-ui/core/spec';
import { Icons } from '../Icons';
import { Label } from '.';
import { LabelGallery, options } from './Label.stories';
@@ -39,6 +40,21 @@ test('renders with monospace prop', () => {
expect(getByText('monospace text')).toBeInTheDocument();
});
// Regression: Ant Design v6's Tag clones the element passed via its `icon` prop
// and overwrites that element's inline `style`, which silently dropped the
// icon's own color. Label wraps the icon in a span so the wrapper (not the
// icon) is Tag's clone target and the icon keeps its explicit color.
test('preserves a custom icon color (antd v6 Tag icon-style regression)', () => {
const { container } = render(
<Label icon={<Icons.CheckCircleOutlined iconColor="#aabbcc" />}>
labeled
</Label>,
);
expect(container.querySelector('[role="img"]')).toHaveStyle({
color: '#aabbcc',
});
});
// test stories from the storybook!
test('renders all the storybook gallery variants', () => {
const { container } = render(<LabelGallery />);

View File

@@ -76,7 +76,27 @@ export const Label = forwardRef<HTMLSpanElement, LabelProps>((props, ref) => {
onClick={onClick}
role={onClick ? 'button' : undefined}
style={style}
icon={icon}
/*
* Ant Design v6's Tag clones the `icon` element and overrides its inline
* `style` (see antd/es/tag: cloneElement(icon, { style: mergedStyles.icon })),
* which would drop the icon's own color/size. Wrapping the icon in a span
* lets Tag override the (empty) wrapper style while the real icon keeps its
* own inline styling.
*/
icon={
icon ? (
<span
css={css`
display: inline-flex;
align-items: center;
`}
>
{icon}
</span>
) : (
icon
)
}
css={labelStyles}
{...rest}
>

View File

@@ -41,7 +41,7 @@ const defaultProps: PageHeaderWithActionsProps = {
data-test="additional-actions-menu"
/>
),
menuDropdownProps: { onVisibleChange: jest.fn(), visible: true },
menuDropdownProps: { onOpenChange: jest.fn(), open: true },
};
test('Renders', async () => {
@@ -52,5 +52,5 @@ test('Renders', async () => {
expect(screen.getByText('Save')).toBeVisible();
await userEvent.click(screen.getByLabelText('Menu actions trigger'));
expect(defaultProps.menuDropdownProps.onVisibleChange).toHaveBeenCalled();
expect(defaultProps.menuDropdownProps.onOpenChange).toHaveBeenCalled();
});

View File

@@ -119,10 +119,22 @@ const findAllSelectOptions = () =>
waitFor(() => getElementsByClassName('.ant-select-item-option-content'));
const findSelectValue = () =>
waitFor(() => getElementByClassName('.ant-select-selection-item'));
// antd v6: single-mode value is `.ant-select-content-has-value`, multiple-mode
// tags remain `.ant-select-selection-item`.
waitFor(() =>
getElementByClassName(
'.ant-select-content-has-value, .ant-select-selection-item',
),
);
const findAllSelectValues = () =>
waitFor(() => getElementsByClassName('.ant-select-selection-item'));
// antd v6: multiple-mode tags keep `.ant-select-selection-item`, single-mode
// value is `.ant-select-content-has-value`.
waitFor(() =>
getElementsByClassName(
'.ant-select-selection-item, .ant-select-content-has-value',
),
);
const clearAll = () => userEvent.click(screen.getByLabelText('close-circle'));
@@ -381,7 +393,7 @@ test('searches for custom fields', async () => {
test('removes duplicated values', async () => {
render(<AsyncSelect {...defaultProps} mode="multiple" allowNewOptions />);
const input = getElementByClassName('.ant-select-selection-search-input');
const input = getElementByClassName('.ant-select-input');
const paste = createEvent.paste(input, {
clipboardData: {
getData: () => 'a,b,b,b,c,d,d',
@@ -814,7 +826,7 @@ test('Renders only an overflow tag if dropdown is open in oneLine mode', async (
);
await open();
const withinSelector = within(getElementByClassName('.ant-select-selector'));
const withinSelector = within(getElementByClassName('.ant-select-content'));
await waitFor(() => {
expect(
withinSelector.queryByText(OPTIONS[0].label),
@@ -887,7 +899,7 @@ test('fires onChange when pasting a selection', async () => {
const onChange = jest.fn();
render(<AsyncSelect {...defaultProps} onChange={onChange} />);
await open();
const input = getElementByClassName('.ant-select-selection-search-input');
const input = getElementByClassName('.ant-select-input');
const paste = createEvent.paste(input, {
clipboardData: {
getData: () => OPTIONS[0].label,
@@ -1164,7 +1176,7 @@ test('keeps loading indicator while a newer request is in flight after a stale r
});
const isSpinnerVisible = (): boolean =>
Boolean(document.querySelector('.ant-select-arrow .ant-spin'));
Boolean(document.querySelector('.ant-select-suffix .ant-spin'));
try {
render(<AsyncSelect {...defaultProps} options={loadOptions} />);
@@ -1312,7 +1324,7 @@ test('appends page>1 results during an active search and discards them when sear
// Wait for loading to finish so handlePagination's `!isLoading` gate is
// open before we fire scroll.
await waitFor(() =>
expect(document.querySelector('.ant-select-arrow .ant-spin')).toBeNull(),
expect(document.querySelector('.ant-select-suffix .ant-spin')).toBeNull(),
);
// Trigger pagination by dispatching a scroll event on the virtual-list
@@ -1392,7 +1404,7 @@ test('pasting an existing option does not duplicate it', async () => {
}));
render(<AsyncSelect {...defaultProps} options={options} />);
await open();
const input = getElementByClassName('.ant-select-selection-search-input');
const input = getElementByClassName('.ant-select-input');
const paste = createEvent.paste(input, {
clipboardData: {
getData: () => OPTIONS[0].label,
@@ -1420,7 +1432,7 @@ test('pasting an existing option does not duplicate it in multiple mode', async
/>,
);
await open();
const input = getElementByClassName('.ant-select-selection-search-input');
const input = getElementByClassName('.ant-select-input');
const paste = createEvent.paste(input, {
clipboardData: {
getData: () => 'John,Liam,Peter',
@@ -1442,7 +1454,7 @@ test('pasting an non-existent option should not add it if allowNewOptions is fal
/>,
);
await open();
const input = getElementByClassName('.ant-select-selection-search-input');
const input = getElementByClassName('.ant-select-input');
const paste = createEvent.paste(input, {
clipboardData: {
getData: () => 'John',
@@ -1456,7 +1468,7 @@ test('onChange is called with the value property when pasting an option that was
const onChange = jest.fn();
render(<AsyncSelect {...defaultProps} onChange={onChange} />);
await open();
const input = getElementByClassName('.ant-select-selection-search-input');
const input = getElementByClassName('.ant-select-input');
const lastOption = OPTIONS[OPTIONS.length - 1];
const paste = createEvent.paste(input, {
clipboardData: {

View File

@@ -675,7 +675,14 @@ const AsyncSelect = forwardRef(
setSelectValue(value);
}
} else {
const token = tokenSeparators.find(token => pastedText.includes(token));
// antd v6 widened `tokenSeparators` to `string[] | (input => string[])`;
// Superset always uses the array form.
const separators = Array.isArray(tokenSeparators)
? tokenSeparators
: [];
const token = separators.find((token: string) =>
pastedText.includes(token),
);
const array = token ? uniq(pastedText.split(token)) : [pastedText];
const values = (
await Promise.all(array.map(item => getPastedTextValue(item)))
@@ -730,7 +737,11 @@ const AsyncSelect = forwardRef(
showSearch={allowNewOptions ? true : showSearch}
tokenSeparators={tokenSeparators}
value={selectValue}
suffixIcon={getSuffixIcon(isLoading, showSearch, isDropdownVisible)}
suffixIcon={getSuffixIcon(
isLoading,
Boolean(showSearch),
isDropdownVisible,
)}
menuItemSelectedIcon={
invertSelection ? (
<StyledStopOutlined iconSize="m" aria-label="stop" />

View File

@@ -108,10 +108,22 @@ const findAllSelectOptions = () =>
waitFor(() => getElementsByClassName('.ant-select-item-option-content'));
const findSelectValue = () =>
waitFor(() => getElementByClassName('.ant-select-selection-item'));
// antd v6: single-mode value is `.ant-select-content-has-value`, multiple-mode
// tags remain `.ant-select-selection-item`.
waitFor(() =>
getElementByClassName(
'.ant-select-content-has-value, .ant-select-selection-item',
),
);
const findAllSelectValues = () =>
waitFor(() => [...getElementsByClassName('.ant-select-selection-item')]);
// antd v6: multiple-mode tags keep `.ant-select-selection-item`, single-mode
// value is `.ant-select-content-has-value`.
waitFor(() => [
...getElementsByClassName(
'.ant-select-selection-item, .ant-select-content-has-value',
),
]);
const clearAll = () => userEvent.click(screen.getByLabelText('close-circle'));
@@ -363,7 +375,7 @@ test('searches for custom fields', async () => {
test('removes duplicated values', async () => {
render(<Select {...defaultProps} mode="multiple" allowNewOptions />);
const input = getElementByClassName('.ant-select-selection-search-input');
const input = getElementByClassName('.ant-select-input');
const paste = createEvent.paste(input, {
clipboardData: {
getData: () => 'a,b,b,b,c,d,d',
@@ -760,7 +772,7 @@ test('Renders only an overflow tag if dropdown is open in oneLine mode', async (
);
await open();
const withinSelector = within(getElementByClassName('.ant-select-selector'));
const withinSelector = within(getElementByClassName('.ant-select-content'));
await waitFor(() => {
expect(
withinSelector.queryByText(OPTIONS[0].label),
@@ -793,7 +805,7 @@ test('Maintains stable maxTagCount to prevent click target disappearing in oneLi
/>,
);
const withinSelector = within(getElementByClassName('.ant-select-selector'));
const withinSelector = within(getElementByClassName('.ant-select-content'));
expect(withinSelector.getByText(OPTIONS[0].label)).toBeVisible();
expect(withinSelector.getByText('+ 2 ...')).toBeVisible();
@@ -830,9 +842,7 @@ test('dropdown width matches input width after tags collapse in oneLine mode', a
// Wait for RAF to complete and tags to collapse
await waitFor(() => {
const withinSelector = within(
getElementByClassName('.ant-select-selector'),
);
const withinSelector = within(getElementByClassName('.ant-select-content'));
expect(
withinSelector.queryByText(OPTIONS[0].label),
).not.toBeInTheDocument();
@@ -1068,7 +1078,7 @@ test('fires onChange when pasting a selection', async () => {
const onChange = jest.fn();
render(<Select {...defaultProps} onChange={onChange} />);
await open();
const input = getElementByClassName('.ant-select-selection-search-input');
const input = getElementByClassName('.ant-select-input');
const paste = createEvent.paste(input, {
clipboardData: {
getData: () => OPTIONS[0].label,
@@ -1096,7 +1106,7 @@ test('does not duplicate options when using numeric values', async () => {
test('pasting an existing option does not duplicate it', async () => {
render(<Select {...defaultProps} options={[OPTIONS[0]]} />);
await open();
const input = getElementByClassName('.ant-select-selection-search-input');
const input = getElementByClassName('.ant-select-input');
const paste = createEvent.paste(input, {
clipboardData: {
getData: () => OPTIONS[0].label,
@@ -1122,7 +1132,7 @@ test('pasting an existing option does not duplicate it in multiple mode', async
/>,
);
await open();
const input = getElementByClassName('.ant-select-selection-search-input');
const input = getElementByClassName('.ant-select-input');
const paste = createEvent.paste(input, {
clipboardData: {
getData: () => 'John,Liam,Peter',
@@ -1136,7 +1146,7 @@ test('pasting an existing option does not duplicate it in multiple mode', async
test('pasting an non-existent option should not add it if allowNewOptions is false', async () => {
render(<Select {...defaultProps} options={[]} allowNewOptions={false} />);
await open();
const input = getElementByClassName('.ant-select-selection-search-input');
const input = getElementByClassName('.ant-select-input');
const paste = createEvent.paste(input, {
clipboardData: {
getData: () => 'John',
@@ -1162,7 +1172,7 @@ test('keeps pasted values outside loaded options when allowNewOptionsOnPaste is
onChange={onChange}
/>,
);
const input = getElementByClassName('.ant-select-selection-search-input');
const input = getElementByClassName('.ant-select-input');
const paste = createEvent.paste(input, {
clipboardData: {
// Liam is a loaded option; OutsideValue is not in the loaded page.
@@ -1198,7 +1208,7 @@ test('trims whitespace around pasted comma-separated values', async () => {
onChange={onChange}
/>,
);
const input = getElementByClassName('.ant-select-selection-search-input');
const input = getElementByClassName('.ant-select-input');
const paste = createEvent.paste(input, {
clipboardData: {
// Note the space after the comma — it must not leak into the value.
@@ -1231,7 +1241,7 @@ test('does not create an empty option when pasting blank text', async () => {
onChange={onChange}
/>,
);
const input = getElementByClassName('.ant-select-selection-search-input');
const input = getElementByClassName('.ant-select-input');
const paste = createEvent.paste(input, {
clipboardData: {
getData: () => ' ',
@@ -1252,7 +1262,7 @@ test('does not create an empty option when pasting blank text', async () => {
test('drops pasted values outside loaded options when allowNewOptionsOnPaste is false', async () => {
render(<Select {...defaultProps} mode="multiple" allowNewOptions={false} />);
const input = getElementByClassName('.ant-select-selection-search-input');
const input = getElementByClassName('.ant-select-input');
const paste = createEvent.paste(input, {
clipboardData: {
getData: () => 'Liam,OutsideValue',

View File

@@ -64,7 +64,6 @@ import {
} from './styles';
import {
DEFAULT_SORT_COMPARATOR,
DROPDOWN_ALIGN_BOTTOM,
EMPTY_OPTIONS,
MAX_TAG_COUNT,
TOKEN_SEPARATORS,
@@ -128,7 +127,9 @@ const Select = forwardRef(
ref: Ref<RefSelectProps>,
) => {
const isSingleMode = mode === 'single';
const shouldShowSearch = allowNewOptions ? true : showSearch;
// antd v6 widened `showSearch` to `boolean | SearchConfig`; coerce to a
// plain boolean for Superset's internal toggles and helpers.
const shouldShowSearch = allowNewOptions ? true : Boolean(showSearch);
const [selectValue, setSelectValue] = useState(value);
const [inputValue, setInputValue] = useState('');
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
@@ -697,7 +698,14 @@ const Select = forwardRef(
setSelectValue(value);
}
} else {
const token = tokenSeparators.find(token => pastedText.includes(token));
// antd v6 widened `tokenSeparators` to `string[] | (input => string[])`;
// Superset always uses the array form.
const separators = Array.isArray(tokenSeparators)
? tokenSeparators
: [];
const token = separators.find((token: string) =>
pastedText.includes(token),
);
const array = token
? uniq(
pastedText
@@ -818,7 +826,6 @@ const Select = forwardRef(
oneLine={oneLine}
popupMatchSelectWidth={oneLine ? dropdownWidth : true}
css={props.css}
dropdownAlign={DROPDOWN_ALIGN_BOTTOM}
{...props}
showSearch={shouldShowSearch}
ref={ref}

View File

@@ -19,7 +19,7 @@
import { LabeledValue as AntdLabeledValue } from 'antd/es/select';
import { t } from '@apache-superset/core/translation';
import { rankedSearchCompare } from '../../utils/rankedSearchCompare';
import { RawValue, SelectProps } from './types';
import { RawValue } from './types';
export const MAX_TAG_COUNT = 4;
@@ -33,12 +33,6 @@ export const SELECT_ALL_VALUE: RawValue = t('Select All');
export const VIRTUAL_THRESHOLD = 20;
export const DROPDOWN_ALIGN_BOTTOM: SelectProps['dropdownAlign'] = {
points: ['tl', 'bl'],
offset: [0, 4],
overflow: { adjustX: 0, adjustY: 1 },
};
export const SELECT_ALL_OPTION = {
value: SELECT_ALL_VALUE,
label: String(SELECT_ALL_VALUE),

View File

@@ -71,7 +71,6 @@ export type AntdExposedProps = Pick<
| 'virtual'
| 'getPopupContainer'
| 'menuItemSelectedIcon'
| 'dropdownAlign'
>;
export type SelectOptionsType = Exclude<AntdProps['options'], undefined>;

View File

@@ -99,9 +99,12 @@ test('Body should be visible', () => {
test('Body content should be blurred loading', () => {
render(<TableCollection {...defaultProps} loading />);
expect(screen.getByTestId('listview-table').parentNode).toHaveClass(
'ant-spin-blur',
);
// antd v6 removed the `ant-spin-blur` class. The body content is now dimmed
// via CSS applied to `.ant-spin-container` while its wrapping `.ant-spin`
// carries the `.ant-spin-spinning` class.
const container = screen.getByTestId('listview-table').parentNode;
expect(container).toHaveClass('ant-spin-container');
expect(container?.parentNode).toHaveClass('ant-spin-spinning');
});
test('Should the loading-indicator be visible during loading', () => {

View File

@@ -48,7 +48,7 @@ describe('Tabs', () => {
expect(getByText('Tab 3')).toBeInTheDocument();
const activeTabContent = container.querySelector(
'.ant-tabs-tabpane-active',
'.ant-tabs-content-active',
);
expect(activeTabContent).toBeDefined();
@@ -61,7 +61,7 @@ describe('Tabs', () => {
const { container } = render(<Tabs items={defaultItems} />);
const tabsElement = container.querySelector('.ant-tabs');
const tabsNav = container.querySelector('.ant-tabs-nav');
const tabsContent = container.querySelector('.ant-tabs-content-holder');
const tabsContent = container.querySelector('.ant-tabs-body-holder');
expect(tabsElement).toBeDefined();
expect(tabsNav).toBeDefined();
@@ -213,7 +213,7 @@ describe('Tabs', () => {
expect(getByText('Legacy Tab 2')).toBeInTheDocument();
const activeTabContent = container.querySelector(
'.ant-tabs-tabpane-active [data-testid="legacy-content-1"]',
'.ant-tabs-content-active [data-testid="legacy-content-1"]',
);
expect(activeTabContent).toBeDefined();
@@ -311,9 +311,9 @@ test('fullHeight prop renders component hierarchy correctly', () => {
const { container } = render(<Tabs items={defaultItems} fullHeight />);
const tabsElement = container.querySelector('.ant-tabs');
const contentHolder = container.querySelector('.ant-tabs-content-holder');
const content = container.querySelector('.ant-tabs-content');
const tabPane = container.querySelector('.ant-tabs-tabpane');
const contentHolder = container.querySelector('.ant-tabs-body-holder');
const content = container.querySelector('.ant-tabs-body');
const tabPane = container.querySelector('.ant-tabs-content');
expect(tabsElement).toBeInTheDocument();
expect(contentHolder).toBeInTheDocument();
@@ -343,9 +343,7 @@ test('fullHeight prop maintains structure when content updates', () => {
rerender(<Tabs items={newItems} fullHeight />);
const updatedTabsElement = container.querySelector('.ant-tabs');
const updatedContentHolder = container.querySelector(
'.ant-tabs-content-holder',
);
const updatedContentHolder = container.querySelector('.ant-tabs-body-holder');
expect(updatedTabsElement).toBeInTheDocument();
expect(updatedContentHolder).toBeInTheDocument();
@@ -359,7 +357,7 @@ test('fullHeight prop works with allowOverflow to handle tall content', () => {
const tabsElement = container.querySelector('.ant-tabs') as HTMLElement;
const contentHolder = container.querySelector(
'.ant-tabs-content-holder',
'.ant-tabs-body-holder',
) as HTMLElement;
expect(tabsElement).toBeInTheDocument();

View File

@@ -78,7 +78,7 @@ InteractiveTooltip.argTypes = {
control: { type: 'color' },
description: 'Custom background color for the tooltip.',
},
onVisibleChange: { action: 'onVisibleChange' },
onOpenChange: { action: 'onOpenChange' },
};
InteractiveTooltip.parameters = {

View File

@@ -27,7 +27,8 @@ export const Tooltip = forwardRef<TooltipRef, TooltipProps>(
<AntdTooltip
ref={ref}
styles={{
body: { overflow: 'hidden', textOverflow: 'ellipsis' },
// antd v6 renamed the inner content slot `body` -> `container`
container: { overflow: 'hidden', textOverflow: 'ellipsis' },
root: overlayStyle ?? {},
}}
{...props}

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { render, screen, userEvent, waitFor } from '@superset-ui/core/spec';
import { render, screen, userEvent } from '@superset-ui/core/spec';
import TooltipParagraph from '.';
test('starts hidden with default props', () => {
@@ -42,30 +42,11 @@ test('not render on hover when not truncated', async () => {
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
});
test('render on hover when truncated', async () => {
render(
<div style={{ width: '200px' }}>
<TooltipParagraph>
<span data-test="test-text">This is too long and should truncate.</span>
</TooltipParagraph>
</div>,
);
// Get the div with the ellipsis class to verify it's truncated
const ellipsisElement = screen
.getByTestId('test-text')
.closest('.ant-typography-ellipsis');
expect(ellipsisElement).toBeInTheDocument();
// Hover over the text
await userEvent.hover(screen.getByTestId('test-text'));
// In Ant Design v5, we can check if the aria-describedby attribute is present
// which indicates the tooltip functionality is active
await waitFor(() => {
const element = screen
.getByTestId('test-text')
.closest('[aria-describedby]');
expect(element).toHaveAttribute('aria-describedby');
});
});
// NOTE: there is intentionally no "renders tooltip when truncated" test here.
// The tooltip only activates once antd's Typography reports the text as
// truncated via its `onEllipsis` callback, which depends on real layout
// measurement (offset/scroll widths, ResizeObserver). jsdom performs no layout,
// so truncation is never detected and the tooltip never opens. Ant Design v5 set
// `aria-describedby` on the trigger regardless, which let this case be asserted;
// v6 only wires it once the tooltip has content, so the truncated branch is no
// longer observable in jsdom. It is covered by visual / end-to-end testing.

View File

@@ -26,7 +26,7 @@ import { TIMEOUT } from '../../utils/constants';
const SELECT_SELECTORS = {
DROPDOWN: '.ant-select-dropdown',
OPTION: '.ant-select-item-option',
SEARCH_INPUT: '.ant-select-selection-search-input',
SEARCH_INPUT: '.ant-select-input',
CLEAR: '.ant-select-clear',
} as const;

View File

@@ -41,7 +41,14 @@ export class ChartCreationPage {
* Gets the dataset selector container (includes the displayed selection value)
*/
getDatasetSelectContainer(): Locator {
return this.page.getByLabel('Dataset', { exact: false }).first();
// antd v6 moved the `aria-label` onto the combobox <input> itself, so
// getByLabel('Dataset') resolves to the empty input (no text content). The
// selected value is rendered in the `.ant-select-content-has-value` element
// (which also wraps that input), so scope to it to read the displayed value.
return this.page
.locator('.ant-select-content-has-value')
.filter({ has: this.page.getByLabel('Dataset', { exact: false }) })
.first();
}
/**

View File

@@ -99,7 +99,7 @@ export default styled.div`
background-color: ${theme.colorBgLayout};
}
.dt-select-page-size .ant-select .ant-select-arrow {
.dt-select-page-size .ant-select .ant-select-suffix {
color: ${theme.colorTextQuaternary};
z-index: 11;
}

View File

@@ -1974,7 +1974,7 @@ describe('plugin-chart-table', () => {
);
const arrow = container.querySelector(
'.dt-select-page-size .ant-select .ant-select-arrow',
'.dt-select-page-size .ant-select .ant-select-suffix',
);
expect(arrow).not.toBeNull();
expect(getComputedStyle(arrow as HTMLElement).zIndex).toBe('11');

View File

@@ -65,11 +65,57 @@ export default class FixJSDOMEnvironment extends JSDOMEnvironment {
this.global.AbortController = AbortController;
this.global.ReadableStream = ReadableStream;
// Mock MessageChannel to prevent hanging Jest tests with rc-overflow@1.4.1
// Forces rc-overflow to use requestAnimationFrame fallback instead
// Can be removed when rc-overflow properly cleans up MessagePorts in test environments
// See: https://github.com/apache/superset/pull/34871
this.global.MessageChannel = undefined as any;
this.global.MessagePort = undefined as any;
// Ant Design v6's scheduler instantiates `new MessageChannel()` directly, so
// the previous `undefined` stub (a workaround for an rc-overflow@1.4.1
// MessagePort leak, see https://github.com/apache/superset/pull/34871) now
// throws "MessageChannel is not a constructor". jsdom's native MessageChannel
// delivers asynchronously and does not flush inside testing-library's act(),
// so we provide a lightweight polyfill that delivers messages as macrotasks
// (matching React's own setTimeout scheduler fallback). setTimeout(0) does
// not keep the event loop alive, so the original hang does not return.
class PolyfillMessagePort {
onmessage: ((event: { data: unknown }) => void) | null = null;
private listeners: Array<(event: { data: unknown }) => void> = [];
_peer: PolyfillMessagePort | null = null;
postMessage(data: unknown) {
const peer = this._peer;
if (!peer) return;
setTimeout(() => {
peer.onmessage?.({ data });
peer.listeners.forEach(fn => fn({ data }));
}, 0);
}
addEventListener(type: string, fn: (event: { data: unknown }) => void) {
if (type === 'message') this.listeners.push(fn);
}
removeEventListener(
type: string,
fn: (event: { data: unknown }) => void,
) {
if (type === 'message')
this.listeners = this.listeners.filter(l => l !== fn);
}
start() {}
close() {}
}
class PolyfillMessageChannel {
port1 = new PolyfillMessagePort();
port2 = new PolyfillMessagePort();
constructor() {
this.port1._peer = this.port2;
this.port2._peer = this.port1;
}
}
this.global.MessageChannel = PolyfillMessageChannel as any;
this.global.MessagePort = PolyfillMessagePort as any;
}
}

View File

@@ -29,7 +29,7 @@ const StyledTooltip = (props: any) => {
{({ css }) => (
<Tooltip
overlayClassName={css`
.ant-tooltip-inner {
.ant-tooltip-container {
max-width: ${theme.sizeUnit * 125}px;
word-wrap: break-word;
text-align: center;

View File

@@ -165,7 +165,7 @@ test('should have an empty state when query editors is empty', async () => {
});
// Clear the new tab applied in componentDidMount and check the state of the empty tab
const removeTabButton = getByRole('tab', { name: 'remove' });
const removeTabButton = getByRole('button', { name: 'remove' });
fireEvent.click(removeTabButton);
await waitFor(() =>

View File

@@ -330,7 +330,8 @@ export const DrillBySubmenu = ({
root: {
paddingLeft: 0,
},
body: {
// antd v6 renamed the inner content slot `body` -> `container`
container: {
padding: theme.sizeUnit * 2,
boxShadow: theme.boxShadow,
borderRadius: theme.borderRadius,

View File

@@ -304,11 +304,11 @@ test('should offer the full set of page-size options', async () => {
await waitForRender();
// The page-size changer renders as an antd Select. In jsdom, antd opens
// its overlay on mouseDown of the .ant-select-selector element rather
// its overlay on mouseDown of the .ant-select-content element rather
// than via a click on the inner combobox input.
const selector = await waitFor(() => {
const el = document.querySelector(
'.ant-pagination-options-size-changer .ant-select-selector',
'.ant-pagination-options-size-changer .ant-select-content',
) as HTMLElement | null;
expect(el).toBeTruthy();
return el!;

View File

@@ -351,7 +351,6 @@ const DatasetUsageTab = ({
pageSize: PAGE_SIZE,
onChange: handlePageChange,
showSizeChanger: false,
size: 'default',
}
}
loading={loading}

View File

@@ -409,7 +409,7 @@ test('shows the default datetime column in dropdown', async () => {
// Verify the current value is 'ds' (from main_dttm_col in props)
const selectedValue = await screen.findByText('ds', {
selector: '.ant-select-selection-item',
selector: '.ant-select-content-has-value, .ant-select-selection-item',
});
expect(selectedValue).toBeInTheDocument();
});

View File

@@ -573,7 +573,6 @@ export function ListView<T extends object = any>({
onChange={(page: number) => {
gotoPage(page - 1);
}}
size="default"
showSizeChanger={false}
showQuickJumper={false}
hideOnSinglePage

View File

@@ -60,7 +60,7 @@ const tablesApiRoute = 'glob:*/api/v1/database/*/tables/*';
const getSelectItemContainer = (select: HTMLElement) =>
select.parentElement?.parentElement?.getElementsByClassName(
'ant-select-selection-item',
'ant-select-content-has-value',
);
// Add cleanup and increase timeout

View File

@@ -78,8 +78,13 @@ test('does not render the tooltip with anchors', async () => {
/>,
);
userEvent.hover(screen.getByRole('link', { name: 'datasource-name' }));
expect(await screen.findByRole('tooltip')).toBeInTheDocument();
const tooltip = await screen.findByRole('tooltip');
// The useState mock forces every TruncatedTextWithTooltip to render its
// tooltip, so multiple role="tooltip" nodes exist. Target the datasource
// tooltip specifically by its accessible name.
const tooltip = await screen.findByRole('tooltip', {
name: 'datasource-name',
});
expect(tooltip).toBeInTheDocument();
expect(within(tooltip).queryByRole('link')).not.toBeInTheDocument();
mock.mockRestore();
});

View File

@@ -275,18 +275,21 @@ export const CustomizationsBadge = ({ chartId }: CustomizationsBadgeProps) => {
return (
<Tooltip
title={tooltipContent}
visible={tooltipVisible}
onVisibleChange={setTooltipVisible}
open={tooltipVisible}
onOpenChange={setTooltipVisible}
placement="bottom"
overlayStyle={{
color: theme.colorText,
backgroundColor: theme.colorBgContainer,
border: `1px solid ${theme.colorBorder}`,
boxShadow: theme.boxShadow,
}}
overlayInnerStyle={{
color: theme.colorText,
backgroundColor: theme.colorBgContainer,
// antd v6: overlayStyle/overlayInnerStyle -> styles.root/styles.container
styles={{
root: {
color: theme.colorText,
backgroundColor: theme.colorBgContainer,
border: `1px solid ${theme.colorBorder}`,
boxShadow: theme.boxShadow,
},
container: {
color: theme.colorText,
backgroundColor: theme.colorBgContainer,
},
}}
>
<StyledTag

View File

@@ -135,7 +135,7 @@ test('Should render editMode:true', () => {
.getAllByRole('tab')
.filter(tab => !tab.classList.contains('ant-tabs-tab-remove')),
).toHaveLength(3);
expect(screen.getAllByRole('tab', { name: 'remove' })).toHaveLength(3);
expect(screen.getAllByRole('button', { name: 'remove' })).toHaveLength(3);
expect(screen.getAllByRole('button', { name: 'Add tab' })).toHaveLength(1);
expect(DashboardComponent).toHaveBeenCalledTimes(4);
expect(DeleteComponentButton).toHaveBeenCalledTimes(1);
@@ -240,7 +240,7 @@ test('Removing a tab', async () => {
expect(props.deleteComponent).not.toHaveBeenCalled();
expect(screen.queryByText('Delete dashboard tab?')).not.toBeInTheDocument();
userEvent.click(screen.getAllByRole('tab', { name: 'remove' })[0]);
userEvent.click(screen.getAllByRole('button', { name: 'remove' })[0]);
expect(props.deleteComponent).not.toHaveBeenCalled();
expect(await screen.findByText('Delete dashboard tab?')).toBeInTheDocument();

View File

@@ -64,7 +64,7 @@ test('Column and value should be visible', () => {
test('Tag should be closable', () => {
setup(mockedProps);
expect(screen.getByRole('img', { name: 'Close' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
});
test('Divider should not be visible', () => {

View File

@@ -81,7 +81,7 @@ test('renders nothing when there is no f= param', () => {
test('removing a chip dispatches updateDataMask with the remaining filters', async () => {
renderAt('?f=(region:EMEA,channel:web)');
const closeButtons = screen.getAllByRole('img', { name: /close/i });
const closeButtons = screen.getAllByRole('button', { name: /close/i });
expect(closeButtons).toHaveLength(2);
await userEvent.click(closeButtons[0]);
@@ -105,7 +105,7 @@ test('removing a chip dispatches updateDataMask with the remaining filters', asy
test('removing the last chip dispatches removeDataMask, not an empty update', async () => {
renderAt('?f=(region:EMEA)');
await userEvent.click(screen.getByRole('img', { name: /close/i }));
await userEvent.click(screen.getByRole('button', { name: /close/i }));
const removeCalls = mockDispatch.mock.calls
.map(([action]) => action)

View File

@@ -180,7 +180,9 @@ test('calls onChange when a dataset is selected', async () => {
await waitFor(() => {
expect(mockOnChange).toHaveBeenCalled();
const callArg = mockOnChange.mock.calls[0][0];
expect(callArg).toEqual({ key: 1, label: expect.anything(), value: 1 });
// antd v6 no longer includes the internal `key` field in the
// labeledValue passed to onChange; assert on the public value/label.
expect(callArg).toEqual({ label: expect.anything(), value: 1 });
});
});

View File

@@ -1040,7 +1040,7 @@ test('shows info tooltips beside value-filter options and reveals tooltip text o
// query the portal node by class and require non-empty text content so an empty
// tooltip render does not pass.
await waitFor(() => {
const tooltip = document.querySelector('.ant-tooltip-inner');
const tooltip = document.querySelector('.ant-tooltip-container');
expect(tooltip).toBeInTheDocument();
expect(tooltip?.textContent?.trim()).toBeTruthy();
});
@@ -1060,7 +1060,9 @@ test('numerical range filter — Range Type selector lets the user pick a displa
// ensures the post-click assertion proves a state change rather than passing on
// the default selection.
expect(
document.querySelector('.ant-select-selection-item[title="Slider"]'),
document.querySelector(
'.ant-select-content-has-value[title="Slider"], .ant-select-selection-item[title="Slider"]',
),
).not.toBeInTheDocument();
await userEvent.click(rangeTypeCombobox);
@@ -1073,7 +1075,9 @@ test('numerical range filter — Range Type selector lets the user pick a displa
// the picked option's label.
await waitFor(() => {
expect(
document.querySelector('.ant-select-selection-item[title="Slider"]'),
document.querySelector(
'.ant-select-content-has-value[title="Slider"], .ant-select-selection-item[title="Slider"]',
),
).toBeInTheDocument();
});
}, 30000);

View File

@@ -20,7 +20,8 @@ import { FC } from 'react';
import { t } from '@apache-superset/core/translation';
import { NativeFilterType, ChartCustomizationType } from '@superset-ui/core';
import { useTheme } from '@apache-superset/core/theme';
import { Button, Dropdown, Menu } from '@superset-ui/core/components';
import { Button, Dropdown } from '@superset-ui/core/components';
import type { MenuProps } from '@superset-ui/core/components/Menu';
import { Icons } from '@superset-ui/core/components/Icons';
interface Props {
@@ -31,51 +32,46 @@ interface Props {
const NewItemDropdown: FC<Props> = ({ onAddFilter, onAddCustomization }) => {
const theme = useTheme();
const menu = (
<Menu
onClick={({ key }) => {
if (key === 'filter') {
onAddFilter(NativeFilterType.NativeFilter);
} else if (key === 'customization') {
onAddCustomization(ChartCustomizationType.ChartCustomization);
} else if (key === 'divider') {
onAddFilter(NativeFilterType.Divider);
}
}}
items={[
{
key: 'filter',
label: t('Add filter'),
icon: (
<Icons.FilterOutlined iconColor={theme.colorPrimary} iconSize="m" />
),
},
{
key: 'customization',
label: t('Add display control'),
icon: (
<Icons.SettingOutlined
iconColor={theme.colorPrimary}
iconSize="m"
/>
),
},
{
key: 'divider',
label: t('Add divider'),
icon: (
<Icons.PicCenterOutlined
iconColor={theme.colorPrimary}
iconSize="m"
/>
),
},
]}
/>
);
const menu: MenuProps = {
onClick: ({ key }) => {
if (key === 'filter') {
onAddFilter(NativeFilterType.NativeFilter);
} else if (key === 'customization') {
onAddCustomization(ChartCustomizationType.ChartCustomization);
} else if (key === 'divider') {
onAddFilter(NativeFilterType.Divider);
}
},
items: [
{
key: 'filter',
label: t('Add filter'),
icon: (
<Icons.FilterOutlined iconColor={theme.colorPrimary} iconSize="m" />
),
},
{
key: 'customization',
label: t('Add display control'),
icon: (
<Icons.SettingOutlined iconColor={theme.colorPrimary} iconSize="m" />
),
},
{
key: 'divider',
label: t('Add divider'),
icon: (
<Icons.PicCenterOutlined
iconColor={theme.colorPrimary}
iconSize="m"
/>
),
},
],
};
return (
<Dropdown overlay={menu} trigger={['hover']}>
<Dropdown menu={menu} trigger={['hover']}>
<Button
buttonSize="default"
buttonStyle="secondary"

View File

@@ -71,8 +71,8 @@ test('renders FormattingPopoverContent component', () => {
// Assert that the component renders correctly
expect(screen.getByLabelText('Column')).toBeInTheDocument();
expect(screen.getAllByLabelText('Color scheme')).toHaveLength(2);
expect(screen.getAllByLabelText('Operator')).toHaveLength(2);
expect(screen.getByLabelText('Color scheme')).toBeInTheDocument();
expect(screen.getByLabelText('Operator')).toBeInTheDocument();
expect(screen.queryByLabelText('Left value')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Right value')).not.toBeInTheDocument();
expect(screen.getByText('Apply')).toBeInTheDocument();

View File

@@ -116,7 +116,7 @@ const StyledTooltip = (props: any) => {
max-height: 410px;
overflow-y: scroll;
.ant-tooltip-inner {
.ant-tooltip-container {
max-width: ${theme.sizeUnit * 125}px;
h3 {
font-size: ${theme.fontSize}px;

View File

@@ -61,7 +61,7 @@ test('renders with default props', async () => {
expect(screen.getByRole('spinbutton')).toBeInTheDocument();
expect(screen.getByText('Days Before')).toBeInTheDocument();
expect(screen.getByText('Specific Date/Time')).toBeInTheDocument();
expect(screen.getByRole('img', { name: 'calendar' })).toBeInTheDocument();
expect(screen.getByLabelText('calendar')).toBeInTheDocument();
});
test('renders with empty store', () => {
@@ -73,7 +73,7 @@ test('renders with empty store', () => {
expect(screen.getByRole('spinbutton')).toBeInTheDocument();
expect(screen.getByText('Days Before')).toBeInTheDocument();
expect(screen.getByText('Specific Date/Time')).toBeInTheDocument();
expect(screen.getByRole('img', { name: 'calendar' })).toBeInTheDocument();
expect(screen.getByLabelText('calendar')).toBeInTheDocument();
});
test('renders since and until with specific date/time with default locale', () => {
@@ -81,7 +81,7 @@ test('renders since and until with specific date/time with default locale', () =
store: emptyStore,
});
expect(screen.getAllByText('Specific Date/Time').length).toBe(2);
expect(screen.getAllByRole('img', { name: 'calendar' }).length).toBe(2);
expect(screen.getAllByLabelText('calendar').length).toBe(2);
});
test('renders with invalid locale', () => {
@@ -93,7 +93,7 @@ test('renders with invalid locale', () => {
expect(screen.getByRole('spinbutton')).toBeInTheDocument();
expect(screen.getByText('Days Before')).toBeInTheDocument();
expect(screen.getByText('Specific Date/Time')).toBeInTheDocument();
expect(screen.getByRole('img', { name: 'calendar' })).toBeInTheDocument();
expect(screen.getByLabelText('calendar')).toBeInTheDocument();
});
test('renders since and until with specific date/time with invalid locale', () => {
@@ -101,7 +101,7 @@ test('renders since and until with specific date/time with invalid locale', () =
store: invalidStore,
});
expect(screen.getAllByText('Specific Date/Time').length).toBe(2);
expect(screen.getAllByRole('img', { name: 'calendar' }).length).toBe(2);
expect(screen.getAllByLabelText('calendar').length).toBe(2);
});
test('renders since and until with specific date/time', async () => {
@@ -110,7 +110,7 @@ test('renders since and until with specific date/time', async () => {
});
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
expect(screen.getAllByText('Specific Date/Time').length).toBe(2);
expect(screen.getAllByRole('img', { name: 'calendar' }).length).toBe(2);
expect(screen.getAllByLabelText('calendar').length).toBe(2);
});
test('renders since and until with relative date/time', async () => {
@@ -237,7 +237,7 @@ test('should translate Date Picker', async () => {
store,
});
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
userEvent.click(screen.getAllByRole('img', { name: 'calendar' })[0]);
userEvent.click(screen.getAllByLabelText('calendar')[0]);
expect(screen.getByText('2021')).toBeInTheDocument();
expect(screen.getByText('lu')).toBeInTheDocument();
@@ -265,7 +265,7 @@ test('calls onChange when START Specific Date/Time is selected', async () => {
const specificDateTimeOptions = screen.getAllByText('Specific Date/Time');
expect(specificDateTimeOptions.length).toBe(2);
const calendarIcons = screen.getAllByRole('img', { name: 'calendar' });
const calendarIcons = screen.getAllByLabelText('calendar');
userEvent.click(calendarIcons[0]);
const randomDate = screen.getByTitle('2021-03-11');
@@ -293,7 +293,7 @@ test('calls onChange when END Specific Date/Time is selected', async () => {
const specificDateTimeOptions = screen.getAllByText('Specific Date/Time');
expect(specificDateTimeOptions.length).toBe(2);
const calendarIcons = screen.getAllByRole('img', { name: 'calendar' });
const calendarIcons = screen.getAllByLabelText('calendar');
userEvent.click(calendarIcons[1]);
const randomDate = screen.getByTitle('2021-03-28');
@@ -329,7 +329,7 @@ test('calls onChange when a date is picked from anchor mode date picker', async
expect(dateTimeRadio).toBeChecked();
const calendarIcon = screen.getByRole('img', { name: 'calendar' });
const calendarIcon = screen.getByLabelText('calendar');
userEvent.click(calendarIcon);
const randomDate = screen.getByTitle('2024-06-05');

View File

@@ -158,8 +158,9 @@ describe('AdhocFilterEditPopover', () => {
const subjectSelect = screen.getByTestId('select-element');
await userEvent.click(subjectSelect);
// Select a value from the dropdown
const valueOption = screen.getByText('value');
// Select a value from the dropdown (scope to the listbox option, since the
// selected subject label also renders the text "value" in antd v6)
const valueOption = screen.getByRole('option', { name: 'value' });
await userEvent.click(valueOption);
// Find and update the value input

View File

@@ -725,7 +725,7 @@ test('advanced data type operator list should update after API response', async
expect(
await screen.findByText('Equal to (=)', {
selector: '.ant-select-selection-item',
selector: '.ant-select-content-has-value, .ant-select-selection-item',
}),
).toBeInTheDocument();
});

View File

@@ -57,7 +57,7 @@ import { useDefaultTimeFilter } from '../../DateFilterControl/utils';
import { Clauses, ExpressionTypes } from '../types';
const SelectWithLabel = styled(Select)<{ labelText: string }>`
.ant-select-selector::after {
.ant-select-content::after {
content: ${({ labelText }) => labelText || '\\A0'};
display: inline-block;
white-space: nowrap;

View File

@@ -90,7 +90,7 @@ test('stepper arrows trigger onChange immediately', async () => {
};
render(<NumberControl {...props} />);
const upButton = document.querySelector(
'.ant-input-number-handler-up',
'.ant-input-number-action-up',
) as HTMLElement;
expect(upButton).toBeInTheDocument();
await userEvent.click(upButton);

View File

@@ -23,7 +23,6 @@ import {
render,
screen,
userEvent,
within,
} from 'spec/helpers/testing-library';
import SelectControl, {
innerGetOptions,
@@ -84,26 +83,24 @@ describe('SelectControl', () => {
describe('render', () => {
test('renders with Select by default', () => {
renderSelectControl();
const selectorWrapper = screen.getByLabelText('Row Limit', {
selector: 'div',
const selectorInput = screen.getByLabelText('Row Limit', {
selector: 'input',
});
const selectorInput = within(selectorWrapper).getByLabelText(
'Row Limit',
{ selector: 'input' },
);
const selectorWrapper = selectorInput.closest(
'.ant-select',
) as HTMLElement;
expect(selectorWrapper).toBeInTheDocument();
expect(selectorInput).toBeInTheDocument();
});
test('renders as mode multiple', () => {
renderSelectControl({ multi: true });
const selectorWrapper = screen.getByLabelText('Row Limit', {
selector: 'div',
const selectorInput = screen.getByLabelText('Row Limit', {
selector: 'input',
});
const selectorInput = within(selectorWrapper).getByLabelText(
'Row Limit',
{ selector: 'input' },
);
const selectorWrapper = selectorInput.closest(
'.ant-select',
) as HTMLElement;
expect(selectorWrapper).toBeInTheDocument();
expect(selectorInput).toBeInTheDocument();
userEvent.click(selectorInput);
@@ -112,13 +109,12 @@ describe('SelectControl', () => {
test('renders with allowNewOptions when freeForm', () => {
renderSelectControl({ freeForm: true });
const selectorWrapper = screen.getByLabelText('Row Limit', {
selector: 'div',
const selectorInput = screen.getByLabelText('Row Limit', {
selector: 'input',
});
const selectorInput = within(selectorWrapper).getByLabelText(
'Row Limit',
{ selector: 'input' },
);
const selectorWrapper = selectorInput.closest(
'.ant-select',
) as HTMLElement;
expect(selectorWrapper).toBeInTheDocument();
expect(selectorInput).toBeInTheDocument();
@@ -126,20 +122,19 @@ describe('SelectControl', () => {
userEvent.click(selectorInput);
userEvent.type(selectorInput, 'a new option');
act(() => jest.runAllTimers());
expect(within(selectorWrapper).getByRole('option')).toHaveTextContent(
'a new option',
);
// antd v6 renders the dropdown options in a portal outside the
// .ant-select wrapper, so query the whole document.
expect(screen.getByRole('option')).toHaveTextContent('a new option');
});
test('renders with allowNewOptions=false when freeForm=false', () => {
const container = renderSelectControl({ freeForm: false });
const selectorWrapper = screen.getByLabelText('Row Limit', {
selector: 'div',
const selectorInput = screen.getByLabelText('Row Limit', {
selector: 'input',
});
const selectorInput = within(selectorWrapper).getByLabelText(
'Row Limit',
{ selector: 'input' },
);
const selectorWrapper = selectorInput.closest(
'.ant-select',
) as HTMLElement;
expect(selectorWrapper).toBeInTheDocument();
expect(selectorInput).toBeInTheDocument();
@@ -151,20 +146,21 @@ describe('SelectControl', () => {
expect(
container.querySelector('[role="option"]'),
).not.toBeInTheDocument();
// antd v6 renders the empty-state ("No data") in a portal outside the
// .ant-select wrapper, so query the whole document.
expect(
within(selectorWrapper).getByText('No data', { selector: 'div' }),
screen.getByText('No data', { selector: 'div' }),
).toBeInTheDocument();
});
test('renders with tokenSeparators', () => {
renderSelectControl({ tokenSeparators: ['\n', '\t', ';'], multi: true });
const selectorWrapper = screen.getByLabelText('Row Limit', {
selector: 'div',
const selectorInput = screen.getByLabelText('Row Limit', {
selector: 'input',
});
const selectorInput = within(selectorWrapper).getByLabelText(
'Row Limit',
{ selector: 'input' },
);
const selectorWrapper = selectorInput.closest(
'.ant-select',
) as HTMLElement;
expect(selectorWrapper).toBeInTheDocument();
expect(selectorInput).toBeInTheDocument();
@@ -414,8 +410,9 @@ describe('SelectControl', () => {
// The SelectControl should receive a sortComparator for numeric values
// This is tested by verifying the component renders without errors
// antd v6 places the aria-label on the Select's input element.
const selectorWrapper = screen.getByLabelText('Row Limit', {
selector: 'div',
selector: 'input',
});
expect(selectorWrapper).toBeInTheDocument();
});
@@ -429,8 +426,9 @@ describe('SelectControl', () => {
renderSelectControl({ options: numericOptions, choices: undefined });
// The SelectControl should receive a sortComparator for numeric values
// antd v6 places the aria-label on the Select's input element.
const selectorWrapper = screen.getByLabelText('Row Limit', {
selector: 'div',
selector: 'input',
});
expect(selectorWrapper).toBeInTheDocument();
});
@@ -444,8 +442,9 @@ describe('SelectControl', () => {
renderSelectControl({ choices: mixedChoices });
// Should render without errors and not apply numeric sorting
// antd v6 places the aria-label on the Select's input element.
const selectorWrapper = screen.getByLabelText('Row Limit', {
selector: 'div',
selector: 'input',
});
expect(selectorWrapper).toBeInTheDocument();
});
@@ -465,8 +464,9 @@ describe('SelectControl', () => {
sortComparator: explicitComparator,
});
// antd v6 places the aria-label on the Select's input element.
const selectorWrapper = screen.getByLabelText('Row Limit', {
selector: 'div',
selector: 'input',
});
expect(selectorWrapper).toBeInTheDocument();
});

View File

@@ -223,9 +223,15 @@ const tabsEndpoint = 'glob:*/api/v1/dashboard/1/tabs';
fetchMock.get(ownersEndpoint, { result: [] });
fetchMock.get(databaseEndpoint, { result: [] });
fetchMock.get(dashboardEndpoint, { result: [] });
// Named so tests can removeRoute() + re-register an override (an unnamed route
// cannot be removed by its matcher string, so the override would be ignored).
fetchMock.get(dashboardEndpoint, { result: [] }, { name: dashboardEndpoint });
fetchMock.get(chartEndpoint, { result: [{ text: 'table chart', value: 1 }] });
fetchMock.get(reportDashboardEndpoint, { result: [] });
fetchMock.get(
reportDashboardEndpoint,
{ result: [] },
{ name: reportDashboardEndpoint },
);
fetchMock.get(reportChartEndpoint, {
result: [{ text: 'table chart', value: 1 }],
});
@@ -694,7 +700,9 @@ test('open chart button opens explore with slice_id', async () => {
});
expect(openChartButton).toBeInTheDocument();
const navSpy = jest.spyOn(navigationUtils, 'navigateTo').mockImplementation(() => null);
const navSpy = jest
.spyOn(navigationUtils, 'navigateTo')
.mockImplementation(() => null);
try {
await userEvent.click(openChartButton);
expect(navSpy).toHaveBeenCalledWith(
@@ -721,7 +729,9 @@ test('open dashboard button opens dashboard url', async () => {
});
expect(openDashButton).toBeInTheDocument();
const navSpy = jest.spyOn(navigationUtils, 'navigateTo').mockImplementation(() => null);
const navSpy = jest
.spyOn(navigationUtils, 'navigateTo')
.mockImplementation(() => null);
try {
await userEvent.click(openDashButton);
expect(navSpy).toHaveBeenCalledWith(
@@ -1121,13 +1131,30 @@ test('dashboard switching resets tab and filter selections', async () => {
const dashboardSelect = screen.getByRole('combobox', {
name: /dashboard/i,
});
userEvent.clear(dashboardSelect);
userEvent.type(dashboardSelect, 'Other Dashboard{enter}');
// Open the async dashboard select, wait for the options to load, then click
// "Other Dashboard". Opening the AsyncSelect triggers an async fetch of the
// dashboard options; the option only appears once that fetch resolves.
await userEvent.click(dashboardSelect);
const otherDashboardOption = await screen.findByText(
'Other Dashboard',
{},
{ timeout: 5000 },
);
await userEvent.click(otherDashboardOption);
// Tab selector should reset: "Other Dashboard" has no tabs, so disabled with placeholder
// Tab selector should reset: "Other Dashboard" has no tabs, so the tab
// TreeSelect becomes disabled with no selected value. (In antd v6 a disabled
// select does not render its placeholder text, so assert on the
// disabled + empty-value state instead of the "Select a tab" placeholder.)
await waitFor(
() => {
expect(screen.getByText(/select a tab/i)).toBeInTheDocument();
const treeSelect = document.querySelector('.ant-tree-select');
expect(treeSelect).toHaveClass('ant-select-disabled');
expect(
treeSelect?.querySelector(
'.ant-select-content-has-value, .ant-select-selection-item',
),
).not.toBeInTheDocument();
},
{ timeout: 10000 },
);
@@ -1141,7 +1168,9 @@ test('dashboard switching resets tab and filter selections', async () => {
filterSelects.forEach(select => {
const container = select.closest('.ant-select');
expect(
container?.querySelector('.ant-select-selection-item'),
container?.querySelector(
'.ant-select-content-has-value, .ant-select-selection-item',
),
).not.toBeInTheDocument();
});
});
@@ -1649,7 +1678,9 @@ test('create mode defaults to dashboard content type with chart null', async ()
// Default content type should be "Dashboard" (not "Chart")
const selectedItem = contentTypeSelect
.closest('.ant-select')
?.querySelector('.ant-select-selection-item');
?.querySelector(
'.ant-select-content-has-value, .ant-select-selection-item',
);
expect(selectedItem).toBeInTheDocument();
expect(selectedItem?.textContent).toBe('Dashboard');
@@ -1812,7 +1843,7 @@ test('filter reappears in dropdown after clearing with X icon', async () => {
await waitFor(() => {
const selectionItem = document.querySelector(
'.ant-select-selection-item[title="Test Filter 1"]',
'.ant-select-content-has-value[title="Test Filter 1"], .ant-select-selection-item[title="Test Filter 1"]',
);
expect(selectionItem).toBeInTheDocument();
});
@@ -1834,7 +1865,7 @@ test('filter reappears in dropdown after clearing with X icon', async () => {
await waitFor(() => {
const selectionItem = document.querySelector(
'.ant-select-selection-item[title="Test Filter 1"]',
'.ant-select-content-has-value[title="Test Filter 1"], .ant-select-selection-item[title="Test Filter 1"]',
);
expect(selectionItem).not.toBeInTheDocument();
});
@@ -2353,14 +2384,14 @@ test('edit mode shows friendly filter names instead of raw IDs', async () => {
await waitFor(() => {
const selectionItem = document.querySelector(
'.ant-select-selection-item[title="Country"]',
'.ant-select-content-has-value[title="Country"], .ant-select-selection-item[title="Country"]',
);
expect(selectionItem).toBeInTheDocument();
});
expect(
document.querySelector(
'.ant-select-selection-item[title="NATIVE_FILTER-abc123"]',
'.ant-select-content-has-value[title="NATIVE_FILTER-abc123"], .ant-select-selection-item[title="NATIVE_FILTER-abc123"]',
),
).not.toBeInTheDocument();
});
@@ -2380,7 +2411,7 @@ test('edit mode falls back to raw ID when filterName is missing', async () => {
await waitFor(() => {
const selectionItem = document.querySelector(
'.ant-select-selection-item[title="NATIVE_FILTER-xyz789"]',
'.ant-select-content-has-value[title="NATIVE_FILTER-xyz789"], .ant-select-selection-item[title="NATIVE_FILTER-xyz789"]',
);
expect(selectionItem).toBeInTheDocument();
});
@@ -2488,7 +2519,7 @@ test('selecting filter triggers chart data request with correct params', async (
// Select the Country Filter using comboboxSelect pattern
await comboboxSelect(filterDropdown, 'Country Filter', () =>
document.querySelector(
'.ant-select-selection-item[title="Country Filter"]',
'.ant-select-content-has-value[title="Country Filter"], .ant-select-selection-item[title="Country Filter"]',
),
);
@@ -2539,7 +2570,7 @@ test('selected filter excluded from other row dropdowns', async () => {
// Select Country Filter in row 1
await comboboxSelect(filterDropdown, 'Country Filter', () =>
document.querySelector(
'.ant-select-selection-item[title="Country Filter"]',
'.ant-select-content-has-value[title="Country Filter"], .ant-select-selection-item[title="Country Filter"]',
),
);

View File

@@ -85,7 +85,7 @@ test('shows a loading state when config.refreshingSchema is true', () => {
const { container } = render(
<MultiEnumControl {...baseProps({ config: { refreshingSchema: true } })} />,
);
expect(container.querySelector('.ant-select-arrow-loading')).toBeTruthy();
expect(container.querySelector('.ant-select-suffix-loading')).toBeTruthy();
});
test('treats non-array data as an empty selection without crashing', () => {

View File

@@ -407,11 +407,6 @@ export default function SemanticLayerModal({
label: type.name,
}))}
getPopupContainer={() => document.body}
dropdownAlign={{
points: ['tl', 'bl'],
offset: [0, 4],
overflow: { adjustX: 0, adjustY: 1 },
}}
/>
</ModalFormField>
) : (

View File

@@ -65,8 +65,8 @@ export default function TaskPayloadPopover({
content={content}
trigger="hover"
placement="leftTop"
visible={visible}
onVisibleChange={setVisible}
open={visible}
onOpenChange={setVisible}
>
<InfoIconWrapper>
<Icons.InfoCircleOutlined iconSize="l" />

View File

@@ -126,8 +126,8 @@ export default function TaskStackTracePopover({
content={content}
trigger="hover"
placement="leftTop"
visible={visible}
onVisibleChange={setVisible}
open={visible}
onOpenChange={setVisible}
>
<ErrorIconWrapper>
<Icons.BugOutlined iconSize="l" />

View File

@@ -322,47 +322,51 @@ export const ChartCreation = ({
return (
<StyledContainer>
<h3>{t('Create a new chart')}</h3>
<Steps direction="vertical" size="small">
<Steps.Step
title={
<StyledStepTitle>
{t('Choose a %s', datasetLabelLower())}
</StyledStepTitle>
}
status={datasource?.value ? 'finish' : 'process'}
description={
<StyledStepDescription className="dataset">
<AsyncSelect
autoFocus
ariaLabel={datasetLabel()}
name="select-datasource"
onChange={changeDatasource}
options={loadDatasources}
optionFilterProps={['id', 'table_name']}
placeholder={t('Choose a %s', datasetLabelLower())}
showSearch
value={datasource}
/>
{datasetHelpText}
</StyledStepDescription>
}
/>
<Steps.Step
title={<StyledStepTitle>{t('Choose chart type')}</StyledStepTitle>}
status={vizType ? 'finish' : 'process'}
description={
<StyledStepDescription>
<VizTypeGallery
denyList={denyList}
className="viz-gallery"
onChange={changeVizType}
onDoubleClick={onVizTypeDoubleClick}
selectedViz={vizType}
/>
</StyledStepDescription>
}
/>
</Steps>
<Steps
direction="vertical"
size="small"
items={[
{
title: (
<StyledStepTitle>
{t('Choose a %s', datasetLabelLower())}
</StyledStepTitle>
),
status: datasource?.value ? 'finish' : 'process',
description: (
<StyledStepDescription className="dataset">
<AsyncSelect
autoFocus
ariaLabel={datasetLabel()}
name="select-datasource"
onChange={changeDatasource}
options={loadDatasources}
optionFilterProps={['id', 'table_name']}
placeholder={t('Choose a %s', datasetLabelLower())}
showSearch
value={datasource}
/>
{datasetHelpText}
</StyledStepDescription>
),
},
{
title: <StyledStepTitle>{t('Choose chart type')}</StyledStepTitle>,
status: vizType ? 'finish' : 'process',
description: (
<StyledStepDescription>
<VizTypeGallery
denyList={denyList}
className="viz-gallery"
onChange={changeVizType}
onDoubleClick={onVizTypeDoubleClick}
selectedViz={vizType}
/>
</StyledStepDescription>
),
},
]}
/>
<div className="footer">
{isButtonDisabled && (
<span>

View File

@@ -228,7 +228,7 @@ test('sorts table when clicking column headers', async () => {
const allHeaders = table.querySelectorAll('.ant-table-column-sorters');
const sortableHeaders = Array.from(allHeaders).filter(
header => !header.closest('.ant-table-measure-cell-content'),
header => !header.closest('.ant-table-measure-row'),
);
expect(sortableHeaders).toHaveLength(3);

View File

@@ -91,7 +91,7 @@ test('should render table headers', () => {
);
const visibleTimeSeriesHeaders = allTimeSeriesHeaders.filter(
el => !el.closest('.ant-table-measure-cell-content'),
el => !el.closest('.ant-table-measure-row'),
);
expect(visibleTimeSeriesHeaders.length).toBe(1);