diff --git a/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilter.spec.ts b/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilter.spec.ts index 753bde37c68..4cdc7d3d3a8 100644 --- a/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilter.spec.ts +++ b/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilter.spec.ts @@ -423,10 +423,122 @@ describe("DimensionFilter", () => { // Close the dropdown so bits-ui's body-scroll-lock cleanup fires while // jsdom is still alive (otherwise the deferred cleanup fires after teardown // and produces an unhandled "document is not defined" error). + await act(() => screen.getByLabelText("Open publisher filter").click()); + }); + + it("rerenders the chip when only the include/exclude operator changes", async () => { + const props = { + filterData: { + name: "device_type", + label: "Device Type", + mode: DimensionFilterMode.Select, + dimensions: new Map([[AD_BIDS_METRICS_NAME, {}]]), + selectedValues: ["ConnectedTV"], + isInclude: false, + }, + expressionMap: new Map(), + openOnMount: false, + timeStart: undefined, + timeEnd: undefined, + timeDimension: undefined, + timeControlsReady: false, + removeDimensionFilter: vi.fn(), + applyDimensionInListMode: vi.fn(), + toggleDimensionValueSelections: vi.fn(), + applyDimensionContainsMode: vi.fn(), + toggleDimensionFilterMode: vi.fn(), + }; + + const { rerender } = render(DimensionFilter, { + props, + context: new Map([ + [ + RUNTIME_CONTEXT_KEY, + new RuntimeClient({ host: "http://localhost", instanceId: "test" }), + ], + ]), + }); + + const chip = screen.getByLabelText("device_type filter"); + expect(screen.getByLabelText("Open device_type filter")).toHaveTextContent( + "Exclude Device Type ConnectedTV", + ); + expect(chip).toHaveClass("exclude"); + + await rerender({ + ...props, + filterData: { + ...props.filterData, + isInclude: true, + }, + }); + + await waitFor(() => + expect( + screen.getByLabelText("Open device_type filter"), + ).toHaveTextContent("Device Type ConnectedTV"), + ); + expect( + screen.getByLabelText("Open device_type filter"), + ).not.toHaveTextContent("Exclude"); + expect(chip).not.toHaveClass("exclude"); + }); + + it("applies a pending include/exclude toggle when select mode closes", async () => { + const toggleDimensionFilterMode = vi.fn().mockResolvedValue(undefined); + const toggleDimensionValueSelections = vi.fn().mockResolvedValue(undefined); + const props = { + filterData: { + name: AD_BIDS_PUBLISHER_DIMENSION, + label: "Publisher", + mode: DimensionFilterMode.Select, + dimensions: new Map([[AD_BIDS_METRICS_NAME, {}]]), + selectedValues: ["Facebook"], + isInclude: true, + }, + expressionMap: new Map(), + openOnMount: false, + timeStart: undefined, + timeEnd: undefined, + timeDimension: undefined, + timeControlsReady: false, + removeDimensionFilter: vi.fn(), + applyDimensionInListMode: vi.fn(), + toggleDimensionValueSelections, + applyDimensionContainsMode: vi.fn(), + toggleDimensionFilterMode, + }; + + render(DimensionFilter, { + props, + context: new Map([ + [ + RUNTIME_CONTEXT_KEY, + new RuntimeClient({ host: "http://localhost", instanceId: "test" }), + ], + ]), + }); + + await act(() => screen.getByLabelText("Open publisher filter").click()); await act(() => - fireEvent.keyDown(screen.getByLabelText("publisher search list"), { - key: "Escape", - }), + fireEvent.click(screen.getByLabelText("Include exclude toggle")), + ); + expect(screen.getByLabelText("Include exclude toggle")).toHaveAttribute( + "data-state", + "checked", + ); + await act(() => screen.getByLabelText("Open publisher filter").click()); + + await waitFor(() => + expect(toggleDimensionFilterMode).toHaveBeenCalledWith( + AD_BIDS_PUBLISHER_DIMENSION, + [AD_BIDS_METRICS_NAME], + ), + ); + expect(toggleDimensionValueSelections).toHaveBeenCalledWith( + AD_BIDS_PUBLISHER_DIMENSION, + [], + [AD_BIDS_METRICS_NAME], ); }); }); diff --git a/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilter.svelte b/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilter.svelte index 3da8eeddb27..e8c8b4e2fc2 100644 --- a/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilter.svelte +++ b/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilter.svelte @@ -70,6 +70,7 @@ : []; let curPinned = filterData.pinned; let curRequired = filterData.required; + let excludeModeDirty = false; const client = useRuntimeClient(); @@ -86,7 +87,19 @@ missingRequired, } = filterData); - $: if (!open && filterData.mode !== curMode) { + $: if ( + !open && + excludeModeDirty && + (isInclude === false) === curExcludeMode + ) { + excludeModeDirty = false; + } + + $: if ( + !open && + (mode !== curMode || + (!excludeModeDirty && (isInclude === false) !== curExcludeMode)) + ) { resyncFilterData(filterData); } @@ -292,6 +305,7 @@ function handleToggleExcludeMode(checked: boolean) { curExcludeMode = checked; + excludeModeDirty = true; } function onToggleSelectAll() { @@ -396,6 +410,7 @@ curMode = filterData.mode; curSearchText = filterData.inputText ?? ""; curExcludeMode = filterData.isInclude === false; + excludeModeDirty = false; selectedValuesProxy = filterData.selectedValues ?? []; searchedBulkValues = filterData.mode === DimensionFilterMode.InList