You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by jl...@apache.org on 2026/10/06 23:52:45 UTC

(superset) branch master updated: test(frontend): await remaining userEvent calls and lint for un-awaited ones (#44947)

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

sadpandajoe 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 794b243c543 test(frontend): await remaining userEvent calls and lint for un-awaited ones (#44947)
794b243c543 is described below

commit 794b243c5435ecf1a13180fe6cc5fd9389052fbf
Author: Sepuri Sai Krishna <sa...@gmail.com>
AuthorDate: Wed Oct 7 05:22:37 2026 +0530

    test(frontend): await remaining userEvent calls and lint for un-awaited ones (#44947)
---
 superset-frontend/scripts/check-custom-rules.js    | 163 +++++++++++++++++++--
 .../ChartContextMenu/ChartContextMenu.test.tsx     |   2 +-
 .../dashboard/components/Header/Header.test.tsx    |   4 +-
 .../FilterControls/GroupByFilterCard.test.tsx      |  24 +--
 .../ColumnSelectPopover.test.tsx                   |  38 ++---
 .../DndColumnSelect.test.tsx                       |  20 +--
 ...AdhocFilterEditPopoverSimpleTabContent.test.tsx |   4 +-
 .../AdhocMetricEditPopover.test.tsx                |   4 +-
 8 files changed, 201 insertions(+), 58 deletions(-)

diff --git a/superset-frontend/scripts/check-custom-rules.js b/superset-frontend/scripts/check-custom-rules.js
index d3502ace105..d623cd31926 100755
--- a/superset-frontend/scripts/check-custom-rules.js
+++ b/superset-frontend/scripts/check-custom-rules.js
@@ -650,32 +650,141 @@ function checkUntranslatedStrings(ast, filepath) {
 }
 
 /**
- * Process a single file
+ * Whether a node is a `userEvent.setup()` call.
  */
-function processFile(filepath) {
-  const code = fs.readFileSync(filepath, 'utf8');
+function isUserEventSetupCall(node) {
+  return (
+    !!node &&
+    node.type === 'CallExpression' &&
+    node.callee.type === 'MemberExpression' &&
+    node.callee.object.type === 'Identifier' &&
+    node.callee.object.name === 'userEvent' &&
+    node.callee.property.type === 'Identifier' &&
+    node.callee.property.name === 'setup'
+  );
+}
+
+/**
+ * Whether an identifier resolves to a `userEvent.setup()` session.
+ *
+ * Only a declarator initialized from the call counts. A session assigned
+ * separately from its declaration (`let user; user = userEvent.setup()`) is not
+ * recognized, and no test file uses that form.
+ *
+ * @returns true when `name` is bound to a user-event session in this scope
+ */
+function isUserEventSession(scope, name) {
+  const binding = scope.getBinding(name);
+  return (
+    !!binding &&
+    binding.path.node.type === 'VariableDeclarator' &&
+    isUserEventSetupCall(binding.path.node.init)
+  );
+}
 
+/**
+ * Check that `userEvent` interactions are awaited.
+ *
+ * Every `@testing-library/user-event` API returns a promise, so a call used as a
+ * bare statement is fire-and-forget: its events are still being dispatched when
+ * the next query or assertion runs. That either charges the dispatch time to a
+ * later `waitFor` budget or lets an assertion observe the state from before the
+ * interaction, and both surface as flaky tests.
+ *
+ * Both call styles are checked: `userEvent.click(el)` and the session style,
+ * `const user = userEvent.setup(); user.click(el)`. The session receiver is
+ * resolved through the scope, so only a binding initialized from
+ * `userEvent.setup()` is treated as one.
+ *
+ * Only bare expression statements are reported. A call whose promise is stored
+ * or returned (`const pending = userEvent.click(el)`) is left alone, because it
+ * may be awaited elsewhere. `userEvent.setup()` is itself exempt: it is
+ * synchronous and returns a session object rather than a promise.
+ */
+function checkAwaitedUserEvent(ast, filepath) {
+  traverse(ast, {
+    ExpressionStatement(path) {
+      const { expression } = path.node;
+      if (
+        expression.type !== 'CallExpression' ||
+        expression.callee.type !== 'MemberExpression'
+      ) {
+        return;
+      }
+      const { object, property } = expression.callee;
+      if (object.type !== 'Identifier' || property.type !== 'Identifier') {
+        return;
+      }
+      if (
+        object.name === 'userEvent'
+          ? property.name === 'setup'
+          : !isUserEventSession(path.scope, object.name)
+      ) {
+        return;
+      }
+      const line = path.node.loc ? path.node.loc.start.line : 0;
+      // eslint-disable-next-line no-console
+      console.error(
+        `${RED}✖${RESET} ${filepath}:${line}: un-awaited ${object.name}.${property.name}(). ` +
+          `user-event APIs return promises; await the call so its events are ` +
+          `dispatched before the next query or assertion.`,
+      );
+      errorCount += 1;
+    },
+  });
+}
+
+/**
+ * Parse a file, reporting an unparseable file as a warning.
+ *
+ * @returns the AST, or null when the file could not be parsed
+ */
+function parseFile(filepath) {
   try {
-    const ast = parser.parse(code, {
+    return parser.parse(fs.readFileSync(filepath, 'utf8'), {
       sourceType: 'module',
       plugins: ['jsx', 'typescript', 'decorators-legacy'],
       attachComments: true,
     });
-
-    // Run all checks
-    checkNoLiteralColors(ast, filepath);
-    checkNoFaIcons(ast, filepath);
-    checkNoDirectAntdImports(ast, filepath);
-    checkI18nTemplates(ast, filepath);
-    checkEagerTranslationsInConfig(ast, filepath);
-    checkUntranslatedStrings(ast, filepath);
   } catch (error) {
     // eslint-disable-next-line no-console
     console.warn(
       `${YELLOW}⚠${RESET} Could not parse ${filepath}: ${error.message}`,
     );
     warningCount += 1;
+    return null;
+  }
+}
+
+/**
+ * Process a single file
+ */
+function processFile(filepath) {
+  const ast = parseFile(filepath);
+  if (!ast) {
+    return;
   }
+
+  // Run all checks
+  checkNoLiteralColors(ast, filepath);
+  checkNoFaIcons(ast, filepath);
+  checkNoDirectAntdImports(ast, filepath);
+  checkI18nTemplates(ast, filepath);
+  checkEagerTranslationsInConfig(ast, filepath);
+  checkUntranslatedStrings(ast, filepath);
+}
+
+/**
+ * Process a single test file. The checks in `processFile` deliberately skip
+ * tests, so the test-only rules run from here instead.
+ */
+function processTestFile(filepath) {
+  const ast = parseFile(filepath);
+  if (!ast) {
+    return;
+  }
+
+  checkAwaitedUserEvent(ast, filepath);
 }
 
 /**
@@ -685,6 +794,11 @@ function processFile(filepath) {
 const TS_ONLY_SOURCE_PATTERN =
   /^(src|packages\/[^/]+\/src|plugins\/[^/]+\/src)\//;
 
+/**
+ * Jest test and spec files, which the test-only rules apply to.
+ */
+const TEST_FILE_PATTERN = /\.(test|spec)\.(ts|tsx|js|jsx)$/;
+
 /**
  * Enforce the TypeScript-only frontend convention: no `.js`/`.jsx` files may be
  * added under the application source trees (including test files). Build
@@ -757,6 +871,30 @@ function main() {
       : args.map(f => f.replace(/^superset-frontend\//, ''));
   checkTypeScriptOnlySource(tsOnlyCandidates);
 
+  // Run the test-only rules. Like the TypeScript-only check above, this works
+  // from the raw file list, because the ignore patterns below strip out tests.
+  const testCandidates =
+    args.length === 0
+      ? glob.sync('**/*.{test,spec}.{ts,tsx,js,jsx}', {
+          ignore: [
+            '**/node_modules/**',
+            '**/esm/**',
+            '**/lib/**',
+            '**/dist/**',
+          ],
+        })
+      : args
+          .map(f => f.replace(/^superset-frontend\//, ''))
+          .filter(f => TEST_FILE_PATTERN.test(f));
+  testCandidates.forEach(file => {
+    const resolvedPath = path.resolve(file);
+    if (fs.existsSync(resolvedPath)) {
+      processTestFile(resolvedPath);
+    } else if (fs.existsSync(file)) {
+      processTestFile(file);
+    }
+  });
+
   // If no files specified, check all
   if (files.length === 0) {
     files = glob.sync('src/**/*.{ts,tsx,js,jsx}', {
@@ -832,4 +970,5 @@ export default {
   checkI18nTemplates,
   checkUntranslatedStrings,
   checkTypeScriptOnlySource,
+  checkAwaitedUserEvent,
 };
diff --git a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.test.tsx b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.test.tsx
index 3e2f72ec1f2..b870fc2fd0a 100644
--- a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.test.tsx
+++ b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.test.tsx
@@ -349,7 +349,7 @@ test('semantic-view datasource resolves via the structure endpoint and renders t
     },
   });
 
-  userEvent.click(screen.getByTestId('open-semantic-context-menu'));
+  await userEvent.click(screen.getByTestId('open-semantic-context-menu'));
   // The menu opens without crashing on the dimension-derived shape.
   expect(await screen.findByRole('menu')).toBeInTheDocument();
 
diff --git a/superset-frontend/src/dashboard/components/Header/Header.test.tsx b/superset-frontend/src/dashboard/components/Header/Header.test.tsx
index 1aaab146987..3b6cbd1e6c8 100644
--- a/superset-frontend/src/dashboard/components/Header/Header.test.tsx
+++ b/superset-frontend/src/dashboard/components/Header/Header.test.tsx
@@ -832,8 +832,8 @@ test('shows Excel export progress after the header menu closes', async () => {
   });
 
   await openActionsDropdown();
-  userEvent.hover(screen.getByText('Download'));
-  userEvent.click(await screen.findByText('Export Data to Excel'));
+  await userEvent.hover(screen.getByText('Download'));
+  await userEvent.click(await screen.findByText('Export Data to Excel'));
 
   await waitFor(() => {
     expect(addInfoToast).toHaveBeenCalledWith(
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.test.tsx
index 54a920f8a93..e756a7e03ad 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.test.tsx
@@ -142,7 +142,7 @@ test('maps columns to options honouring filterable and verbose_name', async () =
   );
 
   const combobox = await screen.findByRole('combobox');
-  userEvent.click(combobox);
+  await userEvent.click(combobox);
 
   expect(await screen.findByText('Deal Size')).toBeInTheDocument();
   expect(screen.getByText('city')).toBeInTheDocument();
@@ -160,7 +160,7 @@ test('fetch failure fires the danger toast and leaves options empty', async () =
   expect(String(mockedAddDangerToast.mock.calls[0][0])).toMatch(/303/);
 
   const combobox = await screen.findByRole('combobox');
-  userEvent.click(combobox);
+  await userEvent.click(combobox);
   expect(screen.queryByText('Deal Size')).not.toBeInTheDocument();
 });
 
@@ -217,7 +217,7 @@ test('semantic-view target lists only the view dimensions under an id collision'
   renderCard([{ datasetId: 306, datasourceType: DatasourceType.SemanticView }]);
 
   const combobox = await screen.findByRole('combobox');
-  userEvent.click(combobox);
+  await userEvent.click(combobox);
 
   expect(await screen.findByText('Orders Status')).toBeInTheDocument();
   expect(screen.queryByText('address_line1')).not.toBeInTheDocument();
@@ -239,8 +239,8 @@ test('selecting a dimension persists a target that still carries datasourceType'
   renderCard([{ datasetId: 307, datasourceType: DatasourceType.SemanticView }]);
 
   const combobox = await screen.findByRole('combobox');
-  userEvent.click(combobox);
-  userEvent.click(await screen.findByText('Orders Users City'));
+  await userEvent.click(combobox);
+  await userEvent.click(await screen.findByText('Orders Users City'));
 
   await waitFor(() => expect(setPendingChartCustomization).toHaveBeenCalled());
   const persisted =
@@ -268,8 +268,8 @@ test('clearing the selection keeps the datasource binding intact', async () => {
   renderCard([{ datasetId: 308, datasourceType: DatasourceType.SemanticView }]);
 
   const combobox = await screen.findByRole('combobox');
-  userEvent.click(combobox);
-  userEvent.click(await screen.findByText('Orders State'));
+  await userEvent.click(combobox);
+  await userEvent.click(await screen.findByText('Orders State'));
   await waitFor(() => expect(setPendingChartCustomization).toHaveBeenCalled());
   setPendingChartCustomization.mockClear();
 
@@ -277,8 +277,10 @@ test('clearing the selection keeps the datasource binding intact', async () => {
   // datasource binding rather than collapsing to an empty object. After
   // selection the label exists twice (selection tag + dropdown option);
   // toggle via the option role.
-  userEvent.click(combobox);
-  userEvent.click(await screen.findByRole('option', { name: 'Orders State' }));
+  await userEvent.click(combobox);
+  await userEvent.click(
+    await screen.findByRole('option', { name: 'Orders State' }),
+  );
   await waitFor(() => expect(setPendingChartCustomization).toHaveBeenCalled());
   const cleared =
     setPendingChartCustomization.mock.calls[
@@ -314,7 +316,7 @@ test('semantic structure failure renders empty options and toasts exactly once w
   expect(mockedAddDangerToast).toHaveBeenCalledTimes(1);
 
   const combobox = await screen.findByRole('combobox');
-  userEvent.click(combobox);
+  await userEvent.click(combobox);
   expect(screen.queryByText('Orders Status')).not.toBeInTheDocument();
   // Never a cross-type fallback.
   expect(fetchMock.callHistory.calls('glob:*/api/v1/dataset/*')).toHaveLength(
@@ -348,7 +350,7 @@ test('switching from a failed binding to a healthy one never toasts the healthy
   // strictly after the switch, so any spurious toast would already have fired
   // and been counted by the time it settles (no wall-clock sleep needed).
   const combobox = await screen.findByRole('combobox');
-  userEvent.click(combobox);
+  await userEvent.click(combobox);
   expect(await screen.findByText('city')).toBeInTheDocument();
 
   // No toast ever names the healthy datasource 311.
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.test.tsx
index 91ce34eea26..f4e40f27b0e 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.test.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.test.tsx
@@ -360,7 +360,7 @@ const renderSemanticPopover = (
 
 const openDimensionsDropdown = async () => {
   const combobox = screen.getByRole('combobox', { name: 'Dimensions' });
-  userEvent.click(combobox);
+  await userEvent.click(combobox);
   await getDropdown();
   return combobox;
 };
@@ -410,8 +410,10 @@ test('Simple-only callers can select a saved-only semantic dimension', async ()
     );
   }
   await openDimensionsDropdown();
-  userEvent.click(within(await getDropdown()).getByText('Product Category'));
-  userEvent.click(screen.getByRole('button', { name: 'Save' }));
+  await userEvent.click(
+    within(await getDropdown()).getByText('Product Category'),
+  );
+  await userEvent.click(screen.getByRole('button', { name: 'Save' }));
   await waitFor(() =>
     expect(onChange).toHaveBeenCalledWith(SEMANTIC_COLUMNS[1]),
   );
@@ -492,10 +494,10 @@ test('lists every expression-less dimension as a Saved option without mutating m
   expect(within(dropdown).getByText('Product Category')).toBeInTheDocument();
   expect(within(dropdown).getByText('region')).toBeInTheDocument();
 
-  userEvent.click(within(dropdown).getByText('Order Date'));
+  await userEvent.click(within(dropdown).getByText('Order Date'));
   const saveButton = screen.getByTestId('ColumnEdit#save');
   await waitFor(() => expect(saveButton).toBeEnabled());
-  userEvent.click(saveButton);
+  await userEvent.click(saveButton);
 
   await waitFor(() =>
     expect(onChange).toHaveBeenCalledWith(SEMANTIC_COLUMNS[0]),
@@ -551,7 +553,7 @@ test('clearing the Saved-only dimension resets the selection', async () => {
   ) as HTMLElement;
   expect(clearButton).toBeInTheDocument();
 
-  userEvent.click(clearButton);
+  await userEvent.click(clearButton);
 
   await waitFor(() => expect(setLabel).toHaveBeenCalledWith(''));
 });
@@ -585,7 +587,7 @@ test('clearing the Simple-mode item resets the selection', async () => {
   ) as HTMLElement;
   expect(clearButton).toBeInTheDocument();
 
-  userEvent.click(clearButton);
+  await userEvent.click(clearButton);
 
   await waitFor(() => expect(setLabel).toHaveBeenCalledWith(''));
 });
@@ -723,10 +725,10 @@ test('an edited dimension that became incompatible cannot be saved until replace
 
   await openDimensionsDropdown();
   const dropdown = await getDropdown();
-  userEvent.click(within(dropdown).getByText('Order Date'));
+  await userEvent.click(within(dropdown).getByText('Order Date'));
 
   await waitFor(() => expect(saveButton).toBeEnabled());
-  userEvent.click(saveButton);
+  await userEvent.click(saveButton);
   await waitFor(() =>
     expect(onChange).toHaveBeenCalledWith(SEMANTIC_COLUMNS[0]),
   );
@@ -761,10 +763,10 @@ test('a legacy edited adhoc value opens Saved, stays inspectable, and blocks Sav
   fireEvent.click(screen.getByRole('tab', { name: 'Saved' }));
   await openDimensionsDropdown();
   const dropdown = await getDropdown();
-  userEvent.click(within(dropdown).getByText('Order Date'));
+  await userEvent.click(within(dropdown).getByText('Order Date'));
 
   await waitFor(() => expect(saveButton).toBeEnabled());
-  userEvent.click(saveButton);
+  await userEvent.click(saveButton);
   await waitFor(() =>
     expect(onChange).toHaveBeenCalledWith(SEMANTIC_COLUMNS[0]),
   );
@@ -806,7 +808,7 @@ test('disables Saved metrics by the compatible-metric list, not the dimension li
   const combobox = screen.getByRole('combobox', {
     name: 'Dimensions and metrics',
   });
-  userEvent.click(combobox);
+  await userEvent.click(combobox);
 
   expect(await getOptionItem('Total Sales')).not.toHaveClass(
     'ant-select-item-option-disabled',
@@ -827,7 +829,7 @@ test('a metrics-only semantic view still renders the Saved select', async () =>
   const combobox = screen.getByRole('combobox', {
     name: 'Dimensions and metrics',
   });
-  userEvent.click(combobox);
+  await userEvent.click(combobox);
 
   expect(await getOptionItem('Total Sales')).toBeInTheDocument();
   expect(await getOptionItem('Tax Amount')).toBeInTheDocument();
@@ -868,7 +870,7 @@ test('table datasources keep expression-based classification and enabled modes',
   const combobox = screen.getByRole('combobox', {
     name: 'Columns and metrics',
   });
-  userEvent.click(combobox);
+  await userEvent.click(combobox);
   const dropdown = await getDropdown();
   expect(within(dropdown).getByText('plain_col')).toBeInTheDocument();
   expect(within(dropdown).queryByText('calc_col')).not.toBeInTheDocument();
@@ -934,17 +936,17 @@ test('a feature-declaring semantic view keeps expression-based classification an
   const simpleCombobox = screen.getByRole('combobox', {
     name: 'Columns and metrics',
   });
-  userEvent.click(simpleCombobox);
+  await userEvent.click(simpleCombobox);
   const dropdown = await getDropdown();
   expect(within(dropdown).getByText('Plain Dimension')).toBeInTheDocument();
   expect(
     within(dropdown).queryByText('calc_dimension'),
   ).not.toBeInTheDocument();
 
-  userEvent.click(within(dropdown).getByText('Plain Dimension'));
+  await userEvent.click(within(dropdown).getByText('Plain Dimension'));
   const saveButton = screen.getByTestId('ColumnEdit#save');
   await waitFor(() => expect(saveButton).toBeEnabled());
-  userEvent.click(saveButton);
+  await userEvent.click(saveButton);
   await waitFor(() => expect(onChange).toHaveBeenCalledWith(columns[0]));
 
   expect(screen.queryByRole('alert')).not.toBeInTheDocument();
@@ -976,7 +978,7 @@ test('non-semantic datasources still filter Saved options by compatibility metad
     { store },
   );
 
-  userEvent.click(
+  await userEvent.click(
     screen.getByRole('combobox', { name: 'Columns and metrics' }),
   );
   const dropdown = await getDropdown();
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.test.tsx
index e63a9e64278..0fd49b66f67 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.test.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.test.tsx
@@ -649,7 +649,7 @@ test('saved-only semantic view disables Simple and Custom SQL modes in the picke
     { useDndKit: true, store: semanticViewStore() },
   );
 
-  userEvent.click(screen.getByText(/Drop columns here or click/i));
+  await userEvent.click(screen.getByText(/Drop columns here or click/i));
 
   await waitFor(() => {
     expect(screen.getByRole('tab', { name: 'Saved' })).toBeInTheDocument();
@@ -678,7 +678,7 @@ test('semantic view declaring adhoc expressions keeps existing modes in the pick
     },
   );
 
-  userEvent.click(screen.getByText(/Drop columns here or click/i));
+  await userEvent.click(screen.getByText(/Drop columns here or click/i));
 
   await waitFor(() => {
     expect(screen.getByRole('tab', { name: 'Simple' })).toBeInTheDocument();
@@ -710,21 +710,21 @@ test('commits a visible Cube dimension in two interactions after opening the pic
     { useDndKit: true, store: semanticViewStore() },
   );
 
-  userEvent.click(screen.getByText(/Drop columns here or click/i));
+  await userEvent.click(screen.getByText(/Drop columns here or click/i));
 
   const combobox = await screen.findByRole('combobox', {
     name: 'Dimensions',
   });
 
   // Interaction 1: select the visible dimension.
-  userEvent.click(combobox);
+  await userEvent.click(combobox);
   const option = await screen.findByRole('option', { name: /Order Date/i });
-  userEvent.click(option);
+  await userEvent.click(option);
 
   // Interaction 2: save.
   const saveButton = await screen.findByTestId('ColumnEdit#save');
   await waitFor(() => expect(saveButton).toBeEnabled());
-  userEvent.click(saveButton);
+  await userEvent.click(saveButton);
 
   await waitFor(() => {
     expect(mockOnChange).toHaveBeenCalledWith(['order_date']);
@@ -743,7 +743,7 @@ test('commits a searched Cube dimension in no more than three interactions', asy
     { useDndKit: true, store: semanticViewStore() },
   );
 
-  userEvent.click(screen.getByText(/Drop columns here or click/i));
+  await userEvent.click(screen.getByText(/Drop columns here or click/i));
 
   const combobox = await screen.findByRole('combobox', {
     name: 'Dimensions',
@@ -756,12 +756,12 @@ test('commits a searched Cube dimension in no more than three interactions', asy
   const option = await screen.findByRole('option', {
     name: /Product Category/i,
   });
-  userEvent.click(option);
+  await userEvent.click(option);
 
   // Interaction 3: save.
   const saveButton = await screen.findByTestId('ColumnEdit#save');
   await waitFor(() => expect(saveButton).toBeEnabled());
-  userEvent.click(saveButton);
+  await userEvent.click(saveButton);
 
   await waitFor(() => {
     expect(mockOnChange).toHaveBeenCalledWith(['category']);
@@ -794,7 +794,7 @@ test('anchors the "add column" popover to a block-level trigger box (sc-120502)'
     { useDndKit: true, store },
   );
 
-  userEvent.click(screen.getByText(/Drop columns here or click/i));
+  await userEvent.click(screen.getByText(/Drop columns here or click/i));
 
   await waitFor(() => {
     expect(screen.getByRole('tab', { name: 'Simple' })).toBeInTheDocument();
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/AdhocFilterEditPopoverSimpleTabContent.test.tsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/AdhocFilterEditPopoverSimpleTabContent.test.tsx
index 72899a9b7f9..0a14eb9c33e 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/AdhocFilterEditPopoverSimpleTabContent.test.tsx
+++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/AdhocFilterEditPopoverSimpleTabContent.test.tsx
@@ -1370,7 +1370,7 @@ test('Filter subject lists and commits an expression-less Cube dimension', async
   const subjectSelect = screen.getByRole('combobox', {
     name: 'Select subject',
   });
-  userEvent.click(subjectSelect);
+  await userEvent.click(subjectSelect);
 
   const dropdown = await waitFor(() => {
     const list = document.querySelector(
@@ -1382,7 +1382,7 @@ test('Filter subject lists and commits an expression-less Cube dimension', async
   expect(within(dropdown).getByText('Order Date')).toBeInTheDocument();
   expect(within(dropdown).getByText('Product Category')).toBeInTheDocument();
 
-  userEvent.click(within(dropdown).getByText('Product Category'));
+  await userEvent.click(within(dropdown).getByText('Product Category'));
 
   await waitFor(() => {
     expect(props.onChange).toHaveBeenCalledWith(
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/AdhocMetricEditPopover.test.tsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/AdhocMetricEditPopover.test.tsx
index 0e4f93a4512..56a6073b95c 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/AdhocMetricEditPopover.test.tsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/AdhocMetricEditPopover.test.tsx
@@ -474,7 +474,7 @@ test('disables saved metrics absent from a verified compatibility result', async
     },
   });
 
-  userEvent.click(
+  await userEvent.click(
     screen.getByRole('combobox', { name: 'Select saved metrics' }),
   );
 
@@ -500,7 +500,7 @@ test('keeps every saved metric enabled after a failed compatibility request', as
     },
   });
 
-  userEvent.click(
+  await userEvent.click(
     screen.getByRole('combobox', { name: 'Select saved metrics' }),
   );