This is an automated email from the ASF dual-hosted git repository.

jli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 75fa474fced test(native-filters): add unit tests for requiredFirst 
filter logic (#37640)
75fa474fced is described below

commit 75fa474fced1b6500755ee2b1c227d652afe7f15
Author: Gabriel Torres Ruiz <[email protected]>
AuthorDate: Thu Feb 5 15:36:35 2026 -0300

    test(native-filters): add unit tests for requiredFirst filter logic (#37640)
---
 .../components/DashboardBuilder/state.test.ts      | 254 +++++++++++++++++++++
 1 file changed, 254 insertions(+)

diff --git 
a/superset-frontend/src/dashboard/components/DashboardBuilder/state.test.ts 
b/superset-frontend/src/dashboard/components/DashboardBuilder/state.test.ts
new file mode 100644
index 00000000000..eb7a77710ce
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/state.test.ts
@@ -0,0 +1,254 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Tests for the requiredFirstFilter logic in useNativeFilters hook.
+ *
+ * This tests the core logic that determines whether to block dashboard 
rendering
+ * based on the "Select first filter value by default" (requiredFirst) setting.
+ */
+
+const requiredFirstFilterPredicate = (filter: {
+  requiredFirst?: boolean;
+  filterType?: string;
+}) =>
+  'requiredFirst' in filter &&
+  filter.requiredFirst === true &&
+  filter.filterType !== 'filter_time';
+
+/**
+ * Unit test for the missingInitialFilters logic.
+ * This determines which requiredFirst filters are missing values.
+ */
+const isMissingValue = (
+  filterId: string,
+  dataMask: Record<string, { filterState?: { value?: unknown } }>,
+) => dataMask[filterId]?.filterState?.value === undefined;
+
+test('requiredFirstFilterPredicate only matches filters with requiredFirst 
explicitly true', () => {
+  expect(
+    requiredFirstFilterPredicate({
+      requiredFirst: true,
+      filterType: 'filter_select',
+    }),
+  ).toBe(true);
+
+  expect(
+    requiredFirstFilterPredicate({
+      requiredFirst: false,
+      filterType: 'filter_select',
+    }),
+  ).toBe(false);
+
+  expect(
+    requiredFirstFilterPredicate({
+      requiredFirst: undefined,
+      filterType: 'filter_select',
+    }),
+  ).toBe(false);
+
+  // Filter without requiredFirst property at all
+  expect(requiredFirstFilterPredicate({ filterType: 'filter_select' })).toBe(
+    false,
+  );
+});
+
+test('requiredFirstFilterPredicate excludes time filters even when 
requiredFirst is true', () => {
+  expect(
+    requiredFirstFilterPredicate({
+      requiredFirst: true,
+      filterType: 'filter_time',
+    }),
+  ).toBe(false);
+
+  expect(
+    requiredFirstFilterPredicate({
+      requiredFirst: true,
+      filterType: 'filter_select',
+    }),
+  ).toBe(true);
+});
+
+test('isMissingValue correctly identifies undefined values', () => {
+  // undefined is missing
+  expect(
+    isMissingValue('filter-1', {
+      'filter-1': { filterState: { value: undefined } },
+    }),
+  ).toBe(true);
+
+  // Missing dataMask entry is also missing
+  expect(isMissingValue('filter-1', {})).toBe(true);
+
+  // Missing filterState is missing
+  expect(isMissingValue('filter-1', { 'filter-1': {} })).toBe(true);
+
+  // null is NOT missing (null !== undefined)
+  expect(
+    isMissingValue('filter-1', {
+      'filter-1': { filterState: { value: null } },
+    }),
+  ).toBe(false);
+
+  // empty array is NOT missing ([] !== undefined)
+  expect(
+    isMissingValue('filter-1', { 'filter-1': { filterState: { value: [] } } }),
+  ).toBe(false);
+
+  // actual value is NOT missing
+  expect(
+    isMissingValue('filter-1', {
+      'filter-1': { filterState: { value: ['val'] } },
+    }),
+  ).toBe(false);
+});
+
+test('filters with requiredFirst:false do not block dashboard (regression test 
for #36062)', () => {
+  // This is the exact bug scenario from PR #36062
+  const filters = [
+    {
+      id: 'filter-1',
+      name: 'Filter 1',
+      requiredFirst: false, // Was true, now disabled
+      filterType: 'filter_select',
+    },
+  ];
+
+  const dataMask = {
+    'filter-1': { filterState: { value: undefined } }, // No value set
+  };
+
+  // Filter should NOT be included in requiredFirstFilter
+  const requiredFirstFilters = filters.filter(requiredFirstFilterPredicate);
+  expect(requiredFirstFilters).toHaveLength(0);
+
+  // Therefore, no missing initial filters
+  const missingInitialFilters = requiredFirstFilters
+    .filter(f => isMissingValue(f.id, dataMask))
+    .map(f => f.name);
+  expect(missingInitialFilters).toHaveLength(0);
+
+  // Dashboard should show (missingInitialFilters.length === 0)
+  const showDashboard = missingInitialFilters.length === 0;
+  expect(showDashboard).toBe(true);
+});
+
+test('filters with requiredFirst:true and no value block dashboard', () => {
+  const filters = [
+    {
+      id: 'filter-1',
+      name: 'Required Filter',
+      requiredFirst: true,
+      filterType: 'filter_select',
+    },
+  ];
+
+  const dataMask = {
+    'filter-1': { filterState: { value: undefined } },
+  };
+
+  const requiredFirstFilters = filters.filter(requiredFirstFilterPredicate);
+  expect(requiredFirstFilters).toHaveLength(1);
+
+  const missingInitialFilters = requiredFirstFilters
+    .filter(f => isMissingValue(f.id, dataMask))
+    .map(f => f.name);
+  expect(missingInitialFilters).toEqual(['Required Filter']);
+
+  // Dashboard should NOT show
+  const showDashboard = missingInitialFilters.length === 0;
+  expect(showDashboard).toBe(false);
+});
+
+test('filters with requiredFirst:true and a value allow dashboard to show', () 
=> {
+  const filters = [
+    {
+      id: 'filter-1',
+      name: 'Required Filter',
+      requiredFirst: true,
+      filterType: 'filter_select',
+    },
+  ];
+
+  const dataMask = {
+    'filter-1': { filterState: { value: ['some-value'] } },
+  };
+
+  const requiredFirstFilters = filters.filter(requiredFirstFilterPredicate);
+  expect(requiredFirstFilters).toHaveLength(1);
+
+  const missingInitialFilters = requiredFirstFilters
+    .filter(f => isMissingValue(f.id, dataMask))
+    .map(f => f.name);
+  expect(missingInitialFilters).toHaveLength(0);
+
+  // Dashboard should show
+  const showDashboard = missingInitialFilters.length === 0;
+  expect(showDashboard).toBe(true);
+});
+
+test('only requiredFirst:true filters without values block dashboard', () => {
+  const filters = [
+    {
+      id: 'filter-1',
+      name: 'Required With Value',
+      requiredFirst: true,
+      filterType: 'filter_select',
+    },
+    {
+      id: 'filter-2',
+      name: 'Required Without Value',
+      requiredFirst: true,
+      filterType: 'filter_select',
+    },
+    {
+      id: 'filter-3',
+      name: 'Non-Required',
+      requiredFirst: false,
+      filterType: 'filter_select',
+    },
+    {
+      id: 'filter-4',
+      name: 'Time Filter',
+      requiredFirst: true,
+      filterType: 'filter_time', // Excluded
+    },
+  ];
+
+  const dataMask = {
+    'filter-1': { filterState: { value: ['val'] } },
+    'filter-2': { filterState: { value: undefined } },
+    'filter-3': { filterState: { value: undefined } },
+    'filter-4': { filterState: { value: undefined } },
+  };
+
+  const requiredFirstFilters = filters.filter(requiredFirstFilterPredicate);
+  // Only filter-1 and filter-2 should be included (not filter-3 or filter-4)
+  expect(requiredFirstFilters.map(f => f.id)).toEqual(['filter-1', 
'filter-2']);
+
+  const missingInitialFilters = requiredFirstFilters
+    .filter(f => isMissingValue(f.id, dataMask))
+    .map(f => f.name);
+  // Only filter-2 is missing a value
+  expect(missingInitialFilters).toEqual(['Required Without Value']);
+
+  // Dashboard should NOT show because filter-2 is missing
+  const showDashboard = missingInitialFilters.length === 0;
+  expect(showDashboard).toBe(false);
+});

Reply via email to