This is an automated email from the ASF dual-hosted git repository. sadpandajoe pushed a commit to branch test/dashboard-load-modes in repository https://gitbox.apache.org/repos/asf/superset.git
commit 4586cd8ac0eaa94059f01eeae33f3e907a0ba649 Author: Joe Li <[email protected]> AuthorDate: Fri Sep 25 15:38:10 2026 -0700 test(dashboard): cover dashboard load modes and URL param hydration - ?edit=true hydrates edit mode only for users who can edit - regular URL params merge into each chart's form_data.url_params, overriding same-key params and excluding reserved params - filter bar orientation is read from dashboard metadata, with a vertical fallback - the dashboard mount log event fires on mount - the app menu is hidden in standalone mode Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .../src/dashboard/actions/hydrate.test.ts | 137 ++++++++++++++++++++- .../src/dashboard/components/Dashboard.test.tsx | 13 ++ superset-frontend/src/features/home/Menu.test.tsx | 19 +++ 3 files changed, 165 insertions(+), 4 deletions(-) diff --git a/superset-frontend/src/dashboard/actions/hydrate.test.ts b/superset-frontend/src/dashboard/actions/hydrate.test.ts index 8da88098cd8..a67aaabfe1a 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.test.ts +++ b/superset-frontend/src/dashboard/actions/hydrate.test.ts @@ -27,6 +27,7 @@ import { TAB_TYPE, } from '../util/componentTypes'; import { DASHBOARD_GRID_ID, DASHBOARD_ROOT_ID } from '../util/constants'; +import { FilterBarOrientation } from '../types'; /** * Regression guard for the follow-up to PR #39417 / PR #41832: the default @@ -52,6 +53,7 @@ const layoutItem = ( const buildDashboard = ( positionData: Record<string, unknown>, title = 'Test dashboard', + metadata: Record<string, unknown> = {}, ) => ({ id: 1, dashboard_title: title, @@ -59,29 +61,40 @@ const buildDashboard = ( published: true, changed_on: '2024-01-01T00:00:00.000Z', owners: [], - metadata: {}, + metadata, position_data: positionData, }); +/** A user with dashboard-editor privileges, for editMode/permission tests. */ +const editorUser = { + userId: 1, + username: 'editor', + permissions: {}, + roles: { Admin: [['can_write', 'Dashboard']] }, +}; + const hydrate = ( positionData: Record<string, unknown>, overrides: { activeTabs?: string[] | null; dashboardState?: Record<string, unknown>; + user?: Record<string, unknown>; + metadata?: Record<string, unknown>; + charts?: unknown[]; } = {}, ) => { const dispatch = jest.fn((action: unknown) => action); const getState = () => ({ - user: { roles: {}, userId: 1 }, + user: overrides.user ?? { roles: {}, userId: 1 }, common: { conf: {} }, dashboardState: overrides.dashboardState ?? {}, }) as any; const action = ( hydrateDashboard({ history: { replace: jest.fn() }, - dashboard: buildDashboard(positionData), - charts: [], + dashboard: buildDashboard(positionData, undefined, overrides.metadata), + charts: overrides.charts ?? [], dataMask: {}, activeTabs: overrides.activeTabs ?? null, chartStates: null, @@ -357,3 +370,119 @@ test('rebuilds stale parents from the layout children', () => { 'ROW-a', ]); }); + +// The following tests read the `edit`/regular query params off +// window.location via extractUrlParams, so each restores the original URL. +const originalLocationHref = window.location.href; + +afterEach(() => { + window.history.replaceState(null, '', originalLocationHref); +}); + +const buildChart = (urlParams: Record<string, unknown> = {}) => ({ + slice_id: 7, + slice_url: '/explore/?slice_id=7', + slice_name: 'Test chart', + form_data: { + slice_id: 7, + viz_type: 'table', + datasource: '1__table', + url_params: urlParams, + }, + description: '', + description_markeddown: '', + editors: [], + modified: '', + changed_on: '2024-01-01T00:00:00.000Z', +}); + +// hydrate.ts derives editMode from the reserved `edit` URL param +// (extractUrlParams('reserved')), gated on whether the viewer may edit the +// dashboard at all. +test('?edit=true hydrates editMode true for a user who can edit', () => { + window.history.replaceState(null, '', '/superset/dashboard/1/?edit=true'); + + const action = hydrate(flatTabsPositionData, { user: editorUser }); + + expect(action.data.dashboardState.editMode).toBe(true); +}); + +test('editMode is false with no edit param, even for a user who can edit', () => { + window.history.replaceState(null, '', '/superset/dashboard/1/'); + + const action = hydrate(flatTabsPositionData, { user: editorUser }); + + expect(action.data.dashboardState.editMode).toBe(false); +}); + +test('?edit=true does not hydrate editMode for a user without edit permission', () => { + window.history.replaceState(null, '', '/superset/dashboard/1/?edit=true'); + + const action = hydrate(flatTabsPositionData, { + user: { roles: {}, userId: 1 }, + }); + + expect(action.data.dashboardState.editMode).toBe(false); +}); + +// hydrate.ts merges the non-reserved ("regular") URL query params into every +// chart's form_data.url_params so drill-through/native-filter query params +// carried on the dashboard URL reach each chart's query. +test('a regular url param is merged into the chart form_data url_params', () => { + window.history.replaceState(null, '', '/superset/dashboard/1/?foo=bar'); + + const action = hydrate(flatTabsPositionData, { + charts: [buildChart()], + }); + + expect(action.data.charts[7].form_data.url_params).toEqual({ + foo: 'bar', + }); +}); + +test('a regular url param overrides a same-key param already on the chart', () => { + window.history.replaceState(null, '', '/superset/dashboard/1/?foo=fromUrl'); + + const action = hydrate(flatTabsPositionData, { + charts: [buildChart({ foo: 'fromChart', keep: 'chartOnly' })], + }); + + expect(action.data.charts[7].form_data.url_params).toEqual({ + foo: 'fromUrl', + keep: 'chartOnly', + }); +}); + +test('reserved edit and standalone params are not merged into chart form_data url_params', () => { + window.history.replaceState( + null, + '', + '/superset/dashboard/1/?foo=bar&edit=true&standalone=1', + ); + + const action = hydrate(flatTabsPositionData, { + charts: [buildChart()], + }); + + expect(action.data.charts[7].form_data.url_params).toEqual({ foo: 'bar' }); +}); + +// hydrate.ts reads filterBarOrientation back from dashboard.metadata +// (metadata.filter_bar_orientation), falling back to Vertical when absent. +test('reads filter_bar_orientation from dashboard metadata', () => { + const action = hydrate(flatTabsPositionData, { + metadata: { filter_bar_orientation: FilterBarOrientation.Horizontal }, + }); + + expect(action.data.dashboardInfo.filterBarOrientation).toBe( + FilterBarOrientation.Horizontal, + ); +}); + +test('falls back to vertical when metadata has no filter_bar_orientation', () => { + const action = hydrate(flatTabsPositionData, { metadata: {} }); + + expect(action.data.dashboardInfo.filterBarOrientation).toBe( + FilterBarOrientation.Vertical, + ); +}); diff --git a/superset-frontend/src/dashboard/components/Dashboard.test.tsx b/superset-frontend/src/dashboard/components/Dashboard.test.tsx index 46bd9802310..3f3ef58a6c5 100644 --- a/superset-frontend/src/dashboard/components/Dashboard.test.tsx +++ b/superset-frontend/src/dashboard/components/Dashboard.test.tsx @@ -36,6 +36,7 @@ import dashboardState from 'spec/fixtures/mockDashboardState'; import { sliceEntitiesForChart as sliceEntities } from 'spec/fixtures/mockSliceEntities'; import { getAllActiveFilters } from 'src/dashboard/util/activeAllDashboardFilters'; import { getRelatedCharts } from 'src/dashboard/util/getRelatedCharts'; +import { LOG_ACTIONS_MOUNT_DASHBOARD } from 'src/logger/LogUtils'; // Test mock data doesn't perfectly match strict component prop types, // so we use a loosely-typed wrapper for test rendering @@ -118,6 +119,18 @@ describe('Dashboard', () => { expect(screen.getByText('Test')).toBeInTheDocument(); }); + test('should log a mount event on mount with the edit/publish state', () => { + renderDashboard({ editMode: true, isPublished: false }); + + expect(mockLogEvent).toHaveBeenCalledWith( + LOG_ACTIONS_MOUNT_DASHBOARD, + expect.objectContaining({ + is_edit_mode: true, + is_published: false, + }), + ); + }); + // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('layout changes', () => { const layoutWithExtraChart = { diff --git a/superset-frontend/src/features/home/Menu.test.tsx b/superset-frontend/src/features/home/Menu.test.tsx index 536491f2f20..84287e078a8 100644 --- a/superset-frontend/src/features/home/Menu.test.tsx +++ b/superset-frontend/src/features/home/Menu.test.tsx @@ -334,6 +334,25 @@ test('should render the navigation', async () => { expect(await screen.findByRole('navigation')).toBeInTheDocument(); }); +test('hides the navigation when the standalone url param is set', async () => { + const originalLocationHref = window.location.href; + useSelectorMock.mockReturnValue({ roles: user.roles }); + window.history.replaceState(null, '', '/superset/dashboard/1/?standalone=1'); + + try { + render(<Menu {...mockedProps} />, { + useRedux: true, + useQueryParams: true, + useRouter: true, + useTheme: true, + }); + + expect(screen.queryByRole('navigation')).not.toBeInTheDocument(); + } finally { + window.history.replaceState(null, '', originalLocationHref); + } +}); + test.each(['', '/myapp'])( 'should render the brand, including app_root "%s"', async app_root => {
