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 => {

Reply via email to