Compare commits

...

142 Commits

Author SHA1 Message Date
Anton Kulyk
f667d79bb6 Show root collection name 2024-12-06 13:19:22 +00:00
Anton Kulyk
6eaf130e33 Improve sorting + dim non-combinable cards 2024-12-06 13:19:22 +00:00
Anton Kulyk
2e6b38f35c Add data type stack component 2024-12-06 13:19:22 +00:00
Anton Kulyk
d77ce41fd7 Prototype basic items ordering in data importer 2024-12-06 13:19:21 +00:00
Anton Kulyk
dce52eb1cf Merge branch 'master' into feature-visualizer 2024-12-06 13:14:32 +00:00
Chris Truter
3d6c90698b Improve queue test and add more buffering (#50980) 2024-12-06 12:31:06 +00:00
Denis Berezin
7b4c298126 chore: Move alerts and pulse components to notifications domain (#50929) 2024-12-06 14:39:45 +03:00
Mahatthana (Kelvin) Nomsawadi
631947bfb8 Streamline embed flow when embedding is off (#50842)
* Always show embed sharing option for admins (#50513)

* Always render the public link option

This is possible because metabase#50504

* Always render the embed option

* Fix unit tests

* Fix E2E tests

* Correct test semantic

* Hide Embed option for non-admins

* New Embed modal (#50580)

* Open "Compare options" in a new tab

* Add an external link icon to interactive embed option when it should

* Add disabled state to SDK

* Add a disable state to static and interactive embedding

* Add interactive embedding disabled state

* Implement disabled link

* Prepare folder for testing

* Fix tests and add SDK tests

* Add interactive embedding tests

* Correct test desribe block levels

* Add static embedding tests

* Add premium interactive embedding tests

* Add enterprise tests (EE without tokens)

* Fix the rest of the failed unit tests

* Update E2E tests

* Fix a case where OSS see disabled interactive embedding

This should be impossible as interactive embedding isn't available on
OSS, so we should not display the disabled state.

* Address review

Co-authored-by: Oisin Coveney <oisin@metabase.com>

* Address review

Co-Authored-by: Poom poom@poom.dev

---------

Co-authored-by: Oisin Coveney <oisin@metabase.com>

* Update Interactive Embedding Admin Settings page (#50693)

* Add a quick start link to interactive embedding settings page

* Update UTM tags for interactive embedding card

* Don't show interactive embedding option as enabled on OSS

* Fix unit tests

* Add a new test

---------

Co-authored-by: Oisin Coveney <oisin@metabase.com>
2024-12-06 10:26:41 +00:00
Uladzimir Havenchyk
6ce8344c90 chore: rework DataSelector styles to css modules (#50912)
* chore: rework DataSelector to css modules

* fix imports

* revert changes

* fix styles

* fix styles

* fix styles

* fix styles

* use upper case

* address review

* add missing files

* one more entry
2024-12-06 13:07:49 +03:00
Aleksandr Lesnenko
6d83cb2b60 fix scatter chart tooltip (#50889)
* fix scatter chart tooltip shows irrelevant info

* remove outdated spec as we always show the tooltip_column setting
2024-12-05 20:53:33 -05:00
Jeff Bruemmer
9f4327cf87 update env vars (#50963) 2024-12-05 22:56:10 +00:00
bryan
989bb6ff23 New and improved Examples collection (#50829)
* fix the script, and add the new edn

* make the migrations run in the proper order

* it works (needs cleanup)

* remove broken binning

- These bins rely on fingerprinting for the custom Age column

- The fingerprinting for that custom column are not properly exported in
serialization, hence do not get loaded into metabase, causing binning by
Age to fail.

* cleanup / linting

* cleanup

* update migration test ids to test

* realign test name to check for to see if sample content was created

* remove Product collection + move Examples into root

* update collection_ids for the content too

* remove click behavior for 'Revenue goal for this quarter'

* Apply suggestions from code review

Co-authored-by: Chris Truter <crisptrutski@users.noreply.github.com>

* for the sample collection is_sample should be true

* Drop CreateInternalUserNoOp

* typo

* update migration comments

* 2 small fixes

* fix data perms for example coll

* add a comment so we don't have 2 blank lines in a row

---------

Co-authored-by: Chris Truter <crisptrutski@users.noreply.github.com>
2024-12-05 15:33:20 -07:00
William
82c39bb942 Make postgres enums not error when using :is-empty/:not-empty (#50914) 2024-12-05 22:23:31 +00:00
github-automation-metabase
bcd41db9ad Add v0.50.35 to the list of releases (#50835) 2024-12-05 22:19:25 +00:00
dpsutton
a135e609e8 Include explicit require to c.t.build.tasks.uber (#50941)
Fixes https://github.com/metabase/metabase/issues/50940

Build sometimes fails with

```clojure
      Compiling namespaces [metabase.driver.sqlserver]
{:via
 [{:type clojure.lang.ExceptionInfo,
   :message
   "Running build steps for Enterprise Edition version UNKNOWN: version, translations, frontend, licenses, drivers, uberjar",
   :data {},
   :at [metabuild_common.steps$do_step invokeStatic "steps.clj" 90]}
  {:type clojure.lang.ExceptionInfo,
   :message "Building all drivers in parallel (:ee edition)",
   :data {},
   :at [metabuild_common.steps$do_step invokeStatic "steps.clj" 90]}
  {:type java.util.concurrent.ExecutionException,
   :message "clojure.lang.ExceptionInfo: Build driver :databricks (edition = :ee, options = nil) {}",
   :at [java.util.concurrent.FutureTask report "FutureTask.java" 122]}
  {:type clojure.lang.ExceptionInfo,
   :message "Build driver :databricks (edition = :ee, options = nil)",
   :data {},
   :at [metabuild_common.steps$do_step invokeStatic "steps.clj" 90]}
  {:type clojure.lang.ExceptionInfo,
   :message
   "Write :databricks :ee uberjar -> /home/runner/work/metabase/metabase/resources/modules/databricks.metabase-driver.jar",
   :data {},
   :at [metabuild_common.steps$do_step invokeStatic "steps.clj" 90]}
  {:type java.lang.IllegalStateException,
   :message "Attempting to call unbound fn: #'clojure.tools.build.tasks.uber/uber",
   :at [clojure.lang.Var$Unbound throwArity "Var.java" 47]}],
 :trace
 [[clojure.lang.Var$Unbound throwArity "Var.java" 47]
  [clojure.lang.AFn invoke "AFn.java" 32]
  [clojure.lang.Var invoke "Var.java" 386]
  [clojure.tools.build.api$uber invokeStatic "api.clj" 511]
  [clojure.tools.build.api$uber invoke "api.clj" 439]
  [build_drivers.create_uberjar$create_uberjar_BANG_$fn__4951 invoke "create_uberjar.clj" 82]
  [metabuild_common.steps$do_step invokeStatic "steps.clj" 87]
  [metabuild_common.steps$do_step invoke "steps.clj" 81]
  [build_drivers.create_uberjar$create_uberjar_BANG_ invokeStatic "create_uberjar.clj" 80]
  [build_drivers.create_uberjar$create_uberjar_BANG_ invoke "create_uberjar.clj" 77]
  [build_drivers.build_driver$build_driver_BANG_$fn__6636 invoke "build_driver.clj" 37]
  [metabuild_common.steps$do_step invokeStatic "steps.clj" 87]
  [metabuild_common.steps$do_step invoke "steps.clj" 81]
  [build_drivers.build_driver$build_driver_BANG_ invokeStatic "build_driver.clj" 33]
  [build_drivers.build_driver$build_driver_BANG_ invoke "build_driver.clj" 17]
  [build_drivers.build_driver$build_driver_BANG_ invokeStatic "build_driver.clj" 24]
  [build_drivers.build_driver$build_driver_BANG_ invoke "build_driver.clj" 17]
  [build_drivers$build_drivers_BANG_$fn__6657$fn__6658 invoke "build_drivers.clj" 31]
  [clojure.core$pmap$fn__8582$fn__8583 invoke "core.clj" 7169]
  [clojure.core$binding_conveyor_fn$fn__5842 invoke "core.clj" 2047]
  [clojure.lang.AFn call "AFn.java" 18]
  [java.util.concurrent.FutureTask run "FutureTask.java" 264]
  [java.util.concurrent.ThreadPoolExecutor runWorker "ThreadPoolExecutor.java" 1128]
  [java.util.concurrent.ThreadPoolExecutor$Worker run "ThreadPoolExecutor.java" 628]
  [java.lang.Thread run "Thread.java" 829]],
 :cause "Attempting to call unbound fn: #'clojure.tools.build.tasks.uber/uber"}
```

Seems that pmap-ing over the tasks hits a race condition with how the api works

```clojure
(defn uber
  [params]
  (assert-required "uber" params [:class-dir :uber-file])
  (assert-specs "uber" params
    :class-dir ::specs/path
    :uber-file ::specs/path)
  ((requiring-resolve 'clojure.tools.build.tasks.uber/uber) params))
```

So we just require `clojure.tools.build.tasks.uber` so that the
namespace is compiled and ready.
2024-12-05 13:37:56 -06:00
metamben
c2504f4e99 Support desugaring during function (#50903) 2024-12-05 22:31:22 +03:00
Alexander Polyankin
fd0cafeb7b [FE] Move exclude date filter parts to MBQL lib (#50888) 2024-12-05 14:20:52 -05:00
Aleksandr Lesnenko
3e1f18a9cf sankey static viz integration test (#50793) 2024-12-05 12:59:01 -05:00
lbrdnk
89faa64681 Add binning to display name (#50531)
Add binning to display name similarly as done with bucketing.
---------

Co-authored-by: metamben <103100869+metamben@users.noreply.github.com>
2024-12-05 17:28:38 +00:00
Oleksandr Yakushev
8e68606eb4 [xlsx-export] Refactor postprocess namespace with perf/concat (#50170) 2024-12-05 18:53:28 +02:00
Chris Truter
26e8f86868 Fix edge cases in New Search around model filtering (#50937) 2024-12-05 15:56:57 +00:00
Anton Kulyk
53537a094e Fix state reset 2024-12-05 15:38:04 +00:00
Anton Kulyk
4ea9070da0 Fix item highlighting in data importer 2024-12-05 15:28:17 +00:00
Anton Kulyk
4347f96db1 Update loading state 2024-12-05 15:28:17 +00:00
Anton Kulyk
e90952591b Fix unexpected history items after init 2024-12-05 15:28:17 +00:00
Anton Kulyk
017bd49c2c Serialize visualizer state into a url hash 2024-12-05 15:28:17 +00:00
Nicolò Pretto
4c27ad5fa3 fix(sdk): remove Error.captureStackTrace as it errors on firefox (#50773)
firefox
2024-12-05 15:38:45 +01:00
Oisin Coveney
2c76bd4a92 Migrate favicon logic to mantine hook (#50878) 2024-12-05 16:17:34 +02:00
Chris Truter
8692d9e000 Keep support for the old search engine name (#50876) 2024-12-05 07:31:39 +00:00
Romeo Van Snick
02e80cf2ae Loki flake: make delays configurable independently (#50806)
* Add delay helper

* Remove delays in storybook

* Use skippable delay in ListField and SingleSelectListField

* Use skippable delay in ExplicitSize

* Remove waitTimeContext in storybook

* Remove waitTimeContext altogether

* Add documentation to delay

* Remove all context references in ExplicitSize

* Update flaky snapshots
2024-12-05 11:58:46 +11:00
Case Nelson
6f1f0c02ed fix: Qualify postgres enums outside public schema (#50900)
Fixes: #50771

The new sync process failed to qualify enums outside public and
therefore failed to properly type them in database_type. This could
either result in matching the wrong type if there was a conflict or not
being typed as an enum.
2024-12-04 14:33:49 -07:00
github-automation-metabase
3be2611784 Add v0.52.1 to the list of releases (#50906) 2024-12-04 16:11:56 -05:00
github-automation-metabase
5b3278831c Add v0.51.7 to the list of releases (#50899) 2024-12-04 15:49:13 -05:00
Case Nelson
c592413341 test: syncing views against all drivers (#50236)
* test: see which drivers pass these tests

* Fix build

* Change test to use standard dataset, add initial implementation for bigquery

* Use fully qualified names for views

* Don't use transactions for databricks

* Make drop table only if exists, fix describe-index in mongo for views

* don't sync system.views

* Fix oracle drop-views, disable non-syncing views

* Use metabase-instance to find the table, this is important because redshift and oracle munge table name

* Fix oracle tests

* Fix oracle views

* Fix formatting

* Fix snowflake qualified components

* Change tests to opt out

* Fix sqlsserver

* Fix postgres test

* Disable h2 describe views test

* Address review
2024-12-04 19:43:25 +00:00
metamben
98e0fce497 Deserialize metrics as cards (#50807) 2024-12-04 19:38:33 +00:00
Jeff Bruemmer
88d5b7bc17 docs - verified content (#50885) 2024-12-04 19:31:28 +00:00
Ryan Laurie
ad3fc72b03 Use global e2e helper import (#50156) 2024-12-04 18:17:10 +00:00
Alexander Polyankin
51b73954e1 Move time filter parts to MBQL lib (#50765) 2024-12-04 12:14:14 -05:00
William
85ce744a2b Applying the underlying records filter to a query with a join display… (#50802)
Applying the underlying records filter to a query with a join displays fields from the join
2024-12-04 17:11:27 +00:00
Phoomparin Mano
33915469e3 feat(sdk): add style and className to static dashboards (#50860)
* add style and className to static dashboards

* unify props for static and interactive
2024-12-05 00:01:38 +07:00
Alexander Polyankin
b85e236113 sql-field-filter-types.cy.spec.js flakes (#50881)
* sql-field-filter-types.cy.spec.js flakes

* sql-field-filter-types.cy.spec.js flakes
2024-12-04 16:43:00 +00:00
Uladzimir Havenchyk
32d8726ac9 chore: drop unused CSS (#50861) 2024-12-04 16:24:50 +00:00
Anton Kulyk
dce32407d4 Reset state when removing last dataset 2024-12-04 15:33:55 +00:00
Chris Truter
72f70ade32 Finish deprecating old search module namespace (#50852) 2024-12-04 15:32:32 +00:00
Alexander Polyankin
80cd4f3c80 Move filter-parts to MBQL lib (#50752) 2024-12-04 10:29:24 -05:00
Anton Kulyk
a05e6805a2 Merge branch 'master' into feature-visualizer 2024-12-04 15:09:57 +00:00
Chris Truter
2e73064e94 [search] replace experimental flag with engine choice (#50850) 2024-12-04 16:08:21 +03:00
Nemanja Glumac
e1f2fb96c3 Trial Banner (#49344)
* Add trial banner

* Use the semantic "warning" color for background

* Simplify the "reappearance" logic

* Refine the banner logic

- Pass the bare minimum information (expiry timestamp) to the `TrialBanner`
- Take timezones into account
- Introduce utilities
- Add unit tests

* Update the remaining tests

* Clean up

* Unbreak my `defsetting` 🎶

* Add comments

* Defer var assignment

* Fix failing tests 🤦

* Adjust the types in order to match `master`

* Move the logic up to the parent component

* Convert the `index` component to TS

* Fix typo in the JSDoc

* Fix another typo

* Make the return statement more readable

* Remove redundant test
2024-12-04 12:49:53 +00:00
Alexander Solovyov
9006696ee3 [serdes] do not require prefix when passing entity ids to export (#50786) 2024-12-04 14:04:53 +02:00
Ngoc Khuat
727246d3b1 fix persist model error email (#50843) 2024-12-04 16:16:00 +07:00
Ngoc Khuat
5e6851a8c3 Add more logs for sending notification (#50836) 2024-12-04 10:38:53 +02:00
Raphael Krut-Landau
83f0c60fea Show /api/search errors in Browse models and Browse metrics (#50713) 2024-12-04 00:00:51 -05:00
Nick Fitzpatrick
414048fba0 Properly handle other users personal collections in collection picker (#50605)
* Properly handle other users personal collections in collection picker

* fixing types and unit test
2024-12-03 22:43:23 -04:00
Romeo Van Snick
2b1c6958c0 Split ACE editor into separate component (#50258)
* Move modals out of resizable area

* Split off ace editor from NativeQueryEditor

* Query for selection target in AceEditor

* Rename editor ref

* Add custom type for selection range

* Move more items into shared types file

* Add shared interface for editors

* Add shared Editor wrapper

* Add SQL colors in core/colors.module.css

* Allow importing global css with .global.css

* Move ace styles to AceEditor.global.css

* Fix Range type

* Fix Ace editor tests

* Scope the ace editor styles

* Remove EditorRoot styled component

* Use parent wrapper instead of classname on editor

* Remove ace editor styles from global styles

* Remove custom ace styles from embedding sdk

* Move NativeQueryEditor styles to css modules

* Fix styles for notebook sql preview

* Remove unused props

* Remove unused variable warnings

* Remove width prop

* Rename openDataReferenceAtQuestion and make it optional

* Rename setNativeEditorSelectedRange and make it optional

* Move selection popover logic out of AceEditor

* Make autocompleteResultsFn optional

* Make cardAutocompleteResultsFn optional

* Remove fetchQuestion prop

* Remove nativeEditorSelectedText prop

* Make props explicit

* Make onChange optional

* Set editor height

* Use Editor in NotebookNativePreview

* Remove cardAutocompleteResultsFn and autocompleteResultsFn props from Editor

* Remove unused snippetCollections prop

* Add missing native-query-autocomplete-match-style setting

* Remove snippets from Editor props

* Resize ace editor on load

* Add custom snippets wrapper

* Flatter completer styles

* Add container class to autocomplete popover

* Fix flex size for editor

* Fix AceEditor types

* Flatten ace styles

* Avoid hard-coded color

* Set height and width on editor

* Flatten ace wrapper

* Do not call api with empty prefix

* Add helpers to target native editor

* Use nativeQueryEditor in action helper

* Use nativeQueryEditor in ad hoc helper

* Remove ace_* references from tests

* Align nativeEditor helpers

* Revert "Align nativeEditor helpers"

This reverts commit e629aecb18.

* Move focusNativeEditor to native editor helpers

* Use focusNativeEditor where applicable

* Remove ace_editor references in test

* Fix test

* Remove references to native-query-editor testid in test

* Remove duplicate test assertion

* Fix nesting in css

* Use font-family from variable

* Use default module.css with :global instead of custom .global.css file

* Move AceEditor into subdirectory

* Fix import path

* More strongly type native-query-autocomplete-match-style

* Remove duplicate default

* Rename EditorHandle to EditorRef

* Add more specificity to ace syntax overwrites

* Simplify EditorRef

* Remove HOC

* Do not perform visibility check inside nativeEditor helper

* Explicitly assert elements are visible

Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>

* Fix formatting issue

* Add highlight for identifier

* Improve gutter cell

* Use relative imports for helpers

* Focus editor before typing

---------

Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>
2024-12-04 12:59:10 +11:00
Noah Moss
590951cacb Ensure user count is fetched within token status memoization (#50813) 2024-12-03 23:18:58 +00:00
Phoomparin Mano
35ec452c6c feat(sdk): rename prop names to be clear and explicit (#50656)
* rename onClose to onCancel

* rename questionHeight to drillThroughQuestionHeight

* rename showVisualizationSelector to withVisualizationSelector

* rename parameter value props

* doc updates for parameter values

* rename the chart type selector prop

* document the withChartTypeSelector prop
2024-12-04 05:19:40 +07:00
Jeff Bruemmer
63ce443b49 docs - commands (#50784) 2024-12-03 17:03:59 -05:00
Nemanja Glumac
44dd5df5da Onboarding checklist - misc. follow ups (#50579)
* Fix the help link

* Update data stack image

* Fix the link appearance
2024-12-03 21:55:27 +00:00
Sloan Sparger
4e8404d3b7 [Clean up] Hide clean up UI in sample collections (#50760)
* hides clean up ui on sample collections

* commit unsaved changes

* pr feedback
2024-12-03 15:51:10 -06:00
Uladzimir Havenchyk
33ff237b59 fix(ObjectDetail): do not show popup when click on View More (#50745) 2024-12-04 00:15:38 +03:00
Nick Fitzpatrick
703638cd10 removing the 'can_write' requirement for selecting things via the question picker (#50783) 2024-12-03 16:21:31 -04:00
Uladzimir Havenchyk
142306a3f7 fix: do not show popup when click on View More (#50745) 2024-12-03 19:47:45 +00:00
Uladzimir Havenchyk
9401a0ba6f fix: do not show popup when click on View More (#50745) 2024-12-03 19:47:31 +00:00
github-actions[bot]
b4aa704be6 Update Translations for v52.1 (#50534)
* Add new strings for v52.0

* Update translations

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-12-03 19:20:18 +00:00
Nemanja Glumac
f5e63b4001 Introduce the non-admin Onboarding Checklist (#50516)
* Add new selectors

* Adjust the onboarding checklist for non-admins

* Add unit tests for non-admins

* Rename the control knob

* Fix the forgotten test

* Stop guarding the route for admins only

* Translate `alt` tags

Co-authored-by: Raphael Krut-Landau <raphael.kl@gmail.com>

* Translate `alt` tags

Co-authored-by: Raphael Krut-Landau <raphael.kl@gmail.com>

* Add e2e coverage for non-admins

* Refine selectors

* Improve the route guard

* Increase E2E coverage for route guards

---------

Co-authored-by: Raphael Krut-Landau <raphael.kl@gmail.com>
2024-12-03 19:23:38 +01:00
Nicolò Pretto
2d9d447492 fix(sdk): introduce .mb-wrapper to scope down our css (#50466)
* add tests for css rules not scoped to the sdk

* fix(sdk): adds mb-wrapper class and scope css to it

* fix typo

* add .mb-wrapper to the body on storybook
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index 52a0d45125d..a4b3a86f4a5 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -42,15 +42,20 @@ const globalStyles = css`
 export const decorators = isEmbeddingSDK
   ? [] // No decorators for Embedding SDK stories, as we want to simulate real use cases
   : [
-      renderStory => (
-        <EmotionCacheProvider>
-          <ThemeProvider>
-            <Global styles={globalStyles} />
-            <CssVariables />
-            {renderStory()}
-          </ThemeProvider>
-        </EmotionCacheProvider>
-      ),
+      renderStory => {
+        if (!document.body.classList.contains("mb-wrapper")) {
+          document.body.classList.add("mb-wrapper");
+        }
+        return (
+          <EmotionCacheProvider>
+            <ThemeProvider>
+              <Global styles={globalStyles} />
+              <CssVariables />
+              {renderStory()}
+            </ThemeProvider>
+          </EmotionCacheProvider>
+        );
+      },
     ];

 function CssVariables() {
2024-12-03 18:19:50 +01:00
Aleksandr Lesnenko
0f9bb8045b fix narrow ordinal line charts have no tooltip (#50715) 2024-12-03 17:02:01 +00:00
Noah Moss
aa0a444be4 Remove memoization of active user counts setting (#50779) 2024-12-03 16:27:53 +00:00
Aleksandr Lesnenko
c7e8d6cae5 Address feedback on Sankey (#50611)
* do not show column selection error on empty datasets

* add more specs to chart-definition logic

* fix entry nodes tooltip and click behavior values

* fix nodes formatting

* do not show sankey data settings on dashboards

* do not initiallt open sankey data columns selects

* hide sankey tooltip on scroll

* drop incorrect types

* set sankey node limit

* prevent clicks on edges close tooltips on native sankey questions

* tweak copy

* update sankey spec according to fixes

* revert changed settings behavior

* fix unit specs
2024-12-03 10:20:24 -05:00
dpsutton
16117c6423 Fix logging with conflict handlers (#50723)
* Fix logging with conflict handlers

Information:
https://github.com/seancorfield/depstar
https://github.com/seancorfield/build-uber-log4j2-handler

Gist is that log4j needs all of it's plugin files merged into one
file. Lots of
`META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat`
files become one.

In our jar currently it is 3247 bytes. In older good jars it is 28463
bytes.

Before:

```
❯ MB_JETTY_PORT=3006 java -jar before.jar
Warning: environ value /Users/dan/.sdkman/candidates/java/current for key :java-home has been overwritten with /Users/dan/.sdkman/candidates/java/21.0.2-tem
2024-12-02T03:58:45.591114Z main ERROR Unrecognized format specifier [d]
2024-12-02T03:58:45.592271Z main ERROR Unrecognized conversion specifier [d] starting at position 16 in conversion pattern.
2024-12-02T03:58:45.592893Z main ERROR Unrecognized format specifier [thread]
2024-12-02T03:58:45.592989Z main ERROR Unrecognized conversion specifier [thread] starting at position 25 in conversion pattern.
2024-12-02T03:58:45.593101Z main ERROR Unrecognized format specifier [level]
2024-12-02T03:58:45.593187Z main ERROR Unrecognized conversion specifier [level] starting at position 35 in conversion pattern.
2024-12-02T03:58:45.593291Z main ERROR Unrecognized format specifier [logger]
2024-12-02T03:58:45.593375Z main ERROR Unrecognized conversion specifier [logger] starting at position 47 in conversion pattern.
2024-12-02T03:58:45.593459Z main ERROR Unrecognized format specifier [msg]
2024-12-02T03:58:45.593541Z main ERROR Unrecognized conversion specifier [msg] starting at position 54 in conversion pattern.
2024-12-02T03:58:45.593628Z main ERROR Unrecognized format specifier [n]
2024-12-02T03:58:45.593710Z main ERROR Unrecognized conversion specifier [n] starting at position 56 in conversion pattern.
2024-12-02T03:58:45.603389Z main ERROR Reconfiguration failed: No configuration found for 'Default' at 'null' in 'null'
WARNING: infinite? already refers to: #'clojure.core/infinite? in namespace: kixi.stats.core, being replaced by: #'kixi.stats.math/infinite?

UPDATE SUMMARY
Run:                        436
Previously run:               0
Filtered out:                52
-------------------------------
Total change sets:          488

FILTERED CHANGE SETS SUMMARY
DBMS mismatch:               52
```

And now after:

```
❯ MB_JETTY_PORT=3007 java -jar after.jar
Warning: environ value /Users/dan/.sdkman/candidates/java/current for key :java-home has been overwritten with /Users/dan/.sdkman/candidates/java/21.0.2-tem
2024-12-01 21:58:49,892 INFO metabase.util :: Maximum memory available to JVM: 8.0 GB
2024-12-01 21:58:51,121 INFO util.encryption :: Saved credentials encryption is DISABLED for this Metabase instance. 🔓
 For more information, see https://metabase.com/docs/latest/operations-guide/encrypting-database-details-at-rest.html
2024-12-01 21:58:51,591 WARN db.env :: WARNING: Using Metabase with an H2 application database is not recommended for production deployments. For production deployments, we highly recommend using Postgres, MySQL, or MariaDB instead. If you decide to continue to use H2, please be sure to back up the database file regularly. For more information, see https://metabase.com/docs/latest/operations-guide/migrating-from-h2.html
WARNING: infinite? already refers to: #'clojure.core/infinite? in namespace: kixi.stats.core, being replaced by: #'kixi.stats.math/infinite?
2024-12-01 21:58:54,353 INFO driver.impl :: Registered abstract driver :sql  🚚
2024-12-01 21:58:54,375 INFO driver.impl :: Registered abstract driver :sql-jdbc (parents: [:sql]) 🚚
2024-12-01 21:58:54,379 INFO metabase.util :: Load driver :sql-jdbc took 7.3 ms
2024-12-01 21:58:54,379 INFO driver.impl :: Registered driver :h2 (parents: [:sql-jdbc]) 🚚
2024-12-01 21:58:54,525 INFO driver.impl :: Registered driver :mysql (parents: [:sql-jdbc]) 🚚
2024-12-01 21:58:54,543 INFO driver.impl :: Registered driver :postgres (parents: [:sql-jdbc]) 🚚
2024-12-01 21:58:54,642 INFO util.namespaces :: Loading namespace: metabase.notification.payload.impl.alert
2024-12-01 21:58:54,646 INFO util.namespaces :: Loading namespace: metabase.notification.payload.impl.dashboard-subscription
2024-12-01 21:58:54,648 INFO util.namespaces :: Loading namespace: metabase.notification.payload.impl.system-event
2024-12-01 21:58:54,655 INFO util.namespaces :: Loading namespace: metabase.channel.impl.email
2024-12-01 21:58:54,663 INFO util.namespaces :: Loading namespace: metabase.channel.impl.http
2024-12-01 21:58:54,667 INFO util.namespaces :: Loading namespace: metabase.channel.impl.slack
2024-12-01 21:58:55,520 INFO metabase.core ::
Metabase v0.52.1-SNAPSHOT (3e7a8a4)

Copyright © 2024 Metabase, Inc.

Metabase Enterprise Edition extensions are NOT PRESENT.
2024-12-01 21:58:55,522 INFO metabase.core :: Starting Metabase in STANDALONE mode
2024-12-01 21:58:55,550 INFO metabase.server :: Launching Embedded Jetty Webserver with config:
 {:port 3007}
```

* formatting fix
2024-12-03 10:08:26 -05:00
adam-james
18bfb20523 Pivot Exports Can Handle nil in Breakout Col, and Multiple Cols of the Same Aggregation Type (#50672)
* Pivot Exports Can Handle `nil` in Breakout Col, and Multiple Cols of the Same Aggregation Type

Fixes #50551

If a pivot table has several measures configured, they might both use the same kind of
aggregation (eg. 'sum'). Previously, this would lead to columns with the same name preventing the pivot-measures from
properly making it through the export post processing. Now, the correct aliased/deduped name is used and all columns
can be properly included in the pivot export.

Also fixes the case where a breakout column (pivot-row) contains `nil` values, which caused the pivot export to
fail. Now, all nil values are grouped and handled appropriately.

* when pivot is disabled, header should still be included

* min and max don't default to 0, add a test for the aggregations
2024-12-03 06:40:59 -08:00
Nemanja Glumac
bf184ac3da Fix a (native-ace-editor) flake 30039 (#50774) 2024-12-03 13:36:21 +01:00
Ngoc Khuat
37acc7e854 [Notification] Rename notification types (#50768)
- notification/alert -> notification/card
- notification/dashboard-subscription -> notification/dashboard
2024-12-03 10:24:23 +00:00
Kamil Mielnik
e2a8d43ed5 Implement new EntityObjectLoaderRtkQuery that uses RTK Query hooks (#44523)
* Add EntityObjectLoaderRtkQuery boilerplate

* Incorporate EntityType logic into the new component

* Improve comment

* Add childrenProps

* Move component into its own directory, extract types to a separate file

* Add entityObjectLoaderRtkQuery and temporary mappings

* Hook up the useGetQuery hook (tables only)

* Pass all the props to children

* Add Playground

* Gather more info in the Playground

* Integrate RTK with redux store

* Adjust props passed down

* Improve typing

* Make the request hook dynamic

* Add missing id in the query

* Handle fetchMetadata

* Change requestType to fetchType

* Add rtk mappings for databases in entity framework

* Use requestType to get request state path

* Enable new component everywhere

* Use legacy loader for entities without rtk mappings

* Improve comment

* Handle fetchMetadataAndForeignTables

* Remove unused function

* Handle reload and remove

* Add TODO

* Add transformResponse option and use it for fetchIdFields

* Format code

* Remove Playground

* Remove unused getUseGetListQuery

* Add rtk mapping for alerts

* Add rtk mapping for dashboards

* Add rtk mapping for groups

* Add rtk mapping for pulses

* Remove options

* Fix permissions tests

* Fix notifications tests

* Add rtk mapping for segments

* Use params instead of body
- It's leftover because this code got reverted after body -> params refactoring for GET endpoint has been merged to master

* Add rtk mapping for snippet collections

* Add TODO

* ESLint

* ESLint

* Update TODO

* Deprecate entity HOCs

* Update TODO

* Revert other entities for now

* Revert other entities for now

* Bring tables back

* Improve todos

* Add comment

* Improve comment

* Remove TODO

* Use reload param

* Remove properties prop

This was used to short-circuit and avoid triggering api calls if we already had an object with given attributes in cache.
With RTK query now managing the cache, we don't want to reproduce this behavior.

* Remove reloadInterval

This is used in EntityListLoader, not EntityObjectLoader

* Don't use setRequestPromise action

* Fix deprecations

* Format code

* Fix flake :(

* Try to fix flake

* Revert "Try to fix flake"

This reverts commit 6408a22a37.

* Try to fix flake

* Fix UseQuery type

* Implement useGetMetadataAndForeignTables

* Fix useMemo and move things down

* Fix typo

* Bring back original hack

* Move file

* Add comment
2024-12-03 08:28:55 +00:00
Raphael Krut-Landau
07ee64373f Make e2e tests of embedding and sql filters date-invariant (#50737) 2024-12-02 23:20:03 -05:00
Nathan Voxland
c63bfcf186 Added nvoxland to team config files (#50761) 2024-12-02 17:03:39 -06:00
William
499553cc74 Add wzimrin to teams and github-slack-map (#50756) 2024-12-02 14:38:28 -06:00
Jeff Bruemmer
8e95128018 update chart list (#50743) 2024-12-02 15:02:03 -05:00
Alex Yarosh
1470cb1abe docs: serialization clarification (#50601)
* serialization clarification

* Apply suggestions from code review

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

---------

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>
2024-12-02 15:01:46 -05:00
Alexander Polyankin
68f645545d Partially fix flaky native e2e tests (#50710) 2024-12-02 12:21:59 -05:00
Anton Kulyk
100c4013d2 Update canvas empty states 2024-11-29 15:13:12 +00:00
Anton Kulyk
b6e1c61fb3 Move columns between pivot wells 2024-11-29 15:10:14 +00:00
Anton Kulyk
ffafee1ee9 Fix tables rendering 2024-11-29 12:12:40 +00:00
Anton Kulyk
9f84294476 Fix trend charts 2024-11-29 12:06:53 +00:00
Anton Kulyk
aaae363ce3 Merge branch 'master' into feature-visualizer 2024-11-28 14:59:31 +00:00
Anton Kulyk
672e6d8862 Clean up columns after removing a data source 2024-11-28 14:59:04 +00:00
Anton Kulyk
302beef76f Merge branch 'master' into feature-visualizer 2024-11-27 13:48:44 +00:00
Anton Kulyk
81d4aff6c7 Fix funnels 2024-11-26 17:39:05 +00:00
Anton Kulyk
b9530848c8 Hide pivot grouping column 2024-11-26 17:24:27 +00:00
Anton Kulyk
302ff52159 Add remove icon to well items 2024-11-26 17:05:13 +00:00
Anton Kulyk
6ec3813e01 Merge branch 'master' into feature-visualizer 2024-11-25 12:59:17 +00:00
Anton Kulyk
1f74334c0d Auto combine bar/area/line charts 2024-11-21 15:11:29 +00:00
Anton Kulyk
ddfcb2ac4a Fix loading state 2024-11-20 17:08:05 +00:00
Anton Kulyk
cc32ab99ae Fix viz settings handling 2024-11-20 16:23:13 +00:00
Anton Kulyk
9b07d9812c Add "Visualize with" button to QB chill mode 2024-11-20 16:02:32 +00:00
Anton Kulyk
f8ec344ee8 Add Urls.visualizer fn 2024-11-20 16:02:19 +00:00
Anton Kulyk
1228d5cd53 Fix cartesian charts 2024-11-20 16:01:55 +00:00
Anton Kulyk
73e7055e81 Support dataSource (ID) query parameter 2024-11-20 15:51:33 +00:00
Anton Kulyk
52e07386cb Make addDataSource accept a data source id 2024-11-20 15:51:13 +00:00
Anton Kulyk
a01beddd2f Add automatic funnel+scalar addition 2024-11-20 15:21:23 +00:00
Anton Kulyk
b0d64b4956 Use names from column refs 2024-11-20 15:21:23 +00:00
Anton Kulyk
108915f231 Temporary remove funnel auto combine 2024-11-20 15:21:23 +00:00
Anton Kulyk
811bc5871c Fix DND for other visualizations 2024-11-20 15:21:23 +00:00
Anton Kulyk
0bcac59912 Use computed settings in well components 2024-11-20 15:21:23 +00:00
Anton Kulyk
f7678aff45 Don't pre-create columns for cartesian charts 2024-11-20 15:21:23 +00:00
Anton Kulyk
2ae5dc679e Fix bad import 2024-11-19 12:18:59 +00:00
Anton Kulyk
223620ab93 Merge branch 'master' into feature-visualizer 2024-11-19 12:11:39 +00:00
Anton Kulyk
93f22e28dd Add basic pie charts support to visualizer (#50195)
* scaffold out basic ui

* wip pie handler

* pie visualizer state working

* Clean up

* Fix pies rendering

* Support multiple pie dimensions

---------

Co-authored-by: Kyle Doherty <5248953+kdoh@users.noreply.github.com>
2024-11-19 12:07:37 +00:00
Anton Kulyk
04f3242041 Fix search 2024-11-14 13:38:44 +00:00
Anton Kulyk
7bb3dce3cc Add back/forward history navigation 2024-11-14 13:26:40 +00:00
Anton Kulyk
f3c579d42c Merge branch 'master' into feature-visualizer 2024-11-13 15:41:58 +00:00
Anton Kulyk
969e109476 Reorg visualizer redux state (#49966)
* Move column makers to utils

* Keep column values mapping in its own map

* Use column names instead of column keys

* Rename type

* Remove `referencedColumns`

* Remove redundant selectors

* Extract selectors into their own file

* Break down `visualizer.slice.ts`
2024-11-13 15:40:53 +00:00
Anton Kulyk
9e348794e8 Temporary remove empty rows checks 2024-11-12 16:25:08 +00:00
Anton Kulyk
187334284d Support pivot tables 2024-11-11 17:30:19 +00:00
Anton Kulyk
93614feb66 Merge branch 'master' into feature-visualizer 2024-11-11 14:32:42 +00:00
Anton Kulyk
28c69f7f9f Visualizer — Basic cartesian charts support (#49618)
* Clean up well components

* Create metric/dimension cols for cartesian charts

* Add hover state to vertical well

* Build a simple cartesian chart with DND

* Keep state when switching between cartesian charts

* Move all DND logic to Redux

* Handle already referenced columns

* Minor column assoc update

* Fix type

* Rework x/y well components

* Build multi series cartesian charts

* DND to remove metrics/dimensions

* Use own settings in well components (vs computed)

* Fix DND logic

* Support scatter charts
2024-11-08 16:10:47 +00:00
Anton Kulyk
4c4fdc5277 Reset visualizer state on unmount 2024-11-06 12:10:15 +00:00
Anton Kulyk
0894fc05b4 Fix type error 2024-11-06 12:09:43 +00:00
Anton Kulyk
2b5051d96b Merge branch 'master' into feature-visualizer 2024-11-06 11:52:38 +00:00
Anton Kulyk
b26c3fd9e9 Clean up StartFromViz 2024-11-06 11:46:55 +00:00
Kyle Doherty
7d9ec78335 start from viz experience (#49412) 2024-11-04 10:30:22 -05:00
Anton Kulyk
6f6a8cb957 Merge branch 'master' into feature-visualizer 2024-11-04 14:37:58 +00:00
Anton Kulyk
fd4681bde8 Fix vertical well 2024-11-01 17:04:33 +00:00
Anton Kulyk
cf2494685c Extract DragOverlayWrapper 2024-11-01 17:04:33 +00:00
Anton Kulyk
54077021b7 Fix dataset column draggable ID 2024-11-01 17:04:33 +00:00
Anton Kulyk
fad96e4275 Reset columns when changing viz type 2024-11-01 17:04:33 +00:00
Anton Kulyk
aba9a695f9 Always add column ref when dispatching importColumn 2024-11-01 17:04:33 +00:00
Anton Kulyk
62f1abb393 Handle duplicate column names 2024-11-01 17:04:33 +00:00
Anton Kulyk
0dd03cbca4 Extract funnel drop handler into its own file 2024-11-01 17:04:33 +00:00
Anton Kulyk
97698a36f7 Extract WellItem component 2024-11-01 17:04:33 +00:00
Anton Kulyk
93398edfb4 Extract visualizer drag handlers 2024-11-01 17:04:32 +00:00
Anton Kulyk
b7d0cb0658 Extract FunnelVerticalWell 2024-11-01 17:04:32 +00:00
Anton Kulyk
c162ae1a2c Visualizer — Build a funnel out of many scalar cards (#49245)
* Add visualization picker

* Add `VizDataSourceMapping`

* Basic DND to make a funnel out of many scalars

* Use map instead of array for `mappings`

* Use column keys instead of column names for mappings

* Rework mapping

* Extract `getVisualizerDataset` selector

* Highlight used columns in the data manager

* Clean up state on removing data source

* Extract utils

* Fix remove data source action

* Rename wells' droppable IDs

* Handle case when there's only one data source

* Rename `importedColumns` to `referencedColumns`

* Combine scalar data sources automatically

* Extract common code
2024-10-31 17:47:28 +00:00
Kyle Doherty
ce8c928661 Visualizer - Stub out header UI (#49395)
* stub out header

* i18n strings
2024-10-31 13:26:48 -04:00
Anton Kulyk
a1f4e00d83 Use VisualizerDataSource instead of Card 2024-10-29 11:35:03 +00:00
Anton Kulyk
dbaf0c2f8d Add VisualizerDataSource type 2024-10-29 11:35:03 +00:00
Anton Kulyk
ea56baeaad Remove selectedCardId 2024-10-28 16:37:01 +00:00
Anton Kulyk
3a65db46a7 Visualizer — Single dataset funnels (#49146)
* Render static bottom well for funnel charts

* Extend redux store to unlock viz settings changes

* Reorder funnel steps with the bottom well

* Minor prop type fix

* Use border radius CSS variable

* Render static vertical well

* Basic drag and drop for swapping metric/dimension

* Move `draggedItem` state to Redux

* Add `getVisualizationType` selector

* Rename `BottomWell` to `HorizontalWell`

* Remove not used file

* Fix type errors

* Rename selector

* Visualizer simple funnel vibe cleanup (#49242)

* play with drag over animation

* bump borders on visualizer zones

* add icon and separator to data importer

* add grabber to column list item

* column list item hover state

* dataset list item styling

* use miw instead of width to prevent jump

* drag overlay style

---------

Co-authored-by: Kyle Doherty <5248953+kdoh@users.noreply.github.com>
2024-10-28 16:36:09 +00:00
Aleksandr Lesnenko
b06b8261d6 filter out non-cards 2024-10-17 13:16:05 -04:00
Aleksandr Lesnenko
af7cba10cc basic data manager 2024-10-17 10:31:18 -04:00
Aleksandr Lesnenko
28af981218 new -> visualization 2024-10-16 10:17:08 -04:00
Aleksandr Lesnenko
2327da2dc8 app skeleton 2024-10-15 19:19:17 -04:00
829 changed files with 57661 additions and 49161 deletions

View File

@@ -345,9 +345,8 @@
metabase.query-analysis.failure-map}
metabase.related #{metabase.related}
metabase.sample-data #{metabase.sample-data}
metabase.search #{metabase.search
;; deprecating naked metabase.search namespace in favor of this.
metabase.search.core
metabase.search #{metabase.search.core
;; Need to expose this submodule, to break circular dependencies with models.
metabase.search.spec}
metabase.server #{metabase.server
metabase.server.handler

View File

@@ -161,6 +161,7 @@
token-features
token-status
toucan-name
trial-banner-dismissal-timestamp
uncached-setting
user-visibility
version

6
.github/team.json vendored
View File

@@ -12,7 +12,8 @@
"metamben",
"lbrdnk",
"appleby",
"uladzimirdev"
"uladzimirdev",
"wzimrin"
]
},
{
@@ -47,7 +48,8 @@
"johnswanson",
"rafpaf",
"sloansparger",
"iethree"
"iethree",
"nvoxland"
]
},
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -3,6 +3,10 @@ import { ThemeProvider } from "metabase/ui";
const isEmbeddingSDK = process.env.IS_EMBEDDING_SDK === "true";
// @ts-expect-error: See metabase/lib/delay
// This will skip the skippable delays in stories
window.METABASE_REMOVE_DELAYS = true;
if (!isEmbeddingSDK) {
require("metabase/css/core/index.css");
require("metabase/css/vendor.css");
@@ -42,15 +46,20 @@ const globalStyles = css`
export const decorators = isEmbeddingSDK
? [] // No decorators for Embedding SDK stories, as we want to simulate real use cases
: [
renderStory => (
<EmotionCacheProvider>
<ThemeProvider>
<Global styles={globalStyles} />
<CssVariables />
{renderStory()}
</ThemeProvider>
</EmotionCacheProvider>
),
renderStory => {
if (!document.body.classList.contains("mb-wrapper")) {
document.body.classList.add("mb-wrapper");
}
return (
<EmotionCacheProvider>
<ThemeProvider>
<Global styles={globalStyles} />
<CssVariables />
{renderStory()}
</ThemeProvider>
</EmotionCacheProvider>
);
},
];
function CssVariables() {

View File

@@ -6,7 +6,8 @@
[clojure.tools.namespace.dependency :as ns.deps]
[clojure.tools.namespace.find :as ns.find]
[clojure.tools.namespace.parse :as ns.parse]
[metabuild-common.core :as u])
[metabuild-common.core :as u]
[org.corfield.log4j2-conflict-handler :refer [log4j2-conflict-handler]])
(:import
(java.io OutputStream)
(java.nio.file Files OpenOption StandardOpenOption)
@@ -133,10 +134,12 @@
(defn- create-uberjar! [basis]
(u/step "Create uberjar"
(with-duration-ms [duration-ms]
(b/uber {:class-dir class-dir
:uber-file uberjar-filename
:basis basis
:exclude dependency-ignore-patterns})
(b/uber {:class-dir class-dir
:uber-file uberjar-filename
;; merge Log4j2Plugins.dat files. (#50721)
:conflict-handlers log4j2-conflict-handler
:basis basis
:exclude dependency-ignore-patterns})
(u/announce "Created uberjar in %.1f seconds." (/ duration-ms 1000.0)))))
(def ^:private manifest-entries

View File

@@ -4,10 +4,12 @@
[build-drivers.common :as c]
[clojure.java.io :as io]
[clojure.tools.build.api :as b]
[clojure.tools.build.tasks.uber] ;; workaround for (#50940)
[clojure.tools.deps.alpha :as deps]
[clojure.tools.deps.alpha.util.dir :as deps.dir]
[colorize.core :as colorize]
[metabuild-common.core :as u]))
[metabuild-common.core :as u]
[org.corfield.log4j2-conflict-handler :refer [log4j2-conflict-handler]]))
(set! *warn-on-reflection* true)
@@ -79,10 +81,12 @@
(u/step (format "Write %s %s uberjar -> %s" driver edition (c/driver-jar-destination-path driver))
(let [start-time-ms (System/currentTimeMillis)]
(b/uber
{:class-dir (c/compiled-source-target-dir driver)
:uber-file (c/driver-jar-destination-path driver)
:basis (uberjar-basis driver edition)
{:class-dir (c/compiled-source-target-dir driver)
:uber-file (c/driver-jar-destination-path driver)
:basis (uberjar-basis driver edition)
;; merge Log4j2Plugins.dat files. (#50721)
:conflict-handlers log4j2-conflict-handler
;; we need to skip this file since on MacOS it conflicts with other licenses, see:
;; https://ask.clojure.org/index.php/13231/switch-tools-build-pull-from-jars-rather-than-exploding-onto
:exclude ["META-INF/LICENSE"]})
:exclude ["META-INF/LICENSE"]})
(u/announce "Created uberjar in %d ms." (- (System/currentTimeMillis) start-time-ms)))))

View File

@@ -667,7 +667,9 @@
io.github.clojure/tools.build {:mvn/version "0.10.6"}
org.clojure/data.xml {:mvn/version "0.2.0-alpha9"}
org.clojure/tools.deps.alpha {:mvn/version "0.15.1254"}
org.fedorahosted.tennera/jgettext {:mvn/version "0.15.1"}}
org.fedorahosted.tennera/jgettext {:mvn/version "0.15.1"}
io.github.seancorfield/build-uber-log4j2-handler {:git/tag "v2.24.0" :git/sha "de93f51"}} ; collect binary files for log4j2
:jvm-opts
["-Dclojure.main.report=stderr"

View File

@@ -132,6 +132,7 @@ Metabase's reference documentation.
- [Data reference](./exploration-and-organization/data-model-reference.md)
- [Events and timelines](./exploration-and-organization/events-and-timelines.md)
- [X-rays](./exploration-and-organization/x-rays.md)
- [Content verification](./exploration-and-organization/content-verification.md)
### People

View File

@@ -43,6 +43,10 @@ Docker:
docker run -d -p 3000:3000 -e MB_SITE_NAME="Awesome Company" --name metabase metabase/metabase
```
## Environment variables on Metabase Cloud
If you're running Metabase Cloud, you can [contact support](https://www.metabase.com/help/premium) to adjust environment variables for your Metabase.
---
## List of environment variables
@@ -107,6 +111,17 @@ Allowed iframe hosts.
Enable the collection of anonymous usage data in order to help Metabase improve.
### `MB_API_KEY`
- Type: string
- Default: `null`
When set, this API key is required for all API requests.
Middleware that enforces validation of the client via the request header X-Metabase-Apikey.
If the header is available, then it’s validated against MB_API_KEY.
When it matches, the request continues; otherwise it’s blocked with a 403 Forbidden response.
### `MB_APPLICATION_COLORS`
> Only available on Metabase [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans.
@@ -382,6 +397,16 @@ The email address you want to use for the sender of emails.
The name you want to use for the sender of emails.
### `MB_EMAIL_MAX_RECIPIENTS_PER_SECOND`
- Type: integer
- Default: `null`
- [Exported as](../installation-and-operation/serialization.md): `email-max-recipients-per-second`.
- [Configuration file name](./config-file.md): `email-max-recipients-per-second`
The maximum number of recipients, summed across emails, that can be sent per second.
Note that the final email sent before reaching the limit is able to exceed it, if it has multiple recipients.
### `MB_EMAIL_REPLY_TO`
- Type: json
@@ -501,6 +526,15 @@ Allow admins to embed Metabase via static embedding?
Allow logging in by email and password.
### `MB_ENABLE_PIVOTED_EXPORTS`
- Type: boolean
- Default: `true`
- [Exported as](../installation-and-operation/serialization.md): `enable-pivoted-exports`.
- [Configuration file name](./config-file.md): `enable-pivoted-exports`
Enable pivoted exports and pivoted subscriptions.
### `MB_ENABLE_PUBLIC_SHARING`
- Type: boolean
@@ -533,13 +567,6 @@ Allow users to explore data using X-rays.
Enumerated field values with cardinality at or below this point are treated as enums in the pseudo-ddl used in some model prompts.
### `MB_EXPERIMENTAL_FULLTEXT_SEARCH_ENABLED`
- Type: boolean
- Default: `false`
Enables search engines which are still in the experimental stage.
### `MB_FOLLOW_UP_EMAIL_SENT`
- Type: boolean
@@ -919,7 +946,7 @@ don't have one.
- [Exported as](../installation-and-operation/serialization.md): `loading-message`.
- [Configuration file name](./config-file.md): `loading-message`
Choose the message to show while a query is running.
Choose the message to show while a query is running. Possible values are "doing-science", "running-query", or "loading-results".
### `MB_LOGIN_PAGE_ILLUSTRATION`
@@ -1377,6 +1404,13 @@ don't have one.
Is SCIM currently enabled?
### `MB_SEARCH_ENGINE`
- Type: keyword
- Default: `:in-place`
Which engine to use when performing search. Supported values are :in-place and :appdb.
### `MB_SEARCH_TYPEAHEAD_ENABLED`
- Type: boolean
@@ -1540,6 +1574,14 @@ This URL is critical for things like SSO authentication, email links, embedding
Bot user OAuth token for connecting the Metabase Slack app. This should be used for all new Slack integrations starting in Metabase v0.42.0.
### `MB_SLACK_BUG_REPORT_CHANNEL`
- Type: string
- Default: `metabase-bugs`
- [Configuration file name](./config-file.md): `slack-bug-report-channel`
The name of the channel where bug reports should be posted.
### `MB_SLACK_FILES_CHANNEL`
- Type: string
@@ -1887,11 +1929,9 @@ Since: v51.3
If `true`, log a stack trace for any connections killed due to exceeding the timeout specified in [MB_JDBC_DATA_WAREHOUSE_UNRETURNED_CONNECTION_TIMEOUT_SECONDS](#mb_jdbc_data_warehouse_unreturned_connection_timeout_seconds).
Note: In addtion to enabling this variable, you need to update the com.mchange log level to INFO or higher via a custom log4j configuration in order to see the stack traces in the logs.
In order to see the stack traces in the logs, you'll also need to update the com.mchange log level to "INFO" or higher via a custom log4j configuration. For configuring log levels, see [Metabase log configuration](./log-configuration.md).
See the [Metabase log configuration](./log-configuration.md) documentation for how to configure log levels.
See [MB_JDBC_DATA_WAREHOUSE_UNRETURNED_CONNECTION_TIMEOUT_SECONDS](#mb_jdbc_data_warehouse_unreturned_connection_timeout_seconds) for setting the timeout after which connections will be killed.
To set a timeout for how long Metabase should wait before it kills unreturned connections, see [MB_JDBC_DATA_WAREHOUSE_UNRETURNED_CONNECTION_TIMEOUT_SECONDS](#mb_jdbc_data_warehouse_unreturned_connection_timeout_seconds).
### `MB_JETTY_ASYNC_RESPONSE_TIMEOUT`
@@ -2165,3 +2205,4 @@ Type: string<br>
Default: `null`
Base-64 encoded public key for this sites SSL certificate. Specify this to enable HTTP Public Key Pinning. Using HPKP is no longer recommended. See http://mzl.la/1EnfqBf for more information.

View File

@@ -293,6 +293,10 @@ For [questions](../questions/start.md), dashboards, and [models](../data-modelin
See [History](../exploration-and-organization/history.md).
## Verifying a dashboard
See [content verification](../exploration-and-organization/content-verification.md).
## Deleting a dashboard
You can move a dashboard to Trash or permanently delete a dashboard that you no longer need.

View File

@@ -21,7 +21,7 @@ You can:
- Add metrics to [dashboards](../dashboards/introduction.md).
- View metrics in the [database browser](../exploration-and-organization/exploration.md#browse-your-databases).
- Pick metrics as aggregation columns in the Summarize block when creating questions, modify them or combine.
- [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans include the ability to mark metrics as verified.
- [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans include the ability to [mark metrics as verified](../exploration-and-organization/content-verification.md).
### Metrics in the query builder
@@ -86,6 +86,10 @@ By "editing" a metric, we mean [editing the metric's query definition](#editing-
These collection permissions also interact with the group's [data permissions](../permissions/data.md), which define whether the group can view or query the metric's source data.
## Verifying a metric
See [content verification](../exploration-and-organization/content-verification.md).
## Further reading
- [Models](./models.md)

View File

@@ -174,9 +174,7 @@ See [Deleting and restoring items](../exploration-and-organization/delete-and-re
## Verifying a model
{% include plans-blockquote.html feature="Model verification" %}
Just like with a question, admins can verify models. Verifying a model will give it a check mark to let others know an admin vetted the model. If anyone makes any changes to the model, the check mark will disappear. An admin will have to verify the question again to restore the check mark.
See [content verification](../exploration-and-organization/content-verification.md).
## Model persistence

View File

@@ -22,15 +22,15 @@ You can embed a dashboard using the one of the dashboard components:
## Dashboard component props
| Prop | Type | Description |
| ---------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Prop | Type | Description |
|-------------------------------| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| dashboardId | `number \| string` | The ID of the dashboard. This is either:<br>- the numerical ID when accessing a dashboard link, i.e. `http://localhost:3000/dashboard/1-my-dashboard` where the ID is `1`<br>- the string ID found in the `entity_id` key of the dashboard object when using the API directly or using the SDK Collection Browser to return data |
| initialParameterValues | `Record<string, string \| string[]>` | Query parameters for the dashboard. For a single option, use a `string` value, and use a list of strings for multiple options. |
| initialSqlParameters | `Record<string, string \| string[]>` | Query parameters for the dashboard. For a single option, use a `string` value, and use a list of strings for multiple options. |
| withTitle | `boolean` | Whether the dashboard should display a title. |
| withCardTitle | `boolean` | Whether the dashboard cards should display a title. |
| withDownloads | `boolean \| null` | Whether to hide the download button. |
| hiddenParameters | `string[] \| null` | A list of [parameters to hide](../../questions/sharing/public-links.md#appearance-parameters). |
| questionHeight\* | `number \| null` | Height of a question component when drilled from the dashboard to a question level. |
| drillThroughQuestionHeight\* | `number \| null` | Height of a question component when drilled from the dashboard to a question level. |
| questionPlugins\* | `{ mapQuestionClickActions: Function } \| null` | Additional mapper function to override or add drill-down menu. See the implementing custom actions section for more details. |
| onLoad | `(dashboard: Dashboard \| null) => void` | Event handler that triggers after dashboard loads with all visible cards and their content. |
| onLoadWithoutCards | `(dashboard: Dashboard \| null) => void` | Event handler that triggers after dashboard loads, but without its cards - at this stage dashboard title, tabs and cards grid is rendered, but cards content is not yet loaded. |
@@ -60,7 +60,7 @@ const config = {...}
export default function App() {
const dashboardId = 1; // This is the dashboard ID you want to embed
const initialParameterValues = {}; // Define your query parameters here
const initialSqlParameters = {}; // Define your query parameters here
// choose parameter names that are in your dashboard
const hiddenParameters = ["location", "city"]
@@ -69,7 +69,7 @@ export default function App() {
<MetabaseProvider config={config}>
<InteractiveDashboard
dashboardId={dashboardId}
initialParameterValues={initialParameterValues}
initialSqlParameters={initialSqlParameters}
withTitle={false}
withDownloads={false}
hiddenParameters={hideParameters}

View File

@@ -31,17 +31,17 @@ export default function App() {
return (
<MetabaseProvider config={config}>
<StaticQuestion questionId={questionId} showVisualizationSelector={false}/>
<StaticQuestion questionId={questionId} withChartTypeSelector={false}/>
</MetabaseProvider>
);
}
```
You can pass parameter values to questions defined with SQL via `parameterValues` prop, in the format of `{parameter_name: parameter_value}`. Learn more about [SQL parameters](../../questions/native-editor/sql-parameters.md).
You can pass parameter values to questions defined with SQL via `initialSqlParameters` prop, in the format of `{parameter_name: parameter_value}`. Learn more about [SQL parameters](../../questions/native-editor/sql-parameters.md).
```jsx
{% raw %}
<StaticQuestion questionId={questionId} parameterValues={{ product_id: 50 }} />
<StaticQuestion questionId={questionId} initialSqlParameters={{ product_id: 50 }} />
{% endraw %}
```

View File

@@ -30,7 +30,7 @@ Metabase admins can designate collections as "official" with the following effec
- Questions in Official collections added to Dashboards that are not in Official collections will show an Official badge next to their name on the Dashboard.
- Questions and dashboards in Official collections are also more likely to show up at the top of search results.
Pairing Official badges with [verified items](./exploration.md#verified-items) can help everyone in your Metabase sort out which questions people can trust.
Pairing Official badges with [verified items](./content-verification.md) can help everyone in your Metabase sort out which questions people can trust.
To add an Official badge to a collection, an admin can visit the collection and click on the dot dot dot menu (**...**) and select **Make collection official**. Admins can also remove an Official badge in the same menu. Admins can also mark a collection as Official or not when they first create the collection.

View File

@@ -0,0 +1,54 @@
---
title: Content verification
---
# Content verification
{% include plans-blockquote.html feature="Content verification" %}
![Verified icon](./images/verified-icon.png)
Admins can verify items to let others know that the content is accurate and up to date.
## Benefits of verifying an item
- Verified items get a fancy blue checkmark next to them.
- They show up higher in search suggestions and results.
- The models and metrics browsers allow you to filter for verified items.
![Verified models toggle](./images/verified-only.png)
## Verifying items
To verify an item, an admin can click on the three-dot menu (**...**) in the upper right when viewing that item and select: **Verify this item**.
![Verify this dashboard](./images/verify-this-dashboard.png)
## Removing verification status
To remove verification from an item, admins can:
1. Visit the item.
2. Click on the three-dot menu (**...**).
3. Select **remove verification**.
## Changes to queries require re-verification
If anyone makes any changes to a question, metric, or model's _query_, Metabase will remove the verification status (the check mark will disappear). The logic here is that changing the results in any way should require re-verification.
Dashboards verification works a little differently (since there's no underlying query). Dashboard verification is sticky; you can modify a verified dashboard and it'll retain its verified status.
## Verifiable items
Admins can verify the following items:
- [Questions](../questions/start.md)
- [Models](../data-modeling/models.md)
- [Metrics](../data-modeling/metrics.md)
- [Dashboards](../dashboards/introduction.md)
Dashboard verification status has no affect on its questions. Questions must be verified independently.
## Further reading
- [Official collections](./collections.md#official-collections)

View File

@@ -41,7 +41,7 @@ You can search by:
- **Last editor**: who made the last saved change on the thing.
- **Creation date**: when the thing entered our universe.
- **Last edit date**: when someone last cared enough to update the thing.
- **Verified items only**. Only show items that [have been verified](#verified-items).
- **Verified items only**. Only show items that [have been verified](./content-verification.md).
- **Search the contents of native queries**: search through the SQL/native code in questions.
- **Search items in trash**: include the depths of oblivion in your search.
@@ -53,7 +53,7 @@ You can search by:
The left sidebar lists your databases, [models](../data-modeling/models.md), and [metrics](../data-modeling/metrics.md).
[Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans include the ability to filter for [verified models and metrics](#verified-items).
[Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans include the ability to filter for [verified models and metrics](./content-verification.md).
The database browser will list all the databases connected to your Metabase. Hover over a table and click on the **bolt** icon to [X-ray](x-rays.md) the table, or click on the **book** icon to view more info about the table: its fields and their descriptions (if any), what questions are based on that table, and more.
@@ -122,18 +122,6 @@ Some things to remember with bookmarks:
- Items that you bookmark will get a boost in your search results (but not the search results of other people).
- To reorder bookmarks, simply drag and drop them in the sidebar.
## Verified items
{% include plans-blockquote.html feature="Verification" %}
Verified questions and models are marked with a blue checkmark icon:
![Verified icon](./images/verified-icon.png)
Administrators can **Verify** a question, [model](../data-modeling/models.md), or [metric](../data-modeling/metrics.md) from the three dot menu (`...`) to signal that they've reviewed the item and deemed it to be trustworthy. That is: the question or model is filtering the right columns, summarizing the right metrics, and querying records from the right tables. Verified items are more likely to show up higher in search suggestions and search results.
If someone modifies a verified question, the question will lose its verified status, and an administrator will need to review and verify the question again to restore its verified status.
[collections]: ./collections.md
[dashboards]: ../dashboards/start.md
[models]: ../data-modeling/models.md

View File

@@ -13,7 +13,7 @@ For questions, dashboards, and models, Metabase keeps a version history for the
3. Click on the **History** tab.
4. The History tab will display the item's history of up to 15 versions.
Metabase will keep track of a version each time you [save](../questions/sharing/answers.md#how-to-save-a-question), [move](../questions/sharing/answers.md#editing-your-question), [revert](#reverting-to-previous-versions), [move to Trash](./delete-and-restore.md), or [verify](./exploration.md#verified-items) an item.
Metabase will keep track of a version each time you [save](../questions/sharing/answers.md#how-to-save-a-question), [move](../questions/sharing/answers.md#editing-your-question), [revert](#reverting-to-previous-versions), [move to Trash](./delete-and-restore.md), or [verify](./content-verification.md) an item.
## Reverting to previous versions

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -33,3 +33,7 @@ Add events to timelines to annotate charts.
## [X-ray your data](./x-rays.md)
X-rays are a fast and easy way to get automatic insights and explorations of your data.
## [Content verification](./content-verification.md)
Admins can verify items to let people know the data is accurate and up to date.

View File

@@ -16,11 +16,9 @@ Metabase will print out the help text for available commands.
Generate a markdown file containing documentation for all API endpoints. This is written to a file called `docs/api-documentation.md`.
## `driver-methods`
## `driver-methods` | `driver-methods _docs`
Or `driver-methods _docs`.
Print a list of all multimethods available for a driver to implement, optionally with their docstrings.
Print a list of all multimethods available for a driver to implement. Add `_docs` to include their docstrings.
## `drop-entity-ids`
@@ -28,29 +26,28 @@ Drop entity IDs for instances of serializable models. Useful for migrating from
## `dump path & options`
> This command is deprecated. Use `export` instead.
**Note: this command is deprecated. Use `export` instead.**
Serializes Metabase instance into directory `path`.
**Options:**
Options:
`-u`, `--user EMAIL` Export collections owned by the specified user
`-s`, `--state (active|all) all` When set to `active`, do not dump archived entities. Default behavior is `all`.
- `-u, --user EMAIL` - Export collections owned by the specified user
- `-s, --state (active|all)` - When set to `active`, do not dump archived entities. Default behavior is `all`
- `--include-entity-id` - Include entity_id property in all dumped entities. Default: false
## `dump-to-h2 h2-filename & opts`
Transfer data from existing database to newly created H2 DB with specified filename. Target H2 file is deleted before dump, unless the --keep-existing flag is given.
**Options:**
Options:
`-k`, `--keep-existing` Do not delete target H2 file if it exists.
`-p`, `--dump-plaintext` Do not encrypt dumped contents.
- `-k, --keep-existing` - Do not delete target H2 file if it exists
- `-p, --dump-plaintext` - Do not encrypt dumped contents
## `environment-variables-documentation`
Generates a markdown file containing documentation for environment variables relevant to configuring Metabase. The command only includes environment variables registered as defsettings. For a full list of environment variables, see [Environment variables](https://www.metabase.com/docs/latest/configuring-metabase/environment-variables).
Generates a markdown file containing documentation for environment variables relevant to configuring Metabase. The command only includes environment variables registered as defsettings. For a full list of environment variables, see https://www.metabase.com/docs/latest/configuring-metabase/environment-variables.
## `export path & options`
@@ -58,53 +55,50 @@ Generates a markdown file containing documentation for environment variables rel
Serialize Metabase instance into directory at `path`.
**Options:**
Options:
`-c`, `--collection ID` Export only specified ID(s). Use commas to separate multiple IDs.
- `-c, --collection ID` - Export only specified ID(s). Use commas to separate multiple IDs. You can pass entity ids with `eid:<...>` as a prefix
- `-C, --no-collections` - Do not export any content in collections
- `-S, --no-settings` - Do not export settings.yaml
- `-D, --no-data-model` - Do not export any data model entities; useful for subsequent exports
- `-f, --include-field-values` - Include field values along with field metadata
- `-s, --include-database-secrets` - Include database connection details (in plain text; use caution)
- `-e, --continue-on-error` - Do not break execution on errors
- `--full-stacktrace` - Output full stacktraces on errors
`-C`, `--no-collections` Do not export any content in collections.
## `help command-name` | `help`
`-S`, `--no-settings` Do not export settings.yaml
`-D`, `--no-data-model` Do not export any data model entities; useful for subsequent exports.
`-f`, `--include-field-values` Include field values along with field metadata.
`-s`, `--include-database-secrets` Include database connection details (in plain text; use caution).
## `help`
Show this help message listing valid Metabase commands.
Show this help message listing valid Metabase commands. Use `help command-name` for specific command details.
## `import path & options`
{% include plans-blockquote.html feature="Serialization" self-hosted-only="true" %}
Load serialized Metabase instance as created by the export command from directory `path`. Has no options.
Load serialized Metabase instance as created by the `export` command from directory `path`.
Options:
- `-e, --continue-on-error` - Do not break execution on errors
- `--full-stacktrace` - Output full stacktraces on errors
## `load path & options`
> This command is deprecated. Use `import` instead.
**Note: this command is deprecated. Use `import` instead.**
Load serialized Metabase instance as created by [[dump]] command from directory `path`.
Load serialized Metabase instance as created by `dump` command from directory `path`.
**Options:**
Options:
`-m`, `--mode (skip|update) skip` Update or skip on conflicts.
- `-m, --mode (skip|update)` - Update or skip on conflicts. Default: skip
- `-e, --on-error (continue|abort)` - Abort or continue on error. Default: continue
`-e`, `--on-error (continue|abort)` continue Abort or continue on error.
## `load-from-h2`
## `load-from-h2` | `load-from-h2 h2-connection-string`
Transfer data from existing H2 database to the newly created MySQL or Postgres DB specified by env vars.
```
load-from-h2 h2-connection-string
```
## `migrate direction`
## `migrate down`
Used for downgrading versions.
Run database migrations. Valid options for `direction` are `up`, `force`, `down`, `print`, or `release-locks`.
## `profile`
@@ -116,8 +110,22 @@ Reset the password for a user with `email-address`.
## `rotate-encryption-key new-key`
Rotate the encryption key of a Metabase database. The `MB_ENCRYPTION_SECRET_KEY` environment variable has to be set to the current key, and the parameter `new-key` has to be the new key. `new-key` has to be at least 16 chars.
Rotate the encryption key of a metabase database. The MB_ENCRYPTION_SECRET_KEY environment variable has to be set to the current key, and the parameter `new-key` has to be the new key. `new-key` has to be at least 16 chars.
## `seed-entity-ids`
Add entity IDs for instances of serializable models that don't already have them.
## `version`
Print version information about Metabase and the current system.
## Additional useful commands
### H2 SQL Shell
Open an SQL shell for the Metabase H2 DB:
```sh
java -cp metabase.jar org.h2.tools.Shell -url jdbc:h2:/path/to/metabase.db
```

View File

@@ -46,7 +46,7 @@ Check out our guides for:
### What gets exported
Metabase will only include some artifacts in its exports:
Metabase will only export the following entities:
- Collections (but personal collections don't get exported unless explicitly specified them through [export options](#customize-what-gets-exported))
- Dashboards
@@ -60,8 +60,9 @@ Metabase will only include some artifacts in its exports:
- Public sharing settings for questions and dashboards
- [General Metabase settings](#general-metabase-settings-that-are-exported)
- Events and timelines
- Database connection strings (only if specified through [export options](#customize-what-gets-exported))
(#customize-what-gets-exported).
- Database connection strings (only if specified through [export options](#customize-what-gets-exported)).
All other entities—including users, groups, permissions, alerts, subscriptions—won't get exported.
Metabase will export its artifacts to a directory of YAML files. The export includes:

View File

@@ -40,41 +40,36 @@ Pro and Enterprise plans include more ways to manage permissions, including data
## People and group management
- [Group managers](../people-and-groups/managing.md#group-managers)
Delegate group membership managemnet by assigning [group managers](../people-and-groups/managing.md#group-managers).
## Interactive embedding
## Appearance
You can embed all of Metabase in your app.
[White-label Metabase's appearance](../configuring-metabase/appearance.md).
- [Embedding the entire Metabase app in your app](../embedding/interactive-embedding.md)
- [Customizing Metabase's appearance](../configuring-metabase/appearance.md)
## Embedding
Some embedding tools are paid features.
### Interactive embedding
[Embed the entire Metabase app in your app](../embedding/interactive-embedding.md).
### Embedded analytics SDK (BETA)
[Embed Metabase components in your app](../embedding/sdk/introduction.md).
## Dashboard subscription and alert customization
### Custom filter values
Send different groups of people the contents of a dashboard with different filters applied. You only need to maintain one dashboard, which you can use to send results relevant to each subscriber.
- [Customizing filter values for each dashboard subscription](../dashboards/subscriptions.md)
### Restrict which domains people can send alerts and subscriptions to
As an additional security layer, you can whitelist domains, which restricts people from sending alerts and subscriptions to email addresses that don't use an approved domain.
- [Approved domains for notifications](../configuring-metabase/email.md#approved-domains-for-notifications)
### Suggest recipients on dashboard subscriptions and alerts
You can also control which recipients Metabase suggests when people create dashboard subscriptions and alert.
- [Recipient suggestion controls](../configuring-metabase/email.md#suggest-recipients-on-dashboard-subscriptions-and-alerts)
- [Customizing filter values for each dashboard subscription](../dashboards/subscriptions.md).
- [Approved domains for notifications](../configuring-metabase/email.md#approved-domains-for-notifications).
- [Recipient suggestion controls](../configuring-metabase/email.md#suggest-recipients-on-dashboard-subscriptions-and-alerts).
## Content moderation tools
Tools for keeping your Metabase organized, so people can find your most important, verified items.
- [Official collections](../exploration-and-organization/collections.md#official-collections)
- [Verified items](../exploration-and-organization/exploration.md#verified-items)
- [Verified items](../exploration-and-organization/content-verification.md)
## Advanced caching controls
@@ -86,13 +81,11 @@ All Metabase editions include global caching controls. Pro and Enterprise plans
## Model persistence for individual models
- [Toggle persistence for individual models](../data-modeling/model-persistence.md#turn-on-model-persistence-for-individual-models)
[Toggle persistence for individual models](../data-modeling/model-persistence.md#turn-on-model-persistence-for-individual-models).
## Usage analytics
See how people are using your Metabase.
- [Usage analytics](../usage-and-performance-tools/usage-analytics.md)
[See how people are using your Metabase](../usage-and-performance-tools/usage-analytics.md).
## Admin tools
@@ -103,9 +96,7 @@ See which queries are failing to help keep your Metabase tidy.
## Serialization
You can export Metabase application data and use that to spin up new instances preloaded with questions, dashboards, and collections.
- [Serialization](../installation-and-operation/serialization.md)
You can [export Metabase application data](../installation-and-operation/serialization.md) and use that to spin up new instances preloaded with questions, dashboards, and collections.
## Configuration file

View File

@@ -44,7 +44,7 @@ Click into the question's title to edit the name of your question.
Open the **three dot** (...) menu to:
- [Verify](../../exploration-and-organization/exploration.md#verified-items) this question
- [Verify](../../exploration-and-organization/content-verification.md) this question
- Add to [dashboard](../../dashboards/start.md)
- Move to another [collection](../../exploration-and-organization/collections.md)
- Turn into a [model](../../data-modeling/models.md)

View File

@@ -85,7 +85,7 @@ The [Numbers](./visualizations/numbers.md) option is for displaying a single num
A [pie chart or donut chart](./visualizations/pie-or-donut-chart.md) can be used when breaking out a metric by a single dimension, especially when the number of possible breakouts is small, like accounts by plan.
A [sunburst chart](./visualizations/pie-or-donut-chart.md) chart is a pie chart with more than one ring to show the data broken out by additional dimensions.
A [sunburst chart](./visualizations/pie-or-donut-chart.md) is a pie chart with more than one ring to show the data broken out by additional dimensions.
![Donut](../images/pie-sunburst-demo.png)

View File

@@ -19,12 +19,15 @@ To see what's new, check out all the [major release announcements](https://www.m
## Metabase Enterprise Edition releases
- [v1.52.1](https://github.com/metabase/metabase/releases/tag/v1.52.1)
- [v1.51.7](https://github.com/metabase/metabase/releases/tag/v1.51.7)
- [v1.51.6](https://github.com/metabase/metabase/releases/tag/v1.51.6)
- [v1.51.5](https://github.com/metabase/metabase/releases/tag/v1.51.5)
- [v1.51.4](https://github.com/metabase/metabase/releases/tag/v1.51.4)
- [v1.51.3](https://github.com/metabase/metabase/releases/tag/v1.51.3)
- [v1.51.2](https://github.com/metabase/metabase/releases/tag/v1.51.2)
- [v1.51.1](https://github.com/metabase/metabase/releases/tag/v1.51.1)
- [v1.50.35](https://github.com/metabase/metabase/releases/tag/v1.50.35)
- [v1.50.34](https://github.com/metabase/metabase/releases/tag/v1.50.34)
- [v1.50.33](https://github.com/metabase/metabase/releases/tag/v1.50.33)
- [v1.50.32](https://github.com/metabase/metabase/releases/tag/v1.50.32)
@@ -219,12 +222,15 @@ To see what's new, check out all the [major release announcements](https://www.m
## Metabase Open Source Edition releases
- [v0.52.1](https://github.com/metabase/metabase/releases/tag/v0.52.1)
- [v0.51.7](https://github.com/metabase/metabase/releases/tag/v0.51.7)
- [v0.51.6](https://github.com/metabase/metabase/releases/tag/v0.51.6)
- [v0.51.5](https://github.com/metabase/metabase/releases/tag/v0.51.5)
- [v0.51.4](https://github.com/metabase/metabase/releases/tag/v0.51.4)
- [v0.51.3](https://github.com/metabase/metabase/releases/tag/v0.51.3)
- [v0.51.2](https://github.com/metabase/metabase/releases/tag/v0.51.2)
- [v0.51.1](https://github.com/metabase/metabase/releases/tag/v0.51.1)
- [v0.50.35](https://github.com/metabase/metabase/releases/tag/v0.50.35)
- [v0.50.34](https://github.com/metabase/metabase/releases/tag/v0.50.34)
- [v0.50.33](https://github.com/metabase/metabase/releases/tag/v0.50.33)
- [v0.50.32](https://github.com/metabase/metabase/releases/tag/v0.50.32)

View File

@@ -1,5 +1,7 @@
import { capitalize } from "inflection";
import { nativeEditor } from "./e2e-native-editor-helpers";
export function setActionsEnabledForDB(dbId, enabled = true) {
return cy.request("PUT", `/api/database/${dbId}`, {
settings: {
@@ -10,7 +12,7 @@ export function setActionsEnabledForDB(dbId, enabled = true) {
export function fillActionQuery(query) {
// Without this wait, content tends to drop from the beginning of the string. TODO: Fix
cy.get(".ace_content:visible").wait(500).type(query, {
nativeEditor().wait(500).type(query, {
parseSpecialCharSequences: false,
delay: 50,
});

View File

@@ -1,5 +1,7 @@
import { SAMPLE_DB_ID, SAMPLE_DB_TABLES } from "e2e/support/cypress_data";
import { runNativeQuery } from "e2e/support/helpers/e2e-misc-helpers";
import { runNativeQuery } from "./e2e-misc-helpers";
import { nativeEditor } from "./e2e-native-editor-helpers";
const {
STATIC_ORDERS_ID,
@@ -88,8 +90,7 @@ export function startNewNativeQuestion(config) {
cy.visit("/question#" + hash);
cy.get(".ace_content").should("be.visible");
cy.findAllByTestId("loading-indicator").should("not.exist");
return nativeEditor();
}
/**
@@ -100,8 +101,7 @@ export function startNewNativeModel(config) {
cy.visit("/model/query#" + hash);
cy.get(".ace_content").should("be.visible");
cy.findAllByTestId("loading-indicator").should("not.exist");
return nativeEditor();
}
/**

View File

@@ -1,3 +1,5 @@
import { focusNativeEditor } from "./e2e-native-editor-helpers";
// Find a text field by label text, type it in, then blur the field.
// Commonly used in our Admin section as we auto-save settings.
export function typeAndBlurUsingLabel(label, value) {
@@ -42,17 +44,6 @@ export function openNativeEditor({
return focusNativeEditor().as(alias);
}
export function focusNativeEditor() {
cy.findByTestId("native-query-editor")
.should("be.visible")
.should("have.class", "ace_editor")
.click();
return cy
.findByTestId("native-query-editor")
.should("have.class", "ace_focus");
}
/**
* Executes native query and waits for the results to load.
* Makes sure that the question is not "dirty" after the query successfully ran.

View File

@@ -0,0 +1,20 @@
export function nativeEditor() {
cy.findAllByTestId("loading-indicator").should("not.exist");
return cy.get("[data-testid=native-query-editor] .ace_content");
}
export function focusNativeEditor() {
nativeEditor().should("be.visible").click();
return cy
.findByTestId("native-query-editor")
.should("have.class", "ace_focus");
}
export function blurNativeEditor() {
cy.get(".ace_text-input").blur();
}
export function nativeEditorCompletions() {
return cy.get(".ace_autocomplete").should("be.visible");
}

View File

@@ -20,6 +20,7 @@ export * from "./e2e-ldap-helpers";
export * from "./e2e-misc-helpers";
export * from "./e2e-mock-app-settings-helpers";
export * from "./e2e-models-metadata-helpers";
export * from "./e2e-native-editor-helpers";
export * from "./e2e-notebook-helpers";
export * from "./e2e-notification-helpers";
export * from "./e2e-permissions-helpers";

4
e2e/support/index.ts Normal file
View File

@@ -0,0 +1,4 @@
// H is for helpers 🤗
import * as H from "./helpers";
export { H };

View File

@@ -1,5 +1,5 @@
import { H } from "e2e/support";
import { USERS } from "e2e/support/cypress_data";
import { restore, visitDashboard } from "e2e/support/helpers";
import { setup } from "./visit-dashboard";
@@ -7,7 +7,7 @@ describe("visitDashboard e2e helper", () => {
Object.keys(Cypress._.omit(USERS, "sandboxed")).forEach(user => {
context(`${user.toUpperCase()}`, () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setup();
@@ -18,27 +18,27 @@ describe("visitDashboard e2e helper", () => {
});
it("should work on an empty dashboard", () => {
visitDashboard("@emptyDashboard");
H.visitDashboard("@emptyDashboard");
});
it("should work on a dashboard with markdown card", () => {
visitDashboard("@markdownOnly");
H.visitDashboard("@markdownOnly");
});
it("should work on a dashboard with a model", () => {
visitDashboard("@modelDashboard");
H.visitDashboard("@modelDashboard");
});
it("should work on a dashboard with a GUI question", () => {
visitDashboard("@guiDashboard");
H.visitDashboard("@guiDashboard");
});
it("should work on a dashboard with a native question", () => {
visitDashboard("@nativeDashboard");
H.visitDashboard("@nativeDashboard");
});
it("should work on a dashboard with multiple cards (including markdown, models, pivot tables, GUI and native)", () => {
visitDashboard("@multiDashboard");
H.visitDashboard("@multiDashboard");
});
});
});

View File

@@ -236,7 +236,7 @@ describeEE("scenarios > embedding-sdk > interactive-question", () => {
{isSaveModalOpen && (
<Modal data-testid="modal" opened={isSaveModalOpen} onClose={close}>
<InteractiveQuestion.SaveQuestionForm onClose={close} />
<InteractiveQuestion.SaveQuestionForm onCancel={close} />
</Modal>
)}

View File

@@ -271,8 +271,68 @@ describeEE("scenarios > embedding-sdk > styles", () => {
// TODO: good place for a visual regression test
});
});
describe("styles should not leak outside of the provider", () => {
const elements = [
{ tag: "body", jsx: undefined }, // no need to render anything specific, the body tag is rendered by cypress
{ tag: "h1", jsx: <h1>h1 tag text</h1> },
{ tag: "h2", jsx: <h2>h2 tag text</h2> },
{ tag: "h3", jsx: <h3>h3 tag text</h3> },
{ tag: "p", jsx: <p>p tag text</p> },
{ tag: "button", jsx: <button>button tag text</button> },
{ tag: "input", jsx: <input placeholder="input tag" type="text" /> },
{ tag: "div", jsx: <div>div tag text</div> },
{ tag: "span", jsx: <span>span tag text</span> },
{ tag: "label", jsx: <label>label tag text</label> },
{ tag: "select", jsx: <select>select tag text</select> },
{ tag: "textarea", jsx: <textarea>textarea tag text</textarea> },
];
it(`no css rule should match ${elements.map(e => e.tag).join(", ")} outside of the provider`, () => {
cy.mount(
<div>
{elements.map(({ jsx }) => jsx)}
<MetabaseProvider config={DEFAULT_SDK_PROVIDER_CONFIG}>
<StaticQuestion questionId={ORDERS_QUESTION_ID} />
</MetabaseProvider>
</div>,
);
// wait for the question to load, to make sure our bundle and styles have loaded
getSdkRoot().findByText("Product ID").should("exist");
for (const { tag } of elements) {
expectElementToHaveNoAppliedCssRules(tag);
}
});
});
});
const expectElementToHaveNoAppliedCssRules = (selector: string) => {
cy.get(selector).then($el => {
const rules = getCssRulesThatApplyToElement($el);
if (rules.length > 0) {
console.warn("rules matching", selector, rules);
}
expect(rules, `No css rules should match ${selector}`).to.be.empty;
});
};
const getCssRulesThatApplyToElement = ($element: JQuery<HTMLElement>) => {
const element = $element[0];
const rulesThatMatch: CSSStyleRule[] = Array.from(
document.styleSheets,
).flatMap(sheet => {
const cssRules = Array.from(sheet.cssRules).filter(
rule => rule instanceof CSSStyleRule,
) as CSSStyleRule[];
return cssRules.filter(rule => element.matches(rule.selectorText));
});
return rulesThatMatch;
};
function wrapBrowserDefaultFont() {
cy.mount(<p>paragraph with default browser font</p>);

View File

@@ -1,17 +1,7 @@
import moment from "moment-timezone"; // eslint-disable-line no-restricted-imports -- deprecated usage
import { H } from "e2e/support";
import { USER_GROUPS, WRITABLE_DB_ID } from "e2e/support/cypress_data";
import {
createImplicitActions,
createModelFromTableName,
popover,
resetTestTable,
restore,
resyncDatabase,
undoToastList,
visitDashboard,
visitModel,
} from "e2e/support/helpers";
const WRITABLE_TEST_TABLE = "scoreboard_actions";
const FIRST_SCORE_ROW_ID = 11;
@@ -37,8 +27,8 @@ describe(
"prefetchValues",
);
resetTestTable({ type: "postgres", table: WRITABLE_TEST_TABLE });
restore("postgres-writable");
H.resetTestTable({ type: "postgres", table: WRITABLE_TEST_TABLE });
H.restore("postgres-writable");
asAdmin(() => {
cy.updatePermissionsGraph({
[ALL_USERS_GROUP]: {
@@ -49,12 +39,12 @@ describe(
},
});
resyncDatabase({
H.resyncDatabase({
dbId: WRITABLE_DB_ID,
tableName: WRITABLE_TEST_TABLE,
});
createModelFromTableName({
H.createModelFromTableName({
tableName: WRITABLE_TEST_TABLE,
idAlias: "modelId",
});
@@ -65,7 +55,7 @@ describe(
beforeEach(() => {
asAdmin(() => {
cy.get("@modelId").then(modelId => {
createImplicitActions({ modelId });
H.createImplicitActions({ modelId });
cy.createQuestionAndDashboard({
questionDetails: {
@@ -86,7 +76,7 @@ describe(
it("does not show model actions in model visualization on a dashboard", () => {
asAdmin(() => {
visitDashboard("@dashboardId");
H.visitDashboard("@dashboardId");
cy.findByTestId("dashcard").within(() => {
assertActionsDropdownNotExists();
@@ -125,7 +115,7 @@ describe(
});
asAdmin(() => {
createImplicitActions({ modelId });
H.createImplicitActions({ modelId });
});
permissionFn(() => {
@@ -202,7 +192,7 @@ describe(
cy.signInAsAdmin();
cy.get("@modelId").then(modelId => {
createImplicitActions({ modelId });
H.createImplicitActions({ modelId });
visitObjectDetail(modelId, FIRST_SCORE_ROW_ID);
openUpdateObjectModal();
});
@@ -241,7 +231,7 @@ function asNormalUser(callback) {
}
function visitObjectDetail(modelId, objectId) {
visitModel(modelId);
H.visitModel(modelId);
cy.get("main").findByText("Loading...").should("not.exist");
cy.findByTestId("TableInteractive-root").findByText(objectId).click();
}
@@ -252,12 +242,12 @@ function openObjectDetailModal(objectId) {
function openUpdateObjectModal() {
cy.findByTestId("actions-menu").click();
popover().findByText("Update").should("be.visible").click();
H.popover().findByText("Update").should("be.visible").click();
}
function openDeleteObjectModal() {
cy.findByTestId("actions-menu").click();
popover().findByText("Delete").should("be.visible").click();
H.popover().findByText("Delete").should("be.visible").click();
}
function assertActionsDropdownExists() {
@@ -311,7 +301,7 @@ function assertUpdatedScoreNotInTable() {
function assertSuccessfullUpdateToast() {
cy.log("it shows a toast informing the update was successful");
undoToastList()
H.undoToastList()
.last()
.should("be.visible")
.should("have.attr", "color", "success")
@@ -320,7 +310,7 @@ function assertSuccessfullUpdateToast() {
function assertSuccessfullDeleteToast() {
cy.log("it shows a toast informing the delete was successful");
undoToastList()
H.undoToastList()
.last()
.should("be.visible")
.should("have.attr", "color", "success")

View File

@@ -1,36 +1,7 @@
import { assocIn } from "icepick";
import { H } from "e2e/support";
import { WRITABLE_DB_ID } from "e2e/support/cypress_data";
import {
createAction,
createImplicitAction,
createModelFromTableName,
createPublicDashboardLink,
describeWithSnowplow,
dragField,
editDashboard,
enableTracking,
expectGoodSnowplowEvent,
expectNoBadSnowplowEvents,
fillActionQuery,
filterWidget,
getActionCardDetails,
modal,
multiAutocompleteInput,
openStaticEmbeddingModal,
popover,
queryWritableDB,
resetSnowplow,
resetTestTable,
restore,
resyncDatabase,
saveDashboard,
setFilter,
sidebar,
updateDashboardCards,
visitDashboard,
visitIframe,
} from "e2e/support/helpers";
import { many_data_types_rows } from "e2e/support/test_tables_data";
import { createMockActionParameter } from "metabase-types/api/mocks";
@@ -64,28 +35,28 @@ const MODEL_NAME = "Test Action Model";
);
});
describeWithSnowplow("adding and executing actions", () => {
H.describeWithSnowplow("adding and executing actions", () => {
beforeEach(() => {
resetSnowplow();
resetTestTable({ type: dialect, table: TEST_TABLE });
restore(`${dialect}-writable`);
H.resetSnowplow();
H.resetTestTable({ type: dialect, table: TEST_TABLE });
H.restore(`${dialect}-writable`);
cy.signInAsAdmin();
enableTracking();
resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: TEST_TABLE });
createModelFromTableName({
H.enableTracking();
H.resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: TEST_TABLE });
H.createModelFromTableName({
tableName: TEST_TABLE,
modelName: MODEL_NAME,
});
});
afterEach(() => {
expectNoBadSnowplowEvents();
H.expectNoBadSnowplowEvents();
});
it("adds a custom query action to a dashboard and runs it", () => {
const ACTION_NAME = "Update Score";
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${TEST_TABLE} WHERE id = 1`,
dialect,
).then(result => {
@@ -105,7 +76,7 @@ const MODEL_NAME = "Test Action Model";
.click();
cy.findByRole("dialog").within(() => {
fillActionQuery(
H.fillActionQuery(
`UPDATE ${TEST_TABLE} SET score = {{ new_score }} WHERE id = {{ id }}`,
);
});
@@ -128,11 +99,11 @@ const MODEL_NAME = "Test Action Model";
idFilter: true,
});
expectGoodSnowplowEvent({
H.expectGoodSnowplowEvent({
event: "new_action_card_created",
});
filterWidget().click();
H.filterWidget().click();
addWidgetStringFilter("1");
cy.findByRole("button", { name: "Update Score" }).click();
@@ -144,7 +115,7 @@ const MODEL_NAME = "Test Action Model";
cy.wait("@executeAction");
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${TEST_TABLE} WHERE id = 1`,
dialect,
).then(result => {
@@ -155,7 +126,7 @@ const MODEL_NAME = "Test Action Model";
it("adds an implicit create action to a dashboard and runs it", () => {
cy.get("@modelId").then(id => {
createImplicitAction({
H.createImplicitAction({
kind: "create",
model_id: id,
});
@@ -165,13 +136,13 @@ const MODEL_NAME = "Test Action Model";
actionName: "Create",
});
expectGoodSnowplowEvent({
H.expectGoodSnowplowEvent({
event: "new_action_card_created",
});
cy.findByRole("button", { name: "Create" }).click();
modal().within(() => {
H.modal().within(() => {
cy.findByPlaceholderText("Team Name").type("Zany Zebras");
cy.findByPlaceholderText("Score").type("44");
@@ -180,7 +151,7 @@ const MODEL_NAME = "Test Action Model";
cy.wait("@executeAction");
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${TEST_TABLE} WHERE team_name = 'Zany Zebras'`,
dialect,
).then(result => {
@@ -194,7 +165,7 @@ const MODEL_NAME = "Test Action Model";
const actionName = "Update";
cy.get("@modelId").then(id => {
createImplicitAction({
H.createImplicitAction({
kind: "update",
model_id: id,
});
@@ -205,18 +176,18 @@ const MODEL_NAME = "Test Action Model";
idFilter: true,
});
expectGoodSnowplowEvent({
H.expectGoodSnowplowEvent({
event: "new_action_card_created",
});
filterWidget().click();
H.filterWidget().click();
addWidgetStringFilter("5");
cy.findByRole("button", { name: actionName }).click();
cy.wait("@prefetchValues");
// let's check that the existing values are pre-filled correctly
modal().within(() => {
H.modal().within(() => {
cy.findByPlaceholderText("Team Name")
.should("have.value", "Energetic Elephants")
.clear()
@@ -232,7 +203,7 @@ const MODEL_NAME = "Test Action Model";
cy.wait("@executeAction");
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${TEST_TABLE} WHERE team_name = 'Emotional Elephants'`,
dialect,
).then(result => {
@@ -243,7 +214,7 @@ const MODEL_NAME = "Test Action Model";
});
it("adds an implicit delete action to a dashboard and runs it", () => {
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${TEST_TABLE} WHERE team_name = 'Cuddly Cats'`,
dialect,
).then(result => {
@@ -252,7 +223,7 @@ const MODEL_NAME = "Test Action Model";
});
cy.get("@modelId").then(id => {
createImplicitAction({
H.createImplicitAction({
kind: "delete",
model_id: id,
});
@@ -262,20 +233,20 @@ const MODEL_NAME = "Test Action Model";
actionName: "Delete",
});
expectGoodSnowplowEvent({
H.expectGoodSnowplowEvent({
event: "new_action_card_created",
});
cy.findByRole("button", { name: "Delete" }).click();
modal().within(() => {
H.modal().within(() => {
cy.findByPlaceholderText("ID").type("3");
cy.button("Delete").click();
});
cy.wait("@executeAction");
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${TEST_TABLE} WHERE team_name = 'Cuddly Cats'`,
dialect,
).then(result => {
@@ -289,7 +260,7 @@ const MODEL_NAME = "Test Action Model";
};
cy.get("@modelId")
.then(id => {
createImplicitAction({
H.createImplicitAction({
kind: "create",
model_id: id,
});
@@ -297,10 +268,10 @@ const MODEL_NAME = "Test Action Model";
.then(({ body: action }) => {
cy.createDashboard(dashboardDetails).then(
({ body: dashboard }) => {
updateDashboardCards({
H.updateDashboardCards({
dashboard_id: dashboard.id,
cards: [
getActionCardDetails({
H.getActionCardDetails({
action_id: action.id,
label: "Create",
}),
@@ -318,7 +289,7 @@ const MODEL_NAME = "Test Action Model";
cy.log("Visit public dashboard");
cy.get("@dashboardId")
.then(dashboardId => {
createPublicDashboardLink(dashboardId);
H.createPublicDashboardLink(dashboardId);
})
.then(({ body: { uuid } }) => {
cy.visit(`/public/dashboard/${uuid}`);
@@ -345,7 +316,7 @@ const MODEL_NAME = "Test Action Model";
};
cy.get("@modelId")
.then(id => {
createImplicitAction({
H.createImplicitAction({
kind: "create",
model_id: id,
});
@@ -353,10 +324,10 @@ const MODEL_NAME = "Test Action Model";
.then(({ body: action }) => {
cy.createDashboard(dashboardDetails).then(
({ body: dashboard }) => {
updateDashboardCards({
H.updateDashboardCards({
dashboard_id: dashboard.id,
cards: [
getActionCardDetails({
H.getActionCardDetails({
action_id: action.id,
label: "Create",
}),
@@ -373,8 +344,8 @@ const MODEL_NAME = "Test Action Model";
cy.log("Visit static embed dashboard");
openStaticEmbeddingModal({ activeTab: "parameters" });
visitIframe();
H.openStaticEmbeddingModal({ activeTab: "parameters" });
H.visitIframe();
cy.log("Assert static embed dashboard");
cy.findByRole("heading", {
@@ -393,7 +364,7 @@ const MODEL_NAME = "Test Action Model";
describe("hidden fields", () => {
it("adds an implicit action and runs it", () => {
cy.get("@modelId").then(id => {
createImplicitAction({
H.createImplicitAction({
kind: "create",
model_id: id,
});
@@ -406,7 +377,7 @@ const MODEL_NAME = "Test Action Model";
cy.findByRole("button", { name: "Create" }).click();
modal().within(() => {
H.modal().within(() => {
cy.findByPlaceholderText("Team Name").type("Zany Zebras");
cy.findByPlaceholderText("Score").type("44");
cy.findByPlaceholderText("Created At").should("not.exist");
@@ -416,7 +387,7 @@ const MODEL_NAME = "Test Action Model";
cy.wait("@executeAction");
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${TEST_TABLE} WHERE team_name = 'Zany Zebras'`,
dialect,
).then(result => {
@@ -429,7 +400,7 @@ const MODEL_NAME = "Test Action Model";
it("adds a query action and runs it", () => {
const ACTION_NAME = "Update Score";
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${TEST_TABLE} WHERE id = 1`,
dialect,
).then(result => {
@@ -453,7 +424,7 @@ const MODEL_NAME = "Test Action Model";
.click();
cy.findByRole("dialog").within(() => {
fillActionQuery(
H.fillActionQuery(
`UPDATE ${TEST_TABLE} SET score = {{ new_score }} WHERE id = {{ id }} [[ and status = {{ current_status }}]]`,
);
});
@@ -474,7 +445,7 @@ const MODEL_NAME = "Test Action Model";
});
});
popover().within(() => {
H.popover().within(() => {
cy.findByLabelText("Required").uncheck();
});
@@ -504,7 +475,7 @@ const MODEL_NAME = "Test Action Model";
cy.wait("@executeAction");
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${TEST_TABLE} WHERE id = 1`,
dialect,
).then(result => {
@@ -527,7 +498,7 @@ const MODEL_NAME = "Test Action Model";
cy.icon("ellipsis").click();
});
popover().within(() => {
H.popover().within(() => {
cy.findByText("Edit").click();
});
@@ -539,7 +510,7 @@ const MODEL_NAME = "Test Action Model";
});
});
popover().within(() => {
H.popover().within(() => {
cy.findByLabelText("Required").check();
});
@@ -547,7 +518,7 @@ const MODEL_NAME = "Test Action Model";
cy.findByText("Update").click();
});
visitDashboard("@dashboardId");
H.visitDashboard("@dashboardId");
cy.findByRole("button", { name: "Update Score" }).click();
@@ -561,7 +532,7 @@ const MODEL_NAME = "Test Action Model";
cy.wait("@executeAction");
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${TEST_TABLE} WHERE id = 1`,
dialect,
).then(result => {
@@ -574,14 +545,14 @@ const MODEL_NAME = "Test Action Model";
describe("Actions Data Types", () => {
beforeEach(() => {
resetTestTable({ type: dialect, table: TEST_COLUMNS_TABLE });
restore(`${dialect}-writable`);
H.resetTestTable({ type: dialect, table: TEST_COLUMNS_TABLE });
H.restore(`${dialect}-writable`);
cy.signInAsAdmin();
resyncDatabase({
H.resyncDatabase({
dbId: WRITABLE_DB_ID,
tableName: TEST_COLUMNS_TABLE,
});
createModelFromTableName({
H.createModelFromTableName({
tableName: TEST_COLUMNS_TABLE,
modelName: MODEL_NAME,
});
@@ -589,7 +560,7 @@ const MODEL_NAME = "Test Action Model";
it("can update various data types via implicit actions", () => {
cy.get("@modelId").then(id => {
createImplicitAction({
H.createImplicitAction({
kind: "update",
model_id: id,
});
@@ -603,7 +574,7 @@ const MODEL_NAME = "Test Action Model";
cy.wait("@getModel");
cy.findByRole("button", { name: "Update" });
filterWidget().click();
H.filterWidget().click();
addWidgetStringFilter("1");
cy.findByRole("button", { name: "Update" }).click();
@@ -612,7 +583,7 @@ const MODEL_NAME = "Test Action Model";
const oldRow = many_data_types_rows[0];
modal()
H.modal()
.first()
.within(() => {
changeValue({
@@ -664,7 +635,7 @@ const MODEL_NAME = "Test Action Model";
cy.wait("@executeAction");
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${TEST_COLUMNS_TABLE} WHERE id = 1`,
dialect,
).then(result => {
@@ -690,7 +661,7 @@ const MODEL_NAME = "Test Action Model";
it("can insert various data types via implicit actions", () => {
cy.get("@modelId").then(id => {
createImplicitAction({
H.createImplicitAction({
kind: "create",
model_id: id,
});
@@ -702,7 +673,7 @@ const MODEL_NAME = "Test Action Model";
cy.findByRole("button", { name: "Create" }).click();
modal().within(() => {
H.modal().within(() => {
cy.findByPlaceholderText("UUID").type(
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a15",
);
@@ -735,7 +706,7 @@ const MODEL_NAME = "Test Action Model";
cy.wait("@executeAction");
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${TEST_COLUMNS_TABLE} WHERE string = 'Zany Zebras'`,
dialect,
).then(result => {
@@ -775,7 +746,7 @@ const MODEL_NAME = "Test Action Model";
it("does not show json, enum, or binary columns for implicit actions", () => {
cy.get("@modelId").then(id => {
createImplicitAction({
H.createImplicitAction({
kind: "create",
model_id: id,
});
@@ -788,7 +759,7 @@ const MODEL_NAME = "Test Action Model";
cy.findByRole("button", { name: "Create" }).click();
modal().within(() => {
H.modal().within(() => {
cy.findByPlaceholderText("UUID").should("be.visible");
cy.findByPlaceholderText("JSON").should("not.exist");
cy.findByPlaceholderText("JSONB").should("not.exist");
@@ -799,7 +770,7 @@ const MODEL_NAME = "Test Action Model";
it("properly loads and updates date and time fields for implicit update actions", () => {
cy.get("@modelId").then(id => {
createImplicitAction({
H.createImplicitAction({
kind: "update",
model_id: id,
});
@@ -813,7 +784,7 @@ const MODEL_NAME = "Test Action Model";
cy.wait("@getModel");
cy.findByRole("button", { name: "Update" });
filterWidget().click();
H.filterWidget().click();
addWidgetStringFilter("1");
cy.findByRole("button", { name: "Update" }).click();
@@ -823,7 +794,7 @@ const MODEL_NAME = "Test Action Model";
const oldRow = many_data_types_rows[0];
const newTime = "2020-01-10T01:35:55";
modal()
H.modal()
.first()
.within(() => {
changeValue({
@@ -873,7 +844,7 @@ const MODEL_NAME = "Test Action Model";
cy.wait("@executeAction");
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${TEST_COLUMNS_TABLE} WHERE id = 1`,
dialect,
).then(result => {
@@ -948,20 +919,20 @@ const MODEL_NAME = "Test Action Model";
);
beforeEach(() => {
resetTestTable({ type: dialect, table: TEST_COLUMNS_TABLE });
restore(`${dialect}-writable`);
H.resetTestTable({ type: dialect, table: TEST_COLUMNS_TABLE });
H.restore(`${dialect}-writable`);
cy.signInAsAdmin();
resyncDatabase({
H.resyncDatabase({
dbId: WRITABLE_DB_ID,
tableName: TEST_COLUMNS_TABLE,
});
createModelFromTableName({
H.createModelFromTableName({
tableName: TEST_COLUMNS_TABLE,
modelName: MODEL_NAME,
});
cy.get("@modelId").then(modelId => {
createAction({
H.createAction({
...SAMPLE_WRITABLE_QUERY_ACTION,
model_id: modelId,
});
@@ -990,7 +961,7 @@ const MODEL_NAME = "Test Action Model";
});
});
popover().within(() => {
H.popover().within(() => {
cy.findByText("Placeholder text").click().type("Test placeholder");
});
@@ -1013,9 +984,9 @@ const MODEL_NAME = "Test Action Model";
});
actionEditorModal().within(() => {
cy.get(".ace_content").click().type("{home}{shift+end}{backspace}");
H.focusNativeEditor().type("{home}{shift+end}{backspace}");
const TEST_COLUMNS_QUERY = `UPDATE ${TEST_COLUMNS_TABLE} SET timestamp = {{ Timestamp }} WHERE id = {{ ID }}`;
cy.get(".ace_content").type(TEST_COLUMNS_QUERY, {
H.focusNativeEditor().type(TEST_COLUMNS_QUERY, {
delay: 0,
parseSpecialCharSequences: false,
});
@@ -1070,11 +1041,11 @@ const MODEL_NAME = "Test Action Model";
describe("action error handling", { tags: ["@external", "@actions"] }, () => {
beforeEach(() => {
resetTestTable({ type: "postgres", table: TEST_TABLE });
restore("postgres-writable");
H.resetTestTable({ type: "postgres", table: TEST_TABLE });
H.restore("postgres-writable");
cy.signInAsAdmin();
resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: TEST_TABLE });
createModelFromTableName({
H.resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: TEST_TABLE });
H.createModelFromTableName({
tableName: TEST_TABLE,
modelName: MODEL_NAME,
});
@@ -1093,7 +1064,7 @@ describe("action error handling", { tags: ["@external", "@actions"] }, () => {
const actionName = "Update";
cy.get("@modelId").then(modelId => {
createImplicitAction({ kind: "update", model_id: modelId });
H.createImplicitAction({ kind: "update", model_id: modelId });
});
createDashboardWithActionButton({ actionName, idFilter: true });
@@ -1101,13 +1072,13 @@ describe("action error handling", { tags: ["@external", "@actions"] }, () => {
cy.wait("@getModel");
cy.findByRole("button", { name: "Update" });
filterWidget().click();
H.filterWidget().click();
addWidgetStringFilter("5");
cy.button(actionName).click();
cy.wait("@prefetchValues");
modal()
H.modal()
.first()
.within(() => {
cy.findByLabelText("Team Name").clear().type("Kind Koalas");
@@ -1145,11 +1116,11 @@ describe(
describe("Inline action edit", () => {
beforeEach(() => {
resetTestTable({ type: "postgres", table: TEST_TABLE });
restore("postgres-writable");
H.resetTestTable({ type: "postgres", table: TEST_TABLE });
H.restore("postgres-writable");
cy.signInAsAdmin();
resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: TEST_TABLE });
createModelFromTableName({
H.resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: TEST_TABLE });
H.createModelFromTableName({
tableName: TEST_TABLE,
modelName: MODEL_NAME,
});
@@ -1159,7 +1130,7 @@ describe(
const actionName = "Update";
cy.get("@modelId").then(id => {
createImplicitAction({
H.createImplicitAction({
kind: "update",
model_id: id,
});
@@ -1170,14 +1141,14 @@ describe(
idFilter: true,
});
filterWidget().click();
H.filterWidget().click();
addWidgetStringFilter("5");
cy.button(actionName).click();
cy.wait("@executePrefetch");
modal().within(() => {
H.modal().within(() => {
cy.findByPlaceholderText("Team Name").should(
"have.value",
"Energetic Elephants",
@@ -1187,9 +1158,9 @@ describe(
cy.icon("close").click();
});
filterWidget().click();
popover().within(() => {
multiAutocompleteInput().type("{backspace}10");
H.filterWidget().click();
H.popover().within(() => {
H.multiAutocompleteInput().type("{backspace}10");
});
cy.button("Update filter").click();
@@ -1197,7 +1168,7 @@ describe(
cy.wait("@executePrefetch");
modal().within(() => {
H.modal().within(() => {
cy.findByPlaceholderText("Team Name").should(
"have.value",
"Jolly Jellyfish",
@@ -1221,7 +1192,7 @@ describe(
.click();
cy.findByRole("dialog").within(() => {
fillActionQuery(
H.fillActionQuery(
`UPDATE ${TEST_TABLE} SET score = {{ new_score }} WHERE id = {{ id }}`,
);
});
@@ -1235,14 +1206,14 @@ describe(
cy.createDashboard({ name: "action packed dashboard" }).then(
({ body: { id: dashboardId } }) => {
visitDashboard(dashboardId);
H.visitDashboard(dashboardId);
},
);
editDashboard();
H.editDashboard();
setFilter("ID");
sidebar().within(() => {
H.setFilter("ID");
H.sidebar().within(() => {
cy.button("Done").click();
});
@@ -1295,15 +1266,15 @@ function createDashboardWithActionButton({
cy.createDashboard({ name: "action packed dashboard" }).then(
({ body: { id: dashboardId } }) => {
cy.wrap(dashboardId).as("dashboardId");
visitDashboard(dashboardId);
H.visitDashboard(dashboardId);
},
);
editDashboard();
H.editDashboard();
if (idFilter) {
setFilter("ID");
sidebar().within(() => {
H.setFilter("ID");
H.sidebar().within(() => {
cy.button("Done").click();
});
}
@@ -1349,7 +1320,7 @@ function createDashboardWithActionButton({
.first()
.click();
});
popover().within(() => {
H.popover().within(() => {
cy.findByText("ID").click();
});
}
@@ -1358,7 +1329,7 @@ function createDashboardWithActionButton({
cy.button("Done").click();
});
saveDashboard();
H.saveDashboard();
}
const changeValue = ({ fieldName, fieldType, oldValue, newValue }) => {
@@ -1384,7 +1355,7 @@ function toggleFieldVisibility() {
}
function reorderFields() {
dragField(1, 0);
H.dragField(1, 0);
}
const clickHelper = buttonName => {

View File

@@ -1,20 +1,7 @@
import { H } from "e2e/support";
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data";
import {
createAction,
editDashboard,
getActionCardDetails,
getDashboardCard,
getNextUnsavedDashboardCardId,
modal,
restore,
saveDashboard,
setActionsEnabledForDB,
undoToast,
updateDashboardCards,
visitDashboard,
} from "e2e/support/helpers";
import {
createMockActionParameter,
createMockParameter,
@@ -32,15 +19,15 @@ describe("metabase#31587", () => {
viewports.forEach(([width, height]) => {
describe(`Testing on resolution ${width} x ${height}`, () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setActionsEnabledForDB(SAMPLE_DB_ID);
H.setActionsEnabledForDB(SAMPLE_DB_ID);
cy.viewport(width, height);
});
it("should not allow action buttons to overflow when editing dashboard", () => {
visitDashboard(ORDERS_DASHBOARD_ID);
editDashboard();
H.visitDashboard(ORDERS_DASHBOARD_ID);
H.editDashboard();
cy.button("Add action").click();
cy.findByTestId("dashboard-parameters-and-cards").within(() => {
@@ -63,11 +50,11 @@ describe("metabase#31587", () => {
});
it("should not allow action buttons to overflow when viewing info sidebar", () => {
visitDashboard(ORDERS_DASHBOARD_ID);
editDashboard();
H.visitDashboard(ORDERS_DASHBOARD_ID);
H.editDashboard();
cy.findByLabelText("Add action").click();
saveDashboard();
H.saveDashboard();
cy.icon("info").click();
cy.findByTestId("dashboard-parameters-and-cards").within(() => {
@@ -165,11 +152,11 @@ describe("Issue 32974", { tags: ["@external", "@actions"] }, () => {
);
cy.then(function () {
updateDashboardCards({
H.updateDashboardCards({
dashboard_id: this.dashboardId,
cards: [
{
id: getNextUnsavedDashboardCardId(),
id: H.getNextUnsavedDashboardCardId(),
card_id: this.modelId,
// Map dashboard parameter to PRODUCTS.ID
parameter_mappings: [
@@ -180,7 +167,7 @@ describe("Issue 32974", { tags: ["@external", "@actions"] }, () => {
},
],
},
getActionCardDetails({
H.getActionCardDetails({
label: QUERY_ACTION.name,
action_id: this.actionId,
// Map action's ID parameter to dashboard parameter
@@ -200,18 +187,18 @@ describe("Issue 32974", { tags: ["@external", "@actions"] }, () => {
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.createQuestion(MODEL_DETAILS, {
wrapId: true,
idAlias: "modelId",
});
setActionsEnabledForDB(SAMPLE_DB_ID, true);
H.setActionsEnabledForDB(SAMPLE_DB_ID, true);
});
it("can submit query action linked with dashboard parameters (metabase#32974)", () => {
cy.get("@modelId").then(modelId => {
createAction({ ...QUERY_ACTION, model_id: modelId }).then(
H.createAction({ ...QUERY_ACTION, model_id: modelId }).then(
({ body: { id: actionId } }) => {
cy.wrap(actionId).as("actionId");
},
@@ -220,15 +207,15 @@ describe("Issue 32974", { tags: ["@external", "@actions"] }, () => {
setupDashboard();
visitDashboard("@dashboardId", { params: { id: 1 } });
H.visitDashboard("@dashboardId", { params: { id: 1 } });
cy.log("Execute action");
cy.button(QUERY_ACTION.name).click();
modal().button(QUERY_ACTION.name).click();
H.modal().button(QUERY_ACTION.name).click();
cy.log("Assertions");
getDashboardCard().findByText(EXPECTED_UPDATED_VALUE).should("exist");
modal().should("not.exist");
undoToast().findByText("Query action ran successfully").should("exist");
H.getDashboardCard().findByText(EXPECTED_UPDATED_VALUE).should("exist");
H.modal().should("not.exist");
H.undoToast().findByText("Query action ran successfully").should("exist");
});
});

View File

@@ -1,25 +1,11 @@
import { assocIn } from "icepick";
import { H } from "e2e/support";
import {
SAMPLE_DB_ID,
USER_GROUPS,
WRITABLE_DB_ID,
} from "e2e/support/cypress_data";
import {
createAction,
createImplicitActions,
createModelFromTableName,
entityPickerModal,
fillActionQuery,
modal,
popover,
queryWritableDB,
resetTestTable,
restore,
resyncDatabase,
setActionsEnabledForDB,
setTokenFeatures,
} from "e2e/support/helpers";
import { getCreatePostgresRoleIfNotExistSql } from "e2e/support/test_roles";
import { createMockActionParameter } from "metabase-types/api/mocks";
@@ -79,11 +65,11 @@ describe(
{ tags: ["@external", "@actions"] },
() => {
beforeEach(() => {
restore("postgres-12");
H.restore("postgres-12");
cy.signInAsAdmin();
setActionsEnabledForDB(WRITABLE_DB_ID);
H.setActionsEnabledForDB(WRITABLE_DB_ID);
createModelFromTableName({
H.createModelFromTableName({
tableName: "orders",
modelName: "Order",
idAlias: "modelId",
@@ -119,12 +105,12 @@ describe(
});
cy.findByRole("link", { name: "New action" }).click();
fillActionQuery("DELETE FROM orders WHERE id = {{ id }}");
H.fillActionQuery("DELETE FROM orders WHERE id = {{ id }}");
cy.findByRole("radiogroup", { name: "Field type" })
.findByText("Number")
.click();
cy.findByRole("button", { name: "Save" }).click();
modal()
H.modal()
.eq(1)
.within(() => {
cy.findByLabelText("Name").type("Delete Order");
@@ -135,7 +121,7 @@ describe(
.should("be.visible");
openActionEditorFor("Delete Order");
fillActionQuery(" AND status = 'pending'");
H.fillActionQuery(" AND status = 'pending'");
cy.findByRole("radiogroup", { name: "Field type" })
.findByLabelText("Number")
.should("be.checked");
@@ -148,9 +134,9 @@ describe(
.should("be.visible");
openActionMenuFor("Delete Order");
popover().findByText("Archive").click();
H.popover().findByText("Archive").click();
modal().within(() => {
H.modal().within(() => {
cy.findByText("Archive Delete Order?").should("be.visible");
cy.findByRole("button", { name: "Archive" }).click();
});
@@ -158,8 +144,8 @@ describe(
cy.findByRole("listitem", { name: "Delete Order" }).should("not.exist");
cy.findByLabelText("Actions menu").click();
popover().findByText("Disable basic actions").click();
modal().within(() => {
H.popover().findByText("Disable basic actions").click();
H.modal().within(() => {
cy.findByText("Disable basic actions?").should("be.visible");
cy.button("Disable").click();
});
@@ -179,10 +165,10 @@ describe(
cy.visit("/");
cy.findByTestId("app-bar").findByText("New").click();
popover().findByText("Action").click();
H.popover().findByText("Action").click();
cy.wait("@getDatabase");
fillActionQuery(QUERY);
H.fillActionQuery(QUERY);
cy.findByRole("dialog").within(() => {
cy.findByText(/New Action/)
@@ -192,8 +178,8 @@ describe(
cy.findByRole("button", { name: "Save" }).click();
});
modal().eq(1).findByText("Select a model").click();
entityPickerModal().within(() => {
H.modal().eq(1).findByText("Select a model").click();
H.entityPickerModal().within(() => {
cy.findByText("Order").click();
});
@@ -214,9 +200,9 @@ describe(
it("should respect permissions", () => {
// Enabling actions for sample database as well
// to test database picker behavior in the action editor
setActionsEnabledForDB(SAMPLE_DB_ID);
H.setActionsEnabledForDB(SAMPLE_DB_ID);
setTokenFeatures("all");
H.setTokenFeatures("all");
cy.updatePermissionsGraph({
[USER_GROUPS.ALL_USERS_GROUP]: {
[WRITABLE_DB_ID]: {
@@ -243,7 +229,7 @@ describe(
});
openActionMenuFor(SAMPLE_QUERY_ACTION.name);
popover().within(() => {
H.popover().within(() => {
cy.findByText("Archive").should("not.exist");
cy.findByText("View").click();
});
@@ -272,13 +258,13 @@ describe(
// Check can pick between all databases
cy.findByRole("dialog").findByText("QA Postgres12").click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("Sample Database").should("be.visible");
cy.findByText("QA Postgres12").should("be.visible");
});
cy.signInAsAdmin();
setActionsEnabledForDB(SAMPLE_DB_ID, false);
H.setActionsEnabledForDB(SAMPLE_DB_ID, false);
cy.signIn("normal");
cy.reload();
@@ -293,16 +279,16 @@ describe(
it("should display parameters for variable template tags only", () => {
cy.visit("/");
cy.findByTestId("app-bar").findByText("New").click();
popover().findByText("Action").click();
H.popover().findByText("Action").click();
fillActionQuery("{{#1-orders-model}}");
H.fillActionQuery("{{#1-orders-model}}");
cy.findByLabelText("#1-orders-model").should("not.exist");
fillActionQuery("{{snippet:101}}");
H.fillActionQuery("{{snippet:101}}");
cy.findByLabelText("#1-orders-model").should("not.exist");
cy.findByLabelText("101").should("not.exist");
fillActionQuery("{{id}}");
H.fillActionQuery("{{id}}");
cy.findByLabelText("#1-orders-model").should("not.exist");
cy.findByLabelText("101").should("not.exist");
cy.findByLabelText("ID").should("be.visible");
@@ -312,7 +298,7 @@ describe(
const actionName = "Update";
cy.get("@modelId").then(modelId => {
createImplicitActions({ modelId });
H.createImplicitActions({ modelId });
cy.visit(`/model/${modelId}/detail`);
cy.wait("@getModel");
@@ -324,7 +310,7 @@ describe(
cy.wait("@getAction");
modal().within(() => {
H.modal().within(() => {
cy.findByLabelText("ID").type("1");
cy.findByLabelText("User ID").type("999999");
cy.button(actionName).click();
@@ -356,19 +342,22 @@ describe(
"disableActionSharing",
);
resetTestTable({ type: dialect, table: WRITABLE_TEST_TABLE });
restore(`${dialect}-writable`);
H.resetTestTable({ type: dialect, table: WRITABLE_TEST_TABLE });
H.restore(`${dialect}-writable`);
cy.signInAsAdmin();
resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: WRITABLE_TEST_TABLE });
H.resyncDatabase({
dbId: WRITABLE_DB_ID,
tableName: WRITABLE_TEST_TABLE,
});
createModelFromTableName({
H.createModelFromTableName({
tableName: WRITABLE_TEST_TABLE,
idAlias: "writableModelId",
});
});
it("should allow action execution from the model detail page", () => {
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${WRITABLE_TEST_TABLE} WHERE id = 1`,
dialect,
).then(result => {
@@ -377,7 +366,7 @@ describe(
});
cy.get("@writableModelId").then(modelId => {
createAction({
H.createAction({
...SAMPLE_WRITABLE_QUERY_ACTION,
model_id: modelId,
});
@@ -387,7 +376,7 @@ describe(
runActionFor(SAMPLE_QUERY_ACTION.name);
modal().within(() => {
H.modal().within(() => {
cy.findByLabelText(TEST_PARAMETER.name).type("1");
cy.button(SAMPLE_QUERY_ACTION.name).click();
});
@@ -396,7 +385,7 @@ describe(
.findByText(`${SAMPLE_QUERY_ACTION.name} ran successfully`)
.should("be.visible");
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${WRITABLE_TEST_TABLE} WHERE id = 1`,
dialect,
).then(result => {
@@ -410,11 +399,11 @@ describe(
const IMPLICIT_ACTION_NAME = "Update";
cy.get("@writableModelId").then(modelId => {
createAction({
H.createAction({
...SAMPLE_WRITABLE_QUERY_ACTION,
model_id: modelId,
});
createAction({
H.createAction({
type: "implicit",
kind: "row/update",
name: IMPLICIT_ACTION_NAME,
@@ -443,7 +432,7 @@ describe(
cy.findByRole("form").should("not.exist");
cy.button(SAMPLE_QUERY_ACTION.name).should("not.exist");
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${WRITABLE_TEST_TABLE} WHERE id = 1`,
dialect,
).then(result => {
@@ -468,7 +457,7 @@ describe(
cy.findByRole("form").should("not.exist");
cy.button(IMPLICIT_ACTION_NAME).should("not.exist");
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${WRITABLE_TEST_TABLE} WHERE id = 2`,
dialect,
).then(result => {
@@ -511,7 +500,7 @@ describe(
verifyScoreValue(0, dialect);
cy.get("@writableModelId").then(modelId => {
createAction({
H.createAction({
...SAMPLE_WRITABLE_QUERY_ACTION,
model_id: modelId,
});
@@ -521,7 +510,7 @@ describe(
openActionEditorFor(SAMPLE_QUERY_ACTION.name);
fillActionQuery(" [[and status = {{ current_status}}]]");
H.fillActionQuery(" [[and status = {{ current_status}}]]");
cy.findAllByTestId("form-field-container")
.filter(":contains('Current Status')")
.within(() => {
@@ -529,7 +518,7 @@ describe(
cy.icon("gear").click();
});
popover().within(() => {
H.popover().within(() => {
cy.findByLabelText("Required").uncheck();
});
@@ -537,7 +526,7 @@ describe(
runActionFor(SAMPLE_QUERY_ACTION.name);
modal().within(() => {
H.modal().within(() => {
cy.findByLabelText(TEST_PARAMETER.name).type("1");
cy.findByLabelText("Current Status").should("not.exist");
@@ -558,14 +547,14 @@ describe(
cy.icon("gear").click();
});
popover().within(() => {
H.popover().within(() => {
cy.findByLabelText("Required").check();
});
cy.findByRole("button", { name: "Update" }).click();
runActionFor(SAMPLE_QUERY_ACTION.name);
modal().within(() => {
H.modal().within(() => {
cy.findByLabelText(TEST_PARAMETER.name).type("1");
cy.findByLabelText("Current Status").should("not.exist");
@@ -591,7 +580,7 @@ describe(
runActionFor(SAMPLE_QUERY_ACTION.name);
modal().within(() => {
H.modal().within(() => {
cy.findByLabelText(TEST_PARAMETER.name).type("1");
cy.button(SAMPLE_QUERY_ACTION.name).should("be.disabled");
@@ -634,7 +623,7 @@ describe(
runActionFor("Create");
modal().within(() => {
H.modal().within(() => {
cy.findByLabelText("Created At").should("not.exist");
cy.findByLabelText("Team Name").type("Zebras");
cy.findByLabelText("Score").type("1");
@@ -647,7 +636,7 @@ describe(
.should("be.visible");
// show toast
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${WRITABLE_TEST_TABLE} WHERE team_name = 'Zebras'`,
dialect,
).then(result => {
@@ -661,7 +650,7 @@ describe(
it("should allow public sharing of query action and execution", () => {
cy.get("@writableModelId").then(modelId => {
createAction({
H.createAction({
...SAMPLE_WRITABLE_QUERY_ACTION,
model_id: modelId,
});
@@ -676,7 +665,7 @@ describe(
openActionEditorFor(SAMPLE_WRITABLE_QUERY_ACTION.name);
fillActionQuery(" [[ AND status = {{new_status}} ]]");
H.fillActionQuery(" [[ AND status = {{new_status}} ]]");
cy.findAllByTestId("form-field-container")
.filter(":contains('New Status')")
@@ -687,7 +676,7 @@ describe(
cy.icon("gear").click();
});
popover().within(() => {
H.popover().within(() => {
cy.findByLabelText("Required").uncheck();
});
@@ -706,7 +695,7 @@ describe(
`${SAMPLE_WRITABLE_QUERY_ACTION.name} ran successfully`,
).should("be.visible");
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${WRITABLE_TEST_TABLE} WHERE id = 1`,
dialect,
).then(result => {
@@ -759,7 +748,7 @@ describe(
cy.findByText("Update ran successfully").should("be.visible");
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${WRITABLE_TEST_TABLE} WHERE id = 2`,
dialect,
).then(result => {
@@ -778,8 +767,8 @@ describe(
role,
`GRANT SELECT ON ${WRITABLE_TEST_TABLE} TO ${role};`,
);
setTokenFeatures("all");
queryWritableDB(sql);
H.setTokenFeatures("all");
H.queryWritableDB(sql);
const impersonatedUserId = 9;
cy.request("PUT", `/api/user/${impersonatedUserId}`, {
@@ -810,7 +799,7 @@ describe(
],
);
queryWritableDB(
H.queryWritableDB(
`SELECT *
FROM ${WRITABLE_TEST_TABLE}
WHERE id = 1`,
@@ -821,7 +810,7 @@ describe(
});
cy.get("@writableModelId").then(modelId => {
createAction({
H.createAction({
...SAMPLE_WRITABLE_QUERY_ACTION,
model_id: modelId,
});
@@ -832,7 +821,7 @@ describe(
runActionFor(SAMPLE_QUERY_ACTION.name);
modal().within(() => {
H.modal().within(() => {
cy.findByLabelText(TEST_PARAMETER.name).type("1");
cy.button(SAMPLE_QUERY_ACTION.name).click();
@@ -841,7 +830,7 @@ describe(
);
});
queryWritableDB(
H.queryWritableDB(
`SELECT *
FROM ${WRITABLE_TEST_TABLE}
WHERE id = 1`,
@@ -868,7 +857,7 @@ function openActionMenuFor(actionName) {
function openActionEditorFor(actionName, { isReadOnly = false } = {}) {
openActionMenuFor(actionName);
popover()
H.popover()
.findByText(isReadOnly ? "View" : "Edit")
.click();
}
@@ -876,7 +865,7 @@ function openActionEditorFor(actionName, { isReadOnly = false } = {}) {
function assertQueryEditorDisabled() {
// Ace doesn't act as a normal input, so we can't use `should("be.disabled")`
// Instead we'd assert that a user can't type in the editor
fillActionQuery("QWERTY");
H.fillActionQuery("QWERTY");
cy.findByText("QWERTY").should("not.exist");
}
@@ -902,7 +891,7 @@ function disableSharingFor(actionName) {
cy.findByRole("button", { name: "Action settings" }).click();
cy.findByLabelText("Make public").should("be.checked").click();
});
modal()
H.modal()
.eq(1)
.within(() => {
cy.findByText("Disable this public link?").should("be.visible");
@@ -917,7 +906,7 @@ function disableSharingFor(actionName) {
function resetAndVerifyScoreValue(dialect) {
const newValue = 0;
queryWritableDB(
H.queryWritableDB(
`UPDATE ${WRITABLE_TEST_TABLE} SET score = ${newValue} WHERE id = 1`,
dialect,
);
@@ -926,7 +915,7 @@ function resetAndVerifyScoreValue(dialect) {
}
function verifyScoreValue(value, dialect) {
queryWritableDB(
H.queryWritableDB(
`SELECT * FROM ${WRITABLE_TEST_TABLE} WHERE id = 1`,
dialect,
).then(result => {

View File

@@ -1,3 +1,4 @@
import { H } from "e2e/support";
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
@@ -8,13 +9,6 @@ import {
ORDERS_QUESTION_ID,
READONLY_GROUP_ID,
} from "e2e/support/cypress_sample_instance_data";
import {
createApiKey,
restore,
sidesheet,
visitDashboard,
visitQuestion,
} from "e2e/support/helpers";
const { PRODUCTS_ID } = SAMPLE_DATABASE;
describe("scenarios > admin > settings > API keys", () => {
@@ -27,7 +21,7 @@ describe("scenarios > admin > settings > API keys", () => {
cy.intercept("DELETE", "/api/api-key/*").as("deleteKey");
cy.intercept("GET", "/api/permissions/group").as("getGroups");
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -37,7 +31,7 @@ describe("scenarios > admin > settings > API keys", () => {
cy.wait("@getKeyCount");
cy.findByTestId("api-keys-setting").findByText("API Keys");
createApiKey("Test API Key One", ALL_USERS_GROUP_ID);
H.createApiKey("Test API Key One", ALL_USERS_GROUP_ID);
cy.reload();
cy.wait("@getKeyCount");
@@ -46,8 +40,8 @@ describe("scenarios > admin > settings > API keys", () => {
.findByTestId("card-badge")
.findByText("1 API Key");
createApiKey("Test API Key Two", ALL_USERS_GROUP_ID);
createApiKey("Test API Key Three", ALL_USERS_GROUP_ID);
H.createApiKey("Test API Key Two", ALL_USERS_GROUP_ID);
H.createApiKey("Test API Key Three", ALL_USERS_GROUP_ID);
cy.reload();
cy.wait("@getKeyCount");
@@ -58,9 +52,9 @@ describe("scenarios > admin > settings > API keys", () => {
});
it("should list existing API keys", () => {
createApiKey("Test API Key One", ALL_USERS_GROUP_ID);
createApiKey("Test API Key Two", NOSQL_GROUP_ID);
createApiKey("Test API Key Three", READONLY_GROUP_ID);
H.createApiKey("Test API Key One", ALL_USERS_GROUP_ID);
H.createApiKey("Test API Key Two", NOSQL_GROUP_ID);
H.createApiKey("Test API Key Three", READONLY_GROUP_ID);
cy.visit("/admin/settings/authentication/api-keys");
cy.wait("@getKeys");
@@ -111,7 +105,7 @@ describe("scenarios > admin > settings > API keys", () => {
});
it("should allow deleting an API key", () => {
createApiKey("Test API Key One", ALL_USERS_GROUP_ID);
H.createApiKey("Test API Key One", ALL_USERS_GROUP_ID);
cy.visit("/admin/settings/authentication/api-keys");
cy.wait("@getKeys");
@@ -129,7 +123,7 @@ describe("scenarios > admin > settings > API keys", () => {
});
it("should allow editing an API key", () => {
createApiKey("Development API Key", ALL_USERS_GROUP_ID);
H.createApiKey("Development API Key", ALL_USERS_GROUP_ID);
cy.visit("/admin/settings/authentication/api-keys");
cy.wait("@getKeys");
@@ -158,7 +152,7 @@ describe("scenarios > admin > settings > API keys", () => {
});
it("should allow regenerating an API key", () => {
createApiKey("Personal API Key", ALL_USERS_GROUP_ID);
H.createApiKey("Personal API Key", ALL_USERS_GROUP_ID);
cy.visit("/admin/settings/authentication/api-keys");
cy.wait("@getKeys").then(({ response }) => {
@@ -189,19 +183,19 @@ describe("scenarios > admin > settings > API keys", () => {
describe("api key actions", () => {
it("should allow creating questions and dashboards with an API key", () => {
createApiKey("Test API Key One", ADMINISTRATORS_GROUP_ID).then(
H.createApiKey("Test API Key One", ADMINISTRATORS_GROUP_ID).then(
({ body }) => {
const apiKey = body.unmasked_key;
createQuestionForApiKey(apiKey).then(({ body }) => {
const questionId = body.id;
cy.signInAsAdmin();
visitQuestion(questionId);
H.visitQuestion(questionId);
cy.findByTestId("qb-header").findByText("Test Question");
cy.findByTestId("view-footer").findByText("Showing 22 rows");
cy.findByTestId("qb-header-info-button").click();
sidesheet().within(() => {
H.sidesheet().within(() => {
cy.findByRole("tab", { name: "History" }).click();
cy.findByText("Test API Key One created this.");
});
@@ -211,10 +205,10 @@ describe("scenarios > admin > settings > API keys", () => {
const dashboardId = body.id;
cy.signInAsAdmin();
visitDashboard(dashboardId);
H.visitDashboard(dashboardId);
cy.findByTestId("dashboard-header").findByText("Test Dashboard");
cy.findByTestId("dashboard-header").icon("info").click();
sidesheet().within(() => {
H.sidesheet().within(() => {
cy.findByRole("tab", { name: "History" }).click();
cy.findByText("Test API Key One created this.");
});
@@ -224,7 +218,7 @@ describe("scenarios > admin > settings > API keys", () => {
});
it("should allow editing questions and dashboards with an api key", () => {
createApiKey("Test API Key One", ADMINISTRATORS_GROUP_ID).then(
H.createApiKey("Test API Key One", ADMINISTRATORS_GROUP_ID).then(
({ body }) => {
const apiKey = body.unmasked_key;
@@ -234,10 +228,10 @@ describe("scenarios > admin > settings > API keys", () => {
"Edited Question Name",
).then(() => {
cy.signInAsAdmin();
visitQuestion(ORDERS_QUESTION_ID);
H.visitQuestion(ORDERS_QUESTION_ID);
cy.findByTestId("qb-header").findByText("Edited Question Name");
cy.findByTestId("qb-header-info-button").click();
sidesheet().within(() => {
H.sidesheet().within(() => {
cy.findByRole("tab", { name: "History" }).click();
cy.findByText("You created this.");
cy.findByText(
@@ -252,12 +246,12 @@ describe("scenarios > admin > settings > API keys", () => {
"Edited Dashboard Name",
).then(() => {
cy.signInAsAdmin();
visitDashboard(ORDERS_DASHBOARD_ID);
H.visitDashboard(ORDERS_DASHBOARD_ID);
cy.findByTestId("dashboard-header").findByText(
"Edited Dashboard Name",
);
cy.findByTestId("dashboard-header").icon("info").click();
sidesheet().within(() => {
H.sidesheet().within(() => {
cy.findByRole("tab", { name: "History" }).click();
cy.findByText("You created this.");
cy.findByText(

View File

@@ -1,24 +1,17 @@
import {
describeEE,
main,
modal,
onlyOnOSS,
restore,
setTokenFeatures,
} from "e2e/support/helpers";
import { H } from "e2e/support";
import { setupSaml } from "./sso/shared/helpers.js";
describe("scenarios > admin > settings > authentication", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
describe("page layout", () => {
describe("oss", { tags: "@OSS" }, () => {
it("should implement a tab layout for oss customers", () => {
onlyOnOSS();
H.onlyOnOSS();
cy.visit("/admin/settings/authentication");
@@ -35,16 +28,16 @@ describe("scenarios > admin > settings > authentication", () => {
cy.findByRole("tab").should("not.exist");
// no tabs on api keys
cy.visit("/admin/settings/authentication/api-keys");
main().within(() => {
H.main().within(() => {
cy.findByText("Manage API Keys");
});
cy.findByRole("tab").should("not.exist");
});
});
describeEE("ee", () => {
H.describeEE("ee", () => {
it("should implement a tab layout for enterprise customers", () => {
setTokenFeatures("all");
H.setTokenFeatures("all");
cy.visit("/admin/settings/authentication");
@@ -79,23 +72,23 @@ describe("scenarios > admin > settings > authentication", () => {
describe("scenarios > admin > settings > user provisioning", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
describe("oss", { tags: "@OSS" }, () => {
it("user provisioning page should not be availble for OSS customers", () => {
onlyOnOSS();
H.onlyOnOSS();
cy.visit("/admin/settings/authentication/user-provisioning");
main().within(() => {
H.main().within(() => {
cy.findByText("We're a little lost...");
});
});
});
describeEE("scim settings management", () => {
H.describeEE("scim settings management", () => {
beforeEach(() => {
setTokenFeatures("all");
H.setTokenFeatures("all");
});
it("should be able to setup and manage scim feature", () => {
@@ -112,7 +105,7 @@ describe("scenarios > admin > settings > user provisioning", () => {
cy.log(
"should not show endpoint and token inputs if scim has never been enabled before",
);
main().within(() => {
H.main().within(() => {
scimEndpointInput().should("not.exist");
scimTokenInput().should("not.exist");
});
@@ -123,7 +116,7 @@ describe("scenarios > admin > settings > user provisioning", () => {
let initialUnmaskedToken = "";
cy.log("should show unmasked info in modal");
modal().within(() => {
H.modal().within(() => {
cy.findByText("Here's what you'll need to set SCIM up").should("exist");
scimEndpointInput().invoke("val").should("contain", "/api/ee/scim/v2");
scimTokenInput()
@@ -153,13 +146,13 @@ describe("scenarios > admin > settings > user provisioning", () => {
cy.log("should be able to regenerate a token");
cy.findByRole("button", { name: /Regenerate/ }).click();
modal().within(() => {
H.modal().within(() => {
cy.findByText("Regenerate token?").should("exist");
cy.findByRole("button", { name: /Regenerate now/ }).click();
});
let regeneratedToken = "";
modal().within(() => {
H.modal().within(() => {
cy.findByText("Copy and save the SCIM token").should("exist");
scimTokenInput()
.invoke("val")
@@ -178,11 +171,11 @@ describe("scenarios > admin > settings > user provisioning", () => {
cy.log("should be able to cancel regenerating a token");
cy.findByRole("button", { name: /Regenerate/ }).click();
modal().within(() => {
H.modal().within(() => {
cy.findByText("Regenerate token?").should("exist");
cy.findByRole("button", { name: /Cancel/ }).click();
});
modal().should("not.exist");
H.modal().should("not.exist");
scimTokenInput()
.invoke("val")
@@ -209,7 +202,7 @@ describe("scenarios > admin > settings > user provisioning", () => {
const samlWarningMessage =
"When enabled, SAML user provisioning will be turned off in favor of SCIM.";
main().within(() => {
H.main().within(() => {
cy.log("message should exist while scim has never been enabled");
cy.findByText(samlWarningMessage).should("exist");
@@ -219,11 +212,11 @@ describe("scenarios > admin > settings > user provisioning", () => {
scimToggle().should("be.checked");
});
modal().within(() => {
H.modal().within(() => {
cy.findByRole("button", { name: /Done/ }).click();
});
main().within(() => {
H.main().within(() => {
cy.findByText(samlWarningMessage).should("not.exist");
cy.log(
@@ -239,7 +232,7 @@ describe("scenarios > admin > settings > user provisioning", () => {
cy.log("should show error when scim token fails to load");
cy.intercept("GET", "/api/ee/scim/api_key", { statusCode: 500 });
cy.visit("/admin/settings/authentication/user-provisioning");
main().within(() => {
H.main().within(() => {
cy.findByText("Error fetching SCIM token");
});
@@ -252,7 +245,7 @@ describe("scenarios > admin > settings > user provisioning", () => {
});
cy.request("PUT", "api/setting/scim-enabled", { value: true });
cy.visit("/admin/settings/authentication/user-provisioning");
main().within(() => {
H.main().within(() => {
cy.findByText("Token failed to generate, please regenerate one.");
});
@@ -263,12 +256,12 @@ describe("scenarios > admin > settings > user provisioning", () => {
});
cy.findByRole("button", { name: /Regenerate/ }).click();
modal().within(() => {
H.modal().within(() => {
cy.findByText("Regenerate token?").should("exist");
cy.findByRole("button", { name: /Regenerate now/ }).click();
});
modal().within(() => {
H.modal().within(() => {
cy.findByText("An error occurred");
});
});

View File

@@ -1,25 +1,16 @@
import { H } from "e2e/support";
import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data";
import {
commandPaletteAction,
commandPaletteButton,
commandPaletteInput,
createQuestion,
modal,
restore,
visitDashboard,
visitFullAppEmbeddingUrl,
} from "e2e/support/helpers";
const downloadsFolder = Cypress.config("downloadsFolder");
describe("error reporting modal", () => {
beforeEach(() => {
cy.deleteDownloadsFolder();
restore();
H.restore();
});
it('should show an error reporting modal when pressing "Ctrl + F1" on the home page', () => {
restore();
H.restore();
cy.signInAsAdmin();
cy.visit("/");
@@ -30,7 +21,7 @@ describe("error reporting modal", () => {
cy.realPress(["Control", "F1"]);
modal().within(() => {
H.modal().within(() => {
cy.findByText("Download diagnostic information").should("be.visible");
cy.button(/Download/i).click();
});
@@ -46,7 +37,7 @@ describe("error reporting modal", () => {
});
it("should allow you to open the error reporting modal via the command palette", () => {
restore();
H.restore();
cy.signInAsAdmin();
cy.visit("/");
@@ -54,9 +45,9 @@ describe("error reporting modal", () => {
.findByText(/see what metabase can do/i)
.should("exist");
commandPaletteButton().click();
commandPaletteInput().type("Error");
commandPaletteAction(/Open error diagnostic modal/).click();
H.commandPaletteButton().click();
H.commandPaletteInput().type("Error");
H.commandPaletteAction(/Open error diagnostic modal/).click();
cy.findByRole("dialog", { name: "Download diagnostic information" }).should(
"be.visible",
@@ -64,9 +55,9 @@ describe("error reporting modal", () => {
});
it("should not show error reporting modal in embedding", () => {
restore();
H.restore();
cy.signInAsAdmin();
visitFullAppEmbeddingUrl({
H.visitFullAppEmbeddingUrl({
url: "/",
qs: {
top_nav: true,
@@ -80,12 +71,12 @@ describe("error reporting modal", () => {
cy.realPress(["Control", "F1"]);
modal().should("not.exist");
H.modal().should("not.exist");
});
it("should include question-specific data when triggered on the question page", () => {
cy.signInAsAdmin();
createQuestion(
H.createQuestion(
{
name: "Diagnostic Question 1",
query: { "source-table": 1, limit: 10 },
@@ -96,7 +87,7 @@ describe("error reporting modal", () => {
cy.findByTestId("TableInteractive-root").realClick();
cy.realPress(["Control", "F1"]);
modal().within(() => {
H.modal().within(() => {
cy.findByText("Download diagnostic information").should("be.visible");
cy.findByLabelText("Query results").should("not.be.checked");
cy.button(/Download/i).click();
@@ -117,7 +108,7 @@ describe("error reporting modal", () => {
cy.realPress(["Control", "F1"]);
modal().within(() => {
H.modal().within(() => {
cy.findByText("Download diagnostic information").should("be.visible");
cy.findByLabelText("Query results").should("not.be.checked");
cy.findByLabelText("Query results").click(); // off by default
@@ -139,7 +130,7 @@ describe("error reporting modal", () => {
it("can include query data on question pages", () => {
cy.signInAsAdmin();
createQuestion(
H.createQuestion(
{
name: "Diagnostic Question 1",
query: { "source-table": 1, limit: 10 },
@@ -150,7 +141,7 @@ describe("error reporting modal", () => {
cy.findByTestId("TableInteractive-root").realClick();
cy.realPress(["Control", "F1"]);
modal().within(() => {
H.modal().within(() => {
cy.findByText("Download diagnostic information").should("be.visible");
cy.findByLabelText("Query results").should("not.be.checked");
cy.findByLabelText("Query results").click(); // off by default
@@ -172,12 +163,12 @@ describe("error reporting modal", () => {
it("should not include backend logs for non-admin users", () => {
cy.signInAsNormalUser();
visitDashboard(ORDERS_DASHBOARD_ID);
H.visitDashboard(ORDERS_DASHBOARD_ID);
cy.findByTestId("dashboard-grid").realClick();
cy.realPress(["Control", "F1"]);
modal().within(() => {
H.modal().within(() => {
cy.findByText("Download diagnostic information").should("be.visible");
cy.findByLabelText("Dashboard definition").should("be.visible");
cy.findByLabelText("Query results").should("not.exist");

View File

@@ -1,24 +1,12 @@
import _ from "underscore";
import { H } from "e2e/support";
import { USERS, USER_GROUPS } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
COLLECTION_GROUP_ID,
NORMAL_USER_ID,
} from "e2e/support/cypress_sample_instance_data";
import {
createAlert,
createApiKey,
createPulse,
describeEE,
getCurrentUser,
getFullName,
modal,
popover,
restore,
setTokenFeatures,
setupSMTP,
} from "e2e/support/helpers";
const { sandboxed, normal, admin, nodata, nocollection } = USERS;
const { ALL_USERS_GROUP, DATA_GROUP } = USER_GROUPS;
@@ -34,9 +22,9 @@ const TEST_USER = {
password: "12341234",
};
const adminUserName = getFullName(admin);
const noCollectionUserName = getFullName(nocollection);
const normalUserName = getFullName(normal);
const adminUserName = H.getFullName(admin);
const noCollectionUserName = H.getFullName(nocollection);
const normalUserName = H.getFullName(normal);
const totalUsers = Object.keys(USERS).length;
@@ -44,7 +32,7 @@ describe("scenarios > admin > people", () => {
beforeEach(() => {
cy.intercept("GET", "/api/permissions/group").as("getGroups");
cy.intercept("GET", "/api/api-key").as("listApiKeys");
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -122,7 +110,7 @@ describe("scenarios > admin > people", () => {
showUserOptions(noCollectionUserName);
popover().findByText("Deactivate user").click();
H.popover().findByText("Deactivate user").click();
clickButton("Deactivate");
@@ -211,7 +199,7 @@ describe("scenarios > admin > people", () => {
it("should disallow admin to deactivate themselves", () => {
cy.visit("/admin/people");
showUserOptions(adminUserName);
popover().within(() => {
H.popover().within(() => {
cy.findByText("Edit user");
cy.findByText("Reset password");
cy.findByText("Deactivate user").should("not.exist");
@@ -223,7 +211,7 @@ describe("scenarios > admin > people", () => {
cy.request("PUT", `/api/user/${NORMAL_USER_ID}`, {
is_superuser: true,
}).then(({ body: user }) => {
const FULL_NAME = getFullName(user);
const FULL_NAME = H.getFullName(user);
cy.visit("/admin/people");
showUserOptions(FULL_NAME);
@@ -256,7 +244,7 @@ describe("scenarios > admin > people", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Edit user").click();
modal().within(() => {
H.modal().within(() => {
cy.log("Should display error messages (metabase#46449)");
cy.findByLabelText("First name").click().clear().type(" ");
clickButton("Update");
@@ -288,7 +276,7 @@ describe("scenarios > admin > people", () => {
});
it("should not offer to reset passwords when password login is disabled", () => {
setTokenFeatures("all");
H.setTokenFeatures("all");
cy.request("PUT", "/api/google/settings", {
"google-auth-auto-create-accounts-domain": null,
"google-auth-client-id": "example1.apps.googleusercontent.com",
@@ -300,14 +288,14 @@ describe("scenarios > admin > people", () => {
});
cy.visit("/admin/people");
showUserOptions(normalUserName);
popover().findByText("Reset password").should("not.exist");
H.popover().findByText("Reset password").should("not.exist");
});
it(
"should reset user password with SMTP set up",
{ tags: "@external" },
() => {
setupSMTP();
H.setupSMTP();
cy.visit("/admin/people");
showUserOptions(normalUserName);
@@ -360,8 +348,8 @@ describe("scenarios > admin > people", () => {
cy.findByText("My New Group").closest("tr").icon("ellipsis").click();
});
popover().findByText("Remove Group").click();
modal().button("Remove group").click();
H.popover().findByText("Remove Group").click();
H.modal().button("Remove group").click();
cy.wait(["@deleteGroup", "@getGroups"]);
cy.findByTestId("admin-panel")
@@ -370,7 +358,7 @@ describe("scenarios > admin > people", () => {
});
it("should display api keys included in a group and display a warning when deleting the group", () => {
createApiKey("MyApiKey", COLLECTION_GROUP_ID);
H.createApiKey("MyApiKey", COLLECTION_GROUP_ID);
cy.visit("/admin/people/groups");
cy.wait(["@getGroups", "@listApiKeys"]);
@@ -385,9 +373,9 @@ describe("scenarios > admin > people", () => {
.icon("ellipsis")
.click();
popover().findByText("Remove Group").click();
H.popover().findByText("Remove Group").click();
modal().within(() => {
H.modal().within(() => {
cy.findByText(
"Are you sure you want remove this group and its API key?",
);
@@ -407,7 +395,7 @@ describe("scenarios > admin > people", () => {
describe("email configured", { tags: "@external" }, () => {
beforeEach(() => {
// Setup email server, since we show different modal message when email isn't configured
setupSMTP();
H.setupSMTP();
setupGoogleAuth();
});
@@ -534,20 +522,20 @@ describe("scenarios > admin > people", () => {
});
});
describeEE("scenarios > admin > people", () => {
H.describeEE("scenarios > admin > people", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
});
it("should unsubscribe a user from all subscriptions and alerts", () => {
getCurrentUser().then(({ body: { id: user_id } }) => {
H.getCurrentUser().then(({ body: { id: user_id } }) => {
cy.createQuestionAndDashboard({
questionDetails: getQuestionDetails(),
}).then(({ body: { card_id, dashboard_id } }) => {
createAlert(getAlertDetails({ user_id, card_id }));
createPulse(getPulseDetails({ card_id, dashboard_id }));
H.createAlert(getAlertDetails({ user_id, card_id }));
H.createPulse(getPulseDetails({ card_id, dashboard_id }));
});
});
@@ -560,11 +548,11 @@ describeEE("scenarios > admin > people", () => {
cy.visit("/admin/people");
showUserOptions(adminUserName);
popover().within(() => {
H.popover().within(() => {
cy.findByText("Unsubscribe from all subscriptions / alerts").click();
});
modal().within(() => {
H.modal().within(() => {
cy.findAllByText(adminUserName, { exact: false });
cy.findByText("Unsubscribe").click();
cy.findByText("Unsubscribe").should("not.exist");
@@ -579,7 +567,7 @@ describeEE("scenarios > admin > people", () => {
});
it("invite member when SSO is configured metabase#23630", () => {
setupSMTP();
H.setupSMTP();
setupGoogleAuth();
cy.request("PUT", "/api/setting", { "enable-password-login": false });
@@ -618,9 +606,9 @@ describeEE("scenarios > admin > people", () => {
});
});
describeEE("scenarios > admin > people > group managers", () => {
H.describeEE("scenarios > admin > people > group managers", () => {
function confirmLosingAbilityToManageGroup() {
modal().within(() => {
H.modal().within(() => {
cy.findByText(
"You will not be able to manage users of this group anymore.",
);
@@ -635,9 +623,9 @@ describeEE("scenarios > admin > people > group managers", () => {
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
cy.visit("/admin/people");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -743,7 +731,7 @@ describeEE("scenarios > admin > people > group managers", () => {
});
// Add the user to a group
popover().within(() => {
H.popover().within(() => {
cy.findByText("collection").click();
});
cy.get("@userRow").within(() => {
@@ -751,7 +739,7 @@ describeEE("scenarios > admin > people > group managers", () => {
});
// Remove the user from the group
popover().within(() => {
H.popover().within(() => {
cy.findByText("collection").click();
});
cy.get("@userRow").within(() => {
@@ -759,7 +747,7 @@ describeEE("scenarios > admin > people > group managers", () => {
});
// Promote and then demote the user
popover().within(() => {
H.popover().within(() => {
cy.icon("arrow_up").click();
cy.icon("arrow_down").click();
});
@@ -773,13 +761,13 @@ describeEE("scenarios > admin > people > group managers", () => {
});
// Demote myself from being manager
popover().within(() => {
H.popover().within(() => {
cy.icon("arrow_down").eq(0).click();
});
confirmLosingAbilityToManageGroup();
// Remove myself from another group
popover().within(() => {
H.popover().within(() => {
cy.findByText("data").click();
});
confirmLosingAbilityToManageGroup();
@@ -800,7 +788,7 @@ describeEE("scenarios > admin > people > group managers", () => {
});
});
describeEE("issue 23689", () => {
H.describeEE("issue 23689", () => {
function findUserByFullName(user) {
const { first_name, last_name } = user;
return cy.findByText(`${first_name} ${last_name}`);
@@ -817,9 +805,9 @@ describeEE("issue 23689", () => {
cy.intercept("GET", "/api/permissions/membership").as("membership");
restore();
H.restore();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
visitGroupPermissionsPage(COLLECTION_GROUP);

View File

@@ -1,3 +1,4 @@
import { H } from "e2e/support";
import {
SAMPLE_DB_ID,
SAMPLE_DB_SCHEMA_ID,
@@ -5,33 +6,6 @@ import {
} from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import { ORDERS_QUESTION_ID } from "e2e/support/cypress_sample_instance_data";
import {
WEBHOOK_TEST_URL,
describeEE,
describeWithSnowplow,
echartsContainer,
expectGoodSnowplowEvent,
expectNoBadSnowplowEvents,
isEE,
isOSS,
main,
modal,
onlyOnOSS,
openNativeEditor,
openOrdersTable,
popover,
resetSnowplow,
restore,
runNativeQuery,
setTokenFeatures,
setupMetabaseCloud,
setupSMTP,
tableHeaderClick,
undoToast,
updateSetting,
visitQuestion,
visitQuestionAdhoc,
} from "e2e/support/helpers";
import {
WEBHOOK_TEST_DASHBOARD,
@@ -42,10 +16,10 @@ import {
const { ORDERS, ORDERS_ID } = SAMPLE_DATABASE;
const { SMTP_PORT, WEB_PORT } = WEBMAIL_CONFIG;
describeWithSnowplow("scenarios > admin > settings", () => {
H.describeWithSnowplow("scenarios > admin > settings", () => {
beforeEach(() => {
resetSnowplow();
restore();
H.resetSnowplow();
H.restore();
cy.signInAsAdmin();
});
@@ -53,11 +27,11 @@ describeWithSnowplow("scenarios > admin > settings", () => {
"should prompt admin to migrate to a hosted instance",
{ tags: "@OSS" },
() => {
onlyOnOSS();
H.onlyOnOSS();
cy.visit("/admin/settings/setup");
cy.findByTestId("upsell-card").findByText(/Migrate to Metabase Cloud/);
expectGoodSnowplowEvent({
H.expectGoodSnowplowEvent({
event: "upsell_viewed",
promoted_feature: "hosting",
});
@@ -65,11 +39,11 @@ describeWithSnowplow("scenarios > admin > settings", () => {
.findAllByRole("link", { name: "Learn more" })
.click();
// link opens in new tab
expectGoodSnowplowEvent({
H.expectGoodSnowplowEvent({
event: "upsell_clicked",
promoted_feature: "hosting",
});
expectNoBadSnowplowEvents();
H.expectNoBadSnowplowEvents();
},
);
@@ -99,7 +73,7 @@ describeWithSnowplow("scenarios > admin > settings", () => {
// If we update UI in the future (for example: we show an error within a popup/modal), the test in current form could fail.
cy.log("Making sure we display an error message in UI");
// Same reasoning for regex as above
undoToast().contains(/^Error: Invalid site URL/);
H.undoToast().contains(/^Error: Invalid site URL/);
});
it("should save a setting", () => {
@@ -152,13 +126,13 @@ describeWithSnowplow("scenarios > admin > settings", () => {
.parent()
.findByTestId("select-button")
.click();
popover().contains("https://").click({ force: true });
H.popover().contains("https://").click({ force: true });
cy.wait("@httpsCheck");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains("Redirect to HTTPS").parent().parent().contains("Disabled");
restore(); // avoid leaving https site url
H.restore(); // avoid leaving https site url
});
it("should display an error if the https redirect check fails", () => {
@@ -174,7 +148,7 @@ describeWithSnowplow("scenarios > admin > settings", () => {
.parent()
.findByTestId("select-button")
.click();
popover().contains("https://").click({ force: true });
H.popover().contains("https://").click({ force: true });
cy.wait("@httpsCheck");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -194,7 +168,7 @@ describeWithSnowplow("scenarios > admin > settings", () => {
.findByText("January 31, 2018")
.click({ force: true });
popover().findByText("2018/1/31").click({ force: true });
H.popover().findByText("2018/1/31").click({ force: true });
cy.wait("@saveFormatting");
cy.findAllByTestId("select-button-content").should("contain", "2018/1/31");
@@ -205,7 +179,7 @@ describeWithSnowplow("scenarios > admin > settings", () => {
cy.wait("@saveFormatting");
cy.findByDisplayValue("HH:mm").should("be.checked");
openOrdersTable({ limit: 2 });
H.openOrdersTable({ limit: 2 });
cy.findByTextEnsureVisible("Created At");
cy.get("[data-testid=cell-data]")
@@ -222,7 +196,7 @@ describeWithSnowplow("scenarios > admin > settings", () => {
cy.wait("@saveFormatting");
cy.findByDisplayValue("h:mm A").should("be.checked");
openOrdersTable({ limit: 2 });
H.openOrdersTable({ limit: 2 });
cy.findByTextEnsureVisible("Created At");
cy.get("[data-testid=cell-data]").and("contain", "2025/2/11, 9:40 PM");
@@ -245,7 +219,7 @@ describeWithSnowplow("scenarios > admin > settings", () => {
});
// Open the orders table
openOrdersTable({ limit: 2 });
H.openOrdersTable({ limit: 2 });
cy.get("#main-data-grid").within(() => {
// Items in the total column should have a leading dollar sign
@@ -309,9 +283,9 @@ describeWithSnowplow("scenarios > admin > settings", () => {
"should display the order of the settings items consistently between OSS/EE versions (metabase#15441)",
{ tags: "@OSS" },
() => {
isEE && setTokenFeatures("all");
H.isEE && H.setTokenFeatures("all");
const lastItem = isOSS ? "Cloud" : "Appearance";
const lastItem = H.isOSS ? "Cloud" : "Appearance";
cy.visit("/admin/settings/setup");
cy.findByTestId("admin-list-settings-items").within(() => {
@@ -324,7 +298,7 @@ describeWithSnowplow("scenarios > admin > settings", () => {
// Unskip when mocking Cloud in Cypress is fixed (#18289)
it.skip("should hide self-hosted settings when running Metabase Cloud", () => {
setupMetabaseCloud();
H.setupMetabaseCloud();
cy.visit("/admin/settings/general");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -340,7 +314,7 @@ describeWithSnowplow("scenarios > admin > settings", () => {
// Unskip when mocking Cloud in Cypress is fixed (#18289)
it.skip("should hide the store link when running Metabase Cloud", () => {
setupMetabaseCloud();
H.setupMetabaseCloud();
cy.visit("/admin/settings/general");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -368,8 +342,8 @@ describeWithSnowplow("scenarios > admin > settings", () => {
describe("scenarios > admin > settings (OSS)", { tags: "@OSS" }, () => {
beforeEach(() => {
onlyOnOSS();
restore();
H.onlyOnOSS();
H.restore();
cy.signInAsAdmin();
});
@@ -382,16 +356,16 @@ describe("scenarios > admin > settings (OSS)", { tags: "@OSS" }, () => {
});
});
describeEE("scenarios > admin > settings (EE)", () => {
H.describeEE("scenarios > admin > settings (EE)", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
});
// Unskip when mocking Cloud in Cypress is fixed (#18289)
it.skip("should hide Enterprise page when running Metabase Cloud", () => {
setupMetabaseCloud();
H.setupMetabaseCloud();
cy.visit("/admin/settings/general");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -438,7 +412,7 @@ describe.skip(
cy.findByLabelText("Name").type(name);
modal().button("Save").click();
H.modal().button("Save").click();
cy.findByText("Not now").click();
@@ -479,7 +453,7 @@ describe.skip(
cy.intercept("POST", "/api/dataset").as("dataset");
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
restore("postgres-12");
H.restore("postgres-12");
cy.signInAsAdmin();
});
@@ -496,8 +470,8 @@ describe.skip(
cy.findByText("Saved");
// Run the query and save the question
openNativeEditor({ databaseName: "QA Postgres12" }).type(nativeQuery);
runNativeQuery();
H.openNativeEditor({ databaseName: "QA Postgres12" }).type(nativeQuery);
H.runNativeQuery();
getCellText().then(res => {
cy.wrap(res).as("tempResult");
@@ -531,7 +505,7 @@ describe.skip(
describe("Cloud settings section", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -539,7 +513,7 @@ describe("Cloud settings section", () => {
// Setting to none will give us an instance where token-features.hosting is set to true
// Allowing us to pretend that we are a hosted instance (seems backwards though haha)
setTokenFeatures("none");
H.setTokenFeatures("none");
cy.visit("/admin");
cy.findByTestId("admin-list-settings-items").findByText("Cloud").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -553,7 +527,7 @@ describe("Cloud settings section", () => {
});
it("should prompt us to migrate to cloud if we are not hosted", () => {
setTokenFeatures("all");
H.setTokenFeatures("all");
cy.visit("/admin");
cy.findByTestId("admin-list-settings-items").findByText("Cloud").click();
@@ -567,7 +541,7 @@ describe("Cloud settings section", () => {
describe("scenarios > admin > settings > email settings", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -588,7 +562,7 @@ describe("scenarios > admin > settings > email settings", () => {
// SMTP settings need to manually be saved
cy.intercept("PUT", "api/email").as("smtpSaved");
main().within(() => {
H.main().within(() => {
cy.findByText("Save changes").click();
});
cy.wait("@smtpSaved");
@@ -646,7 +620,7 @@ describe("scenarios > admin > settings > email settings", () => {
cy.button("Save changes").should("be.disabled");
// should be able to clear email settings
main().findByText("Clear").click();
H.main().findByText("Clear").click();
cy.reload();
@@ -670,7 +644,7 @@ describe("scenarios > admin > settings > email settings", () => {
"email-smtp-username": null,
});
cy.visit("/admin/settings/email/smtp");
main().findByText("Send test email").click();
H.main().findByText("Send test email").click();
cy.findAllByText(
"Couldn't connect to host, port: localhost, 1234; timeout -1",
);
@@ -680,10 +654,10 @@ describe("scenarios > admin > settings > email settings", () => {
"should send a test email for a valid SMTP configuration",
{ tags: "@external" },
() => {
setupSMTP();
H.setupSMTP();
cy.visit("/admin/settings/email/smtp");
main().findByText("Send test email").click();
main().findByText("Sent!");
H.main().findByText("Send test email").click();
H.main().findByText("Sent!");
cy.request("GET", `http://localhost:${WEB_PORT}/email`).then(
({ body }) => {
@@ -711,11 +685,11 @@ describe("scenarios > admin > license and billing", () => {
};
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
describeEE("store info", () => {
H.describeEE("store info", () => {
it("should show the user a link to the store for an unlincensed enterprise instance", () => {
cy.visit("/admin/settings/license");
cy.findByTestId("license-and-billing-content")
@@ -724,7 +698,7 @@ describe("scenarios > admin > license and billing", () => {
});
it("should show the user store info for an self-hosted instance managed by the store", () => {
setTokenFeatures("all");
H.setTokenFeatures("all");
mockBillingTokenFeatures([
STORE_MANAGED_FEATURE_KEY,
NO_UPSELL_FEATURE_HEY,
@@ -767,7 +741,7 @@ describe("scenarios > admin > license and billing", () => {
});
it("should not show license input for cloud-hosted instances", () => {
setTokenFeatures("all");
H.setTokenFeatures("all");
mockBillingTokenFeatures([
STORE_MANAGED_FEATURE_KEY,
NO_UPSELL_FEATURE_HEY,
@@ -778,7 +752,7 @@ describe("scenarios > admin > license and billing", () => {
});
it("should render an error if something fails when fetching billing info", () => {
setTokenFeatures("all");
H.setTokenFeatures("all");
mockBillingTokenFeatures([
STORE_MANAGED_FEATURE_KEY,
NO_UPSELL_FEATURE_HEY,
@@ -797,11 +771,11 @@ describe("scenarios > admin > license and billing", () => {
describe("scenarios > admin > localization", () => {
function setFirstWeekDayTo(day) {
updateSetting("start-of-week", day.toLowerCase());
H.updateSetting("start-of-week", day.toLowerCase());
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setFirstWeekDayTo("monday");
});
@@ -833,7 +807,7 @@ describe("scenarios > admin > localization", () => {
cy.log("Assert the dates on the X axis");
// it's hard and tricky to invoke hover in Cypress, especially in our graphs
// that's why we have to assert on the x-axis, instead of a popover that shows on a dot hover
echartsContainer().get("text").contains("April 25, 2022");
H.echartsContainer().get("text").contains("April 25, 2022");
});
it("should display days on X-axis correctly when grouped by 'Day of the Week' (metabase#13604)", () => {
@@ -865,14 +839,14 @@ describe("scenarios > admin > localization", () => {
cy.findByText("13604").click();
cy.log("Reported failing on v0.37.0.2 and labeled as `.Regression`");
echartsContainer()
H.echartsContainer()
.get("text")
.contains(/sunday/i)
.should("not.exist");
echartsContainer()
H.echartsContainer()
.get("text")
.contains(/monday/i);
echartsContainer()
H.echartsContainer()
.get("text")
.contains(/tuesday/i);
});
@@ -939,9 +913,9 @@ describe("scenarios > admin > localization", () => {
cy.findByText("US Dollar").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Euro").click();
undoToast().findByText("Changes saved").should("be.visible");
H.undoToast().findByText("Changes saved").should("be.visible");
visitQuestionAdhoc({
H.visitQuestionAdhoc({
display: "scalar",
dataset_query: {
type: "native",
@@ -977,7 +951,7 @@ describe("scenarios > admin > localization", () => {
cy.findByText("January 31, 2018").click();
});
popover().findByText("2018/1/31").click();
H.popover().findByText("2018/1/31").click();
cy.wait("@updateFormatting");
cy.findByTestId("custom-formatting-setting").within(() => {
@@ -992,12 +966,12 @@ describe("scenarios > admin > localization", () => {
cy.findByDisplayValue("HH:mm").should("be.checked");
});
visitQuestion(ORDERS_QUESTION_ID);
H.visitQuestion(ORDERS_QUESTION_ID);
// create a date filter and set it to the 'On' view to see a specific date
tableHeaderClick("Created At");
H.tableHeaderClick("Created At");
popover().within(() => {
H.popover().within(() => {
cy.findByText("Filter by this column").click();
cy.findByText("Specific dates…").click();
cy.findByText("On").click();
@@ -1031,7 +1005,7 @@ describe("scenarios > admin > localization", () => {
describe("scenarios > admin > settings > map settings", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -1129,7 +1103,7 @@ describe("scenarios > admin > settings > map settings", () => {
cy.visit("/admin/settings/maps");
cy.button("Add a map").click();
modal().within(() => {
H.modal().within(() => {
// GeoJSON with an unsupported format (not a Feature or FeatureCollection)
cy.findByPlaceholderText("Like https://my-mb-server.com/maps/my-map.json")
.clear()
@@ -1146,7 +1120,7 @@ describe("scenarios > admin > settings > map settings", () => {
// docker run -p 9080:8080/tcp tarampampam/webhook-tester serve --create-session 00000000-0000-0000-0000-000000000000
describe("notifications", { tags: "@external" }, () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -1164,7 +1138,7 @@ describe("notifications", { tags: "@external" }, () => {
const COMMON_FIELDS = [
{
label: "Webhook URL",
value: WEBHOOK_TEST_URL,
value: H.WEBHOOK_TEST_URL,
},
{
label: "Give it a name",
@@ -1223,7 +1197,7 @@ describe("notifications", { tags: "@external" }, () => {
cy.visit("/admin/settings/notifications");
cy.findByRole("heading", { name: "Add a webhook" }).click();
modal().within(() => {
H.modal().within(() => {
COMMON_FIELDS.forEach(field => {
cy.findByLabelText(field.label).type(field.value);
});
@@ -1253,14 +1227,14 @@ describe("notifications", { tags: "@external" }, () => {
cy.findByRole("heading", { name: "Add a webhook" }).click();
modal().within(() => {
H.modal().within(() => {
cy.findByRole("heading", { name: "New webhook destination" }).should(
"exist",
);
cy.findByLabelText("Give it a name").type("Awesome Hook");
cy.findByLabelText("Description").type("The best hook ever");
cy.findByLabelText("Webhook URL").clear().type(WEBHOOK_TEST_URL);
cy.findByLabelText("Webhook URL").clear().type(H.WEBHOOK_TEST_URL);
cy.button("Create destination").click();
});
@@ -1268,7 +1242,7 @@ describe("notifications", { tags: "@external" }, () => {
cy.findByRole("heading", { name: "Awesome Hook" }).click();
modal().within(() => {
H.modal().within(() => {
cy.findByRole("heading", { name: "Edit this webhook" }).should("exist");
cy.findByLabelText("Give it a name").clear().type("Updated Hook");
cy.button("Save changes").click();
@@ -1276,7 +1250,7 @@ describe("notifications", { tags: "@external" }, () => {
cy.findByRole("heading", { name: "Updated Hook" }).click();
modal()
H.modal()
.button(/Delete this destination/)
.click();
@@ -1318,7 +1292,7 @@ describe("admin > settings > updates", () => {
const currentVersion = "v1.86.70";
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.visit("/admin/settings/updates");
@@ -1360,7 +1334,7 @@ describe("admin > settings > updates", () => {
cy.findByText("Stable releases").click();
});
popover().findByText("Beta releases").click();
H.popover().findByText("Beta releases").click();
cy.findByTestId("settings-updates").within(() => {
cy.findByText(/Metabase 1\.86\.75\.309 is available/).should(
@@ -1370,7 +1344,7 @@ describe("admin > settings > updates", () => {
cy.findByText("Beta releases").click();
});
popover().findByText("Nightly builds").click();
H.popover().findByText("Nightly builds").click();
cy.findByTestId("settings-updates").within(() => {
cy.findByText(/Metabase 1\.86\.75\.311 is available/).should(

View File

@@ -1,15 +1,10 @@
import {
modal,
popover,
restore,
typeAndBlurUsingLabel,
} from "e2e/support/helpers";
import { H } from "e2e/support";
const CLIENT_ID_SUFFIX = "apps.googleusercontent.com";
describe("scenarios > admin > settings > SSO > Google", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.intercept("PUT", "/api/setting").as("updateSettings");
cy.intercept("PUT", "/api/setting/*").as("updateSetting");
@@ -19,13 +14,13 @@ describe("scenarios > admin > settings > SSO > Google", () => {
it("should save the client id on subsequent tries (metabase#15974)", () => {
cy.visit("/admin/settings/authentication/google");
typeAndBlurUsingLabel("Client ID", "example1.apps.googleusercontent.com");
H.typeAndBlurUsingLabel("Client ID", "example1.apps.googleusercontent.com");
cy.button("Save and enable").click();
cy.wait("@updateGoogleSettings");
cy.reload();
cy.findByDisplayValue(`example1.${CLIENT_ID_SUFFIX}`).should("be.visible");
typeAndBlurUsingLabel("Client ID", `example2.${CLIENT_ID_SUFFIX}`);
H.typeAndBlurUsingLabel("Client ID", `example2.${CLIENT_ID_SUFFIX}`);
cy.button("Save changes").click();
cy.wait("@updateGoogleSettings");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -37,12 +32,12 @@ describe("scenarios > admin > settings > SSO > Google", () => {
cy.visit("/admin/settings/authentication");
getGoogleCard().icon("ellipsis").click();
popover().findByText("Pause").click();
H.popover().findByText("Pause").click();
cy.wait("@updateSetting");
getGoogleCard().findByText("Paused").should("exist");
getGoogleCard().icon("ellipsis").click();
popover().findByText("Resume").click();
H.popover().findByText("Resume").click();
cy.wait("@updateSetting");
getGoogleCard().findByText("Active").should("exist");
});
@@ -52,8 +47,8 @@ describe("scenarios > admin > settings > SSO > Google", () => {
cy.visit("/admin/settings/authentication");
getGoogleCard().icon("ellipsis").click();
popover().findByText("Deactivate").click();
modal().button("Deactivate").click();
H.popover().findByText("Deactivate").click();
H.modal().button("Deactivate").click();
cy.wait("@updateSettings");
getGoogleCard().findByText("Set up").should("exist");
@@ -62,7 +57,7 @@ describe("scenarios > admin > settings > SSO > Google", () => {
it("should show an error message if the client id does not end with the correct suffix (metabase#15975)", () => {
cy.visit("/admin/settings/authentication/google");
typeAndBlurUsingLabel("Client ID", "fake-client-id");
H.typeAndBlurUsingLabel("Client ID", "fake-client-id");
cy.button("Save and enable").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(

View File

@@ -1,11 +1,4 @@
import {
describeEE,
modal,
popover,
restore,
setTokenFeatures,
typeAndBlurUsingLabel,
} from "e2e/support/helpers";
import { H } from "e2e/support";
import { enableJwtAuth } from "e2e/support/helpers/e2e-jwt-helpers";
import {
@@ -14,11 +7,11 @@ import {
} from "./shared/group-mappings-widget";
import { getSuccessUi, getUserProvisioningInput } from "./shared/helpers";
describeEE("scenarios > admin > settings > SSO > JWT", () => {
H.describeEE("scenarios > admin > settings > SSO > JWT", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
cy.intercept("PUT", "/api/setting").as("updateSettings");
cy.intercept("PUT", "/api/setting/*").as("updateSetting");
});
@@ -39,12 +32,12 @@ describeEE("scenarios > admin > settings > SSO > JWT", () => {
cy.visit("/admin/settings/authentication");
getJwtCard().icon("ellipsis").click();
popover().findByText("Pause").click();
H.popover().findByText("Pause").click();
cy.wait("@updateSetting");
getJwtCard().findByText("Paused").should("exist");
getJwtCard().icon("ellipsis").click();
popover().findByText("Resume").click();
H.popover().findByText("Resume").click();
cy.wait("@updateSetting");
getJwtCard().findByText("Active").should("exist");
});
@@ -65,8 +58,8 @@ describeEE("scenarios > admin > settings > SSO > JWT", () => {
cy.visit("/admin/settings/authentication");
getJwtCard().icon("ellipsis").click();
popover().findByText("Deactivate").click();
modal().button("Deactivate").click();
H.popover().findByText("Deactivate").click();
H.modal().button("Deactivate").click();
cy.wait("@updateSettings");
getJwtCard().findByText("Set up").should("exist");
@@ -77,7 +70,7 @@ describeEE("scenarios > admin > settings > SSO > JWT", () => {
cy.visit("/admin/settings/authentication/jwt");
cy.button("Regenerate key").click();
modal().within(() => {
H.modal().within(() => {
cy.findByText("Regenerate JWT signing key?").should("exist");
cy.findByText(
"This will cause existing tokens to stop working until the identity provider is updated with the new key.",
@@ -116,6 +109,6 @@ const getJwtCard = () => {
};
const enterJwtSettings = () => {
typeAndBlurUsingLabel(/JWT Identity Provider URI/, "https://example.test");
H.typeAndBlurUsingLabel(/JWT Identity Provider URI/, "https://example.test");
cy.button("Generate key").click();
};

View File

@@ -1,13 +1,4 @@
import {
describeEE,
modal,
popover,
restore,
setTokenFeatures,
setupLdap,
typeAndBlurUsingLabel,
updateSetting,
} from "e2e/support/helpers";
import { H } from "e2e/support";
import {
checkGroupConsistencyAfterDeletingMappings,
@@ -20,7 +11,7 @@ describe(
{ tags: "@external" },
() => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.intercept("PUT", "/api/setting").as("updateSettings");
cy.intercept("PUT", "/api/setting/*").as("updateSetting");
@@ -40,7 +31,7 @@ describe(
});
it("should update ldap settings", () => {
setupLdap();
H.setupLdap();
cy.visit("/admin/settings/authentication/ldap");
enterLdapPort("389");
@@ -52,22 +43,22 @@ describe(
});
it("should allow to disable and enable ldap", () => {
setupLdap();
H.setupLdap();
cy.visit("/admin/settings/authentication");
getLdapCard().icon("ellipsis").click();
popover().findByText("Pause").click();
H.popover().findByText("Pause").click();
cy.wait("@updateSetting");
getLdapCard().findByText("Paused").should("exist");
getLdapCard().icon("ellipsis").click();
popover().findByText("Resume").click();
H.popover().findByText("Resume").click();
cy.wait("@updateSetting");
getLdapCard().findByText("Active").should("exist");
});
it("should not show the user provision UI to OSS users", () => {
setupLdap();
H.setupLdap();
cy.visit("/admin/settings/authentication/ldap");
cy.findByTestId("admin-layout-content")
@@ -76,12 +67,12 @@ describe(
});
it("should allow to reset ldap settings", () => {
setupLdap();
H.setupLdap();
cy.visit("/admin/settings/authentication");
getLdapCard().icon("ellipsis").click();
popover().findByText("Deactivate").click();
modal().button("Deactivate").click();
H.popover().findByText("Deactivate").click();
H.modal().button("Deactivate").click();
cy.wait("@updateSettings");
getLdapCard().findByText("Set up").should("exist");
@@ -127,7 +118,7 @@ describe(
});
it("should allow user login on OSS when LDAP is enabled", () => {
setupLdap();
H.setupLdap();
cy.signOut();
cy.visit("/auth/login");
cy.findByLabelText("Username or email address").type(
@@ -163,19 +154,19 @@ describe(
},
);
describeEE(
H.describeEE(
"scenarios > admin > settings > SSO > LDAP",
{ tags: "@external" },
() => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
cy.intercept("PUT", "/api/ldap/settings").as("updateLdapSettings");
});
it("should allow the user to enable/disable user provisioning", () => {
setupLdap();
H.setupLdap();
cy.visit("/admin/settings/authentication/ldap");
const { label, input } = getUserProvisioningInput();
@@ -188,8 +179,8 @@ describeEE(
});
it("should show the login form when ldap is enabled but password login isn't (metabase#25661)", () => {
setupLdap();
updateSetting("enable-password-login", false);
H.setupLdap();
H.updateSetting("enable-password-login", false);
cy.signOut();
cy.visit("/auth/login");
@@ -200,7 +191,7 @@ describeEE(
});
it("should allow user login on EE when LDAP is enabled", () => {
setupLdap();
H.setupLdap();
cy.signOut();
cy.visit("/auth/login");
cy.findByLabelText("Username or email address").type(
@@ -224,7 +215,7 @@ describeEE(
cy.icon("ellipsis").click();
});
});
popover().within(() => {
H.popover().within(() => {
cy.findByText("Edit user").click();
});
cy.findByDisplayValue("uid").should("exist");
@@ -238,13 +229,13 @@ const getLdapCard = () => {
};
const enterLdapPort = value => {
typeAndBlurUsingLabel("LDAP Port", value);
H.typeAndBlurUsingLabel("LDAP Port", value);
};
const enterLdapSettings = () => {
typeAndBlurUsingLabel(/LDAP Host/, "localhost");
typeAndBlurUsingLabel("LDAP Port", "389");
typeAndBlurUsingLabel("Username or DN", "cn=admin,dc=example,dc=org");
typeAndBlurUsingLabel("Password", "adminpass");
typeAndBlurUsingLabel(/User search base/, "ou=users,dc=example,dc=org");
H.typeAndBlurUsingLabel(/LDAP Host/, "localhost");
H.typeAndBlurUsingLabel("LDAP Port", "389");
H.typeAndBlurUsingLabel("Username or DN", "cn=admin,dc=example,dc=org");
H.typeAndBlurUsingLabel("Password", "adminpass");
H.typeAndBlurUsingLabel(/User search base/, "ou=users,dc=example,dc=org");
};

View File

@@ -1,11 +1,4 @@
import {
describeEE,
modal,
popover,
restore,
setTokenFeatures,
typeAndBlurUsingLabel,
} from "e2e/support/helpers";
import { H } from "e2e/support";
import {
checkGroupConsistencyAfterDeletingMappings,
@@ -18,11 +11,11 @@ import {
setupSaml,
} from "./shared/helpers";
describeEE("scenarios > admin > settings > SSO > SAML", () => {
H.describeEE("scenarios > admin > settings > SSO > SAML", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
cy.intercept("PUT", "/api/setting").as("updateSettings");
cy.intercept("PUT", "/api/setting/*").as("updateSetting");
cy.intercept("PUT", "/api/saml/settings").as("updateSamlSettings");
@@ -45,7 +38,7 @@ describeEE("scenarios > admin > settings > SSO > SAML", () => {
setupSaml();
cy.visit("/admin/settings/authentication/saml");
typeAndBlurUsingLabel(/SAML Identity Provider URL/, "https://other.test");
H.typeAndBlurUsingLabel(/SAML Identity Provider URL/, "https://other.test");
cy.button("Save changes").click();
cy.wait("@updateSamlSettings");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -60,12 +53,12 @@ describeEE("scenarios > admin > settings > SSO > SAML", () => {
cy.visit("/admin/settings/authentication");
getSamlCard().icon("ellipsis").click();
popover().findByText("Pause").click();
H.popover().findByText("Pause").click();
cy.wait("@updateSetting");
getSamlCard().findByText("Paused").should("exist");
getSamlCard().icon("ellipsis").click();
popover().findByText("Resume").click();
H.popover().findByText("Resume").click();
cy.wait("@updateSetting");
getSamlCard().findByText("Active").should("exist");
});
@@ -75,8 +68,8 @@ describeEE("scenarios > admin > settings > SSO > SAML", () => {
cy.visit("/admin/settings/authentication");
getSamlCard().icon("ellipsis").click();
popover().findByText("Deactivate").click();
modal().button("Deactivate").click();
H.popover().findByText("Deactivate").click();
H.modal().button("Deactivate").click();
cy.wait("@updateSettings");
getSamlCard().findByText("Set up").should("exist");
@@ -119,7 +112,10 @@ const getSamlCard = () => {
const enterSamlSettings = () => {
getSamlCertificate().then(certificate => {
typeAndBlurUsingLabel(/SAML Identity Provider URL/, "https://example.test");
typeAndBlurUsingLabel(/SAML Identity Provider Certificate/, certificate);
H.typeAndBlurUsingLabel(
/SAML Identity Provider URL/,
"https://example.test",
);
H.typeAndBlurUsingLabel(/SAML Identity Provider Certificate/, certificate);
});
};

View File

@@ -1,18 +1,5 @@
import { H } from "e2e/support";
import { ORDERS_QUESTION_ID } from "e2e/support/cypress_sample_instance_data";
import {
appBar,
describeEE,
entityPickerModal,
main,
modal,
popover,
restore,
setTokenFeatures,
undoToast,
updateSetting,
visitDashboard,
visitQuestion,
} from "e2e/support/helpers";
function checkFavicon(url) {
cy.request("/api/setting/application-favicon-url")
@@ -28,11 +15,11 @@ function checkLogo() {
const MB = 1024 * 1024;
describeEE("formatting > whitelabel", () => {
H.describeEE("formatting > whitelabel", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
});
describe("company name", () => {
@@ -45,7 +32,7 @@ describeEE("formatting > whitelabel", () => {
// Helps scroll the page up in order to see "Saved" notification
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Application Name").click();
undoToast().findByText("Changes saved").should("be.visible");
H.undoToast().findByText("Changes saved").should("be.visible");
cy.findByDisplayValue(COMPANY_NAME);
cy.log("Company name has been updated!");
});
@@ -73,7 +60,7 @@ describeEE("formatting > whitelabel", () => {
cy.log("Add a logo");
cy.readFile("e2e/support/assets/logo.jpeg", "base64").then(
logo_data => {
updateSetting(
H.updateSetting(
"application-logo-url",
`data:image/jpeg;base64,${logo_data}`,
);
@@ -103,7 +90,7 @@ describeEE("formatting > whitelabel", () => {
it("should work for people that set favicon URL before we change the input to file input", () => {
const faviconUrl =
"https://cdn.ecosia.org/assets/images/ico/favicon.ico";
updateSetting("application-favicon-url", faviconUrl);
H.updateSetting("application-favicon-url", faviconUrl);
checkFavicon(faviconUrl);
cy.signInAsNormalUser();
cy.visit("/");
@@ -123,7 +110,7 @@ describeEE("formatting > whitelabel", () => {
},
{ force: true },
);
undoToast().findByText("Changes saved").should("be.visible");
H.undoToast().findByText("Changes saved").should("be.visible");
cy.readFile("e2e/support/assets/favicon.ico", "base64").then(
base64Url => {
const faviconUrl = `data:image/jpeg;base64,${base64Url}`;
@@ -158,7 +145,7 @@ describeEE("formatting > whitelabel", () => {
cy.findByRole("searchbox", {
name: "Login and unsubscribe pages",
}).click();
popover().findByText("Custom").click();
H.popover().findByText("Custom").click();
/**
* Clicking "Choose File" doesn't actually open the file browser on Cypress,
* so I need to use `selectFile` with the file input instead.
@@ -180,7 +167,7 @@ describeEE("formatting > whitelabel", () => {
cy.findByRole("searchbox", {
name: "Login and unsubscribe pages",
}).click();
popover().findByText("Custom").click();
H.popover().findByText("Custom").click();
cy.log("test uploading a corrupted file");
cy.findByTestId("login-page-illustration-setting").within(() => {
cy.findByTestId("file-input").selectFile(
@@ -214,8 +201,8 @@ describeEE("formatting > whitelabel", () => {
"The image you chose is corrupted. Please choose another one.",
).should("not.exist");
});
undoToast().findByText("Changes saved").should("be.visible");
undoToast().icon("close").click();
H.undoToast().findByText("Changes saved").should("be.visible");
H.undoToast().icon("close").click();
cy.log("test removing the custom illustration");
cy.findByTestId("login-page-illustration-setting").within(() => {
@@ -225,14 +212,14 @@ describeEE("formatting > whitelabel", () => {
);
cy.findByDisplayValue("Lighthouse").should("be.visible");
});
undoToast().findByText("Changes saved").should("be.visible");
undoToast().icon("close").click();
H.undoToast().findByText("Changes saved").should("be.visible");
H.undoToast().icon("close").click();
cy.log("test uploading a valid image file");
cy.findByTestId("login-page-illustration-setting")
.findByRole("searchbox", { name: "Login and unsubscribe pages" })
.click();
popover().findByText("Custom").click();
H.popover().findByText("Custom").click();
cy.findByTestId("login-page-illustration-setting").within(() => {
cy.findByTestId("file-input").selectFile(
{
@@ -243,7 +230,7 @@ describeEE("formatting > whitelabel", () => {
);
cy.findByText("logo.jpeg").should("be.visible");
});
undoToast().findByText("Changes saved").should("be.visible");
H.undoToast().findByText("Changes saved").should("be.visible");
cy.readFile("e2e/support/assets/logo.jpeg", "base64").then(
logo_data => {
@@ -274,7 +261,7 @@ describeEE("formatting > whitelabel", () => {
cy.findByRole("searchbox", {
name: "Login and unsubscribe pages",
}).click();
popover().findByText("No illustration").click();
H.popover().findByText("No illustration").click();
cy.signOut();
cy.visit("/");
@@ -298,7 +285,7 @@ describeEE("formatting > whitelabel", () => {
);
cy.findByRole("searchbox", { name: "Landing page" }).click();
popover().findByText("Custom").click();
H.popover().findByText("Custom").click();
cy.findByTestId("landing-page-illustration-setting").within(() => {
cy.findByTestId("file-input").selectFile(
@@ -310,7 +297,7 @@ describeEE("formatting > whitelabel", () => {
);
cy.findByText("logo.jpeg").should("be.visible");
});
undoToast().findByText("Changes saved").should("be.visible");
H.undoToast().findByText("Changes saved").should("be.visible");
cy.readFile("e2e/support/assets/logo.jpeg", "base64").then(
logo_data => {
@@ -328,7 +315,7 @@ describeEE("formatting > whitelabel", () => {
cy.visit("/admin/settings/whitelabel/conceal-metabase");
cy.findByLabelText("Landing page").click();
popover().findByText("No illustration").click();
H.popover().findByText("No illustration").click();
cy.visit("/");
cy.findByTestId("landing-page-illustration").should("not.exist");
@@ -346,7 +333,7 @@ describeEE("formatting > whitelabel", () => {
cy.findByRole("searchbox", {
name: "When calculations return no results",
}).click();
popover().findByText("Custom").click();
H.popover().findByText("Custom").click();
cy.findByTestId("no-data-illustration-setting").within(() => {
cy.findByTestId("file-input").selectFile(
@@ -358,7 +345,7 @@ describeEE("formatting > whitelabel", () => {
);
cy.findByText("logo.jpeg").should("be.visible");
});
undoToast().findByText("Changes saved").should("be.visible");
H.undoToast().findByText("Changes saved").should("be.visible");
cy.createDashboardWithQuestions({
dashboardName: "No results dashboard",
@@ -377,7 +364,7 @@ describeEE("formatting > whitelabel", () => {
cy.log("test custom illustration");
visitDashboard("@dashboardId");
H.visitDashboard("@dashboardId");
cy.readFile("e2e/support/assets/logo.jpeg", "base64").then(
logo_data => {
const imageDataUrl = `data:image/jpeg;base64,${logo_data}`;
@@ -390,7 +377,7 @@ describeEE("formatting > whitelabel", () => {
},
);
visitQuestion("@questionId");
H.visitQuestion("@questionId");
cy.get("@imageDataUrl").then(imageDataUrl => {
cy.findByAltText("No results").should(
"have.attr",
@@ -405,12 +392,12 @@ describeEE("formatting > whitelabel", () => {
cy.findByRole("searchbox", {
name: "When calculations return no results",
}).click();
popover().findByText("No illustration").click();
H.popover().findByText("No illustration").click();
visitDashboard("@dashboardId");
H.visitDashboard("@dashboardId");
cy.findByAltText("No results").should("not.exist");
visitQuestion("@questionId");
H.visitQuestion("@questionId");
cy.findByAltText("No results").should("not.exist");
});
});
@@ -428,7 +415,7 @@ describeEE("formatting > whitelabel", () => {
cy.findByRole("searchbox", {
name: "When no objects can be found",
}).click();
popover().findByText("Custom").click();
H.popover().findByText("Custom").click();
cy.findByTestId("no-object-illustration-setting").within(() => {
cy.findByTestId("file-input").selectFile(
@@ -440,15 +427,15 @@ describeEE("formatting > whitelabel", () => {
);
cy.findByText("logo.jpeg").should("be.visible");
});
undoToast().findByText("Changes saved").should("be.visible");
H.undoToast().findByText("Changes saved").should("be.visible");
cy.log("test custom illustration");
cy.findByRole("navigation").findByText("Exit admin").click();
appBar().findByText("New").click();
popover().findByText("Dashboard").click();
modal().findByTestId("collection-picker-button").click();
entityPickerModal().within(() => {
H.appBar().findByText("New").click();
H.popover().findByText("Dashboard").click();
H.modal().findByTestId("collection-picker-button").click();
H.entityPickerModal().within(() => {
cy.readFile("e2e/support/assets/logo.jpeg", "base64").then(
logo_data => {
const imageDataUrl = `data:image/jpeg;base64,${logo_data}`;
@@ -475,13 +462,13 @@ describeEE("formatting > whitelabel", () => {
cy.findByRole("searchbox", {
name: "When no objects can be found",
}).click();
popover().findByText("No illustration").click();
H.popover().findByText("No illustration").click();
cy.findByRole("navigation").findByText("Exit admin").click();
appBar().findByText("New").click();
popover().findByText("Dashboard").click();
modal().findByTestId("collection-picker-button").click();
entityPickerModal().within(() => {
H.appBar().findByText("New").click();
H.popover().findByText("Dashboard").click();
H.modal().findByTestId("collection-picker-button").click();
H.entityPickerModal().within(() => {
cy.findByText(emptyCollectionName).click();
cy.findByAltText("No results").should("not.exist");
@@ -526,7 +513,7 @@ describeEE("formatting > whitelabel", () => {
.findByText("Show links and references to Metabase")
.click();
undoToast().findByText("Changes saved").should("be.visible");
H.undoToast().findByText("Changes saved").should("be.visible");
cy.visit("/");
cy.findByAltText("Metabot").should("not.exist");
@@ -634,7 +621,7 @@ describeEE("formatting > whitelabel", () => {
});
it("should link to metabase help when the whitelabel feature is disabled (eg OSS)", () => {
setTokenFeatures("none");
H.setTokenFeatures("none");
cy.signInAsNormalUser();
cy.visit("/");
@@ -657,17 +644,19 @@ describeEE("formatting > whitelabel", () => {
.clear()
.type("ftp://something")
.blur();
main()
H.main()
.findByText(/This needs to be/i)
.should("exist");
getHelpLinkCustomDestinationInput().clear().type("https://").blur();
main().findByText("Please make sure this is a valid URL").should("exist");
H.main()
.findByText("Please make sure this is a valid URL")
.should("exist");
getHelpLinkCustomDestinationInput().type("example");
main()
H.main()
.findByText("Please make sure this is a valid URL")
.should("not.exist");
});
@@ -682,7 +671,7 @@ describeEE("formatting > whitelabel", () => {
});
it("should not render the widget when users does not have a valid license", () => {
setTokenFeatures("none");
H.setTokenFeatures("none");
cy.reload();
cy.findByLabelText("Landing page custom destination").should("not.exist");
});
@@ -693,7 +682,7 @@ describeEE("formatting > whitelabel", () => {
.clear()
.type("/test-1")
.blur();
undoToast().findByText("Changes saved").should("be.visible");
H.undoToast().findByText("Changes saved").should("be.visible");
cy.findByTestId("landing-page-error").should("not.exist");
cy.findByRole("navigation").findByText("Exit admin").click();
@@ -706,7 +695,7 @@ describeEE("formatting > whitelabel", () => {
.clear()
.type("/test-2")
.blur();
undoToast().findByText("Changes saved").should("be.visible");
H.undoToast().findByText("Changes saved").should("be.visible");
// set to valid value then test invalid value is not persisted
cy.findByLabelText("Landing page custom destination")
@@ -731,12 +720,12 @@ function changeLoadingMessage(message) {
}
function setApplicationFontTo(font) {
updateSetting("application-font", font);
H.updateSetting("application-font", font);
}
const openSettingsMenu = () => appBar().icon("gear").click();
const openSettingsMenu = () => H.appBar().icon("gear").click();
const helpLink = () => popover().findByRole("link", { name: "Help" });
const helpLink = () => H.popover().findByRole("link", { name: "Help" });
const getHelpLinkCustomDestinationInput = () =>
cy.findByPlaceholderText("Enter a URL it should go to");

View File

@@ -1,19 +1,9 @@
import { H } from "e2e/support";
import { SAMPLE_DB_ID, WRITABLE_DB_ID } from "e2e/support/cypress_data";
import {
appBar,
entityPickerModal,
getNotebookStep,
popover,
queryWritableDB,
relativeDatePicker,
resetTestTable,
restore,
resyncDatabase,
} from "e2e/support/helpers";
describe("issue 26470", { tags: "@external" }, () => {
beforeEach(() => {
restore("postgres_12");
H.restore("postgres_12");
cy.signInAsAdmin();
cy.request("POST", "/api/persist/enable");
});
@@ -30,7 +20,7 @@ describe("issue 26470", { tags: "@external" }, () => {
describe("issue 33035", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.request("GET", "/api/user/current").then(({ body: { id: user_id } }) => {
cy.request("PUT", `/api/user/${user_id}`, { locale: "de" });
@@ -45,7 +35,7 @@ describe("issue 33035", () => {
describe("issue 21532", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -78,31 +68,31 @@ describe("issue 41765", { tags: ["@external"] }, () => {
const COLUMN_DISPLAY_NAME = "Another Column";
beforeEach(() => {
resetTestTable({ type: "postgres", table: TEST_TABLE });
restore("postgres-writable");
H.resetTestTable({ type: "postgres", table: TEST_TABLE });
H.restore("postgres-writable");
cy.signInAsAdmin();
resyncDatabase({
H.resyncDatabase({
dbId: WRITABLE_DB_ID,
tableName: TEST_TABLE,
});
});
function enterAdmin() {
appBar().icon("gear").click();
popover().findByText("Admin settings").click();
H.appBar().icon("gear").click();
H.popover().findByText("Admin settings").click();
}
function exitAdmin() {
appBar().findByText("Exit admin").click();
H.appBar().findByText("Exit admin").click();
}
function openWritableDatabaseQuestion() {
// start new question without navigating
appBar().findByText("New").click();
popover().findByText("Question").click();
H.appBar().findByText("New").click();
H.popover().findByText("Question").click();
entityPickerModal().within(() => {
H.entityPickerModal().within(() => {
cy.findByText("Tables").click();
cy.findByText(WRITABLE_DB_DISPLAY_NAME).click();
cy.findByText(TEST_TABLE_DISPLAY_NAME).click();
@@ -112,33 +102,33 @@ describe("issue 41765", { tags: ["@external"] }, () => {
it("re-syncing a database should invalidate the table cache (metabase#41765)", () => {
cy.visit("/");
queryWritableDB(
H.queryWritableDB(
`ALTER TABLE ${TEST_TABLE} ADD ${COLUMN_NAME} text;`,
"postgres",
);
openWritableDatabaseQuestion();
getNotebookStep("data").button("Pick columns").click();
popover().findByText(COLUMN_DISPLAY_NAME).should("not.exist");
H.getNotebookStep("data").button("Pick columns").click();
H.popover().findByText(COLUMN_DISPLAY_NAME).should("not.exist");
enterAdmin();
appBar().findByText("Databases").click();
H.appBar().findByText("Databases").click();
cy.findAllByRole("link").contains(WRITABLE_DB_DISPLAY_NAME).click();
cy.button("Sync database schema now").click();
exitAdmin();
openWritableDatabaseQuestion();
getNotebookStep("data").button("Pick columns").click();
popover().findByText(COLUMN_DISPLAY_NAME).should("be.visible");
H.getNotebookStep("data").button("Pick columns").click();
H.popover().findByText(COLUMN_DISPLAY_NAME).should("be.visible");
});
});
describe("(metabase#45042)", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -172,14 +162,14 @@ describe("(metabase#45042)", () => {
describe("(metabase#46714)", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.visit("/admin/datamodel/segment/create");
cy.findByTestId("segment-editor").findByText("Select a table").click();
entityPickerModal().within(() => {
H.entityPickerModal().within(() => {
cy.findByText("Orders").click();
});
@@ -189,21 +179,21 @@ describe("(metabase#46714)", () => {
});
it("should allow users to apply relative date options in the segment date picker", () => {
popover().within(() => {
H.popover().within(() => {
cy.findByText("Created At").click();
cy.findByText("Relative dates…").click();
cy.findByRole("tab", { name: "Previous" }).click();
cy.findByLabelText("Starting from…").click();
});
relativeDatePicker.setValue({ value: 68, unit: "day" });
H.relativeDatePicker.setValue({ value: 68, unit: "day" });
relativeDatePicker.setStartingFrom({
H.relativeDatePicker.setStartingFrom({
value: 70,
unit: "day",
});
popover().findByText("Add filter").click();
H.popover().findByText("Add filter").click();
cy.findByTestId("filter-pill").should(
"have.text",
@@ -212,17 +202,17 @@ describe("(metabase#46714)", () => {
});
it("should not hide operator select menu behind the main filter popover", () => {
popover().within(() => {
H.popover().within(() => {
cy.findByText("Total").click();
});
cy.findByLabelText("Filter operator")
.should("have.text", "Between")
.click();
popover().last().findByText("Less than").click();
H.popover().last().findByText("Less than").click();
cy.findByLabelText("Filter operator").should("have.text", "Less than");
popover().findByPlaceholderText("Enter a number").clear().type("1000");
popover().findByText("Add filter").click();
H.popover().findByPlaceholderText("Enter a number").clear().type("1000");
H.popover().findByText("Add filter").click();
cy.findByTestId("filter-pill").should(
"have.text",

View File

@@ -1,3 +1,4 @@
import { H } from "e2e/support";
import {
QA_MONGO_PORT,
QA_MYSQL_PORT,
@@ -8,20 +9,6 @@ import {
} from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import { ORDERS_QUESTION_ID } from "e2e/support/cypress_sample_instance_data";
import {
createSegment,
describeWithSnowplow,
enableTracking,
expectGoodSnowplowEvent,
isEE,
modal,
popover,
resetSnowplow,
restore,
setTokenFeatures,
tooltip,
typeAndBlurUsingLabel,
} from "e2e/support/helpers";
import { visitDatabase } from "./helpers/e2e-database-helpers";
@@ -33,7 +20,7 @@ describe(
() => {
["mysql", "postgres"].forEach(dialect => {
it(`should show ${dialect} writable_db with actions enabled`, () => {
restore(`${dialect}-writable`);
H.restore(`${dialect}-writable`);
cy.signInAsAdmin();
visitDatabase(WRITABLE_DB_ID).then(({ response: { body } }) => {
@@ -63,7 +50,7 @@ describe("admin > database > add", () => {
function selectFieldOption(fieldName, option) {
cy.findByLabelText(fieldName).click();
popover().contains(option).click({ force: true });
H.popover().contains(option).click({ force: true });
}
function chooseDatabase(database) {
@@ -110,7 +97,7 @@ describe("admin > database > add", () => {
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.intercept("POST", "/api/database").as("createDatabase");
@@ -127,15 +114,15 @@ describe("admin > database > add", () => {
describe("external databases", { tags: "@external" }, () => {
describe("postgres", () => {
beforeEach(() => {
popover().within(() => {
if (isEE) {
H.popover().within(() => {
if (H.isEE) {
// EE should ship with Oracle and Vertica as options
cy.findByText("Oracle");
cy.findByText("Vertica");
}
});
popover().contains("PostgreSQL").click({ force: true });
H.popover().contains("PostgreSQL").click({ force: true });
cy.findByTestId("database-form").within(() => {
cy.findByText("Show advanced options").click();
@@ -158,7 +145,7 @@ describe("admin > database > add", () => {
cy.findByLabelText("Host").parent().icon("info").realHover();
});
tooltip()
H.tooltip()
.findByText(/your databases ip address/i)
.should("be.visible");
@@ -170,12 +157,12 @@ describe("admin > database > add", () => {
});
// make sure fields needed to connect to the database are properly trimmed (metabase#12972)
typeAndBlurUsingLabel("Display name", "QA Postgres12");
typeAndBlurUsingLabel("Host", "localhost");
typeAndBlurUsingLabel("Port", QA_POSTGRES_PORT);
typeAndBlurUsingLabel("Database name", "sample");
typeAndBlurUsingLabel("Username", "metabase");
typeAndBlurUsingLabel("Password", "metasample123");
H.typeAndBlurUsingLabel("Display name", "QA Postgres12");
H.typeAndBlurUsingLabel("Host", "localhost");
H.typeAndBlurUsingLabel("Port", QA_POSTGRES_PORT);
H.typeAndBlurUsingLabel("Database name", "sample");
H.typeAndBlurUsingLabel("Username", "metabase");
H.typeAndBlurUsingLabel("Password", "metasample123");
});
const confirmSSLFields = (visible, hidden) => {
@@ -293,13 +280,13 @@ describe("admin > database > add", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains("Additional connection string options");
typeAndBlurUsingLabel("Display name", "QA Mongo");
typeAndBlurUsingLabel("Host", "localhost");
typeAndBlurUsingLabel("Port", QA_MONGO_PORT);
typeAndBlurUsingLabel("Database name", "sample");
typeAndBlurUsingLabel("Username", "metabase");
typeAndBlurUsingLabel("Password", "metasample123");
typeAndBlurUsingLabel("Authentication database (optional)", "admin");
H.typeAndBlurUsingLabel("Display name", "QA Mongo");
H.typeAndBlurUsingLabel("Host", "localhost");
H.typeAndBlurUsingLabel("Port", QA_MONGO_PORT);
H.typeAndBlurUsingLabel("Database name", "sample");
H.typeAndBlurUsingLabel("Username", "metabase");
H.typeAndBlurUsingLabel("Password", "metasample123");
H.typeAndBlurUsingLabel("Authentication database (optional)", "admin");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Save").should("not.be.disabled").click();
@@ -337,11 +324,11 @@ describe("admin > database > add", () => {
const badPasswordString = `mongodb://metabase:wrongPassword@localhost:${QA_MONGO_PORT}/sample?authSource=admin`;
const validConnectionString = `mongodb://metabase:metasample123@localhost:${QA_MONGO_PORT}/sample?authSource=admin`;
popover().findByText("MongoDB").click({ force: true });
H.popover().findByText("MongoDB").click({ force: true });
cy.findByTestId("database-form").within(() => {
cy.findByText("Paste a connection string").click();
typeAndBlurUsingLabel("Display name", "QA Mongo");
H.typeAndBlurUsingLabel("Display name", "QA Mongo");
cy.findByLabelText("Port").should("not.exist");
cy.findByLabelText("Paste your connection string").type(badDBString, {
delay: 0,
@@ -403,16 +390,16 @@ describe("admin > database > add", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains("Additional JDBC connection string options");
typeAndBlurUsingLabel("Display name", "QA MySQL8");
typeAndBlurUsingLabel("Host", "localhost");
typeAndBlurUsingLabel("Port", QA_MYSQL_PORT);
typeAndBlurUsingLabel("Database name", "sample");
typeAndBlurUsingLabel("Username", "metabase");
typeAndBlurUsingLabel("Password", "metasample123");
H.typeAndBlurUsingLabel("Display name", "QA MySQL8");
H.typeAndBlurUsingLabel("Host", "localhost");
H.typeAndBlurUsingLabel("Port", QA_MYSQL_PORT);
H.typeAndBlurUsingLabel("Database name", "sample");
H.typeAndBlurUsingLabel("Username", "metabase");
H.typeAndBlurUsingLabel("Password", "metasample123");
// Bypass the RSA public key error for MySQL database
// https://github.com/metabase/metabase/issues/12545
typeAndBlurUsingLabel(
H.typeAndBlurUsingLabel(
"Additional JDBC connection string options",
"allowPublicKeyRetrieval=true",
);
@@ -449,7 +436,7 @@ describe("admin > database > add", () => {
cy.visit("/admin/databases/create");
chooseDatabase("BigQuery");
typeAndBlurUsingLabel("Display name", "BQ");
H.typeAndBlurUsingLabel("Display name", "BQ");
selectFieldOption("Datasets", "Only these...");
cy.findByPlaceholderText("E.x. public,auth*").type("some-dataset");
@@ -465,7 +452,7 @@ describe("admin > database > add", () => {
describe("scenarios > admin > databases > exceptions", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -517,9 +504,9 @@ describe("scenarios > admin > databases > exceptions", () => {
cy.visit("/admin/databases/create");
typeAndBlurUsingLabel("Display name", "Test");
typeAndBlurUsingLabel("Database name", "db");
typeAndBlurUsingLabel("Username", "admin");
H.typeAndBlurUsingLabel("Display name", "Test");
H.typeAndBlurUsingLabel("Database name", "db");
H.typeAndBlurUsingLabel("Username", "admin");
cy.button("Save").click();
cy.wait("@createDatabase");
@@ -542,13 +529,13 @@ describe("scenarios > admin > databases > exceptions", () => {
it("should handle a failure to `GET` the list of all databases (metabase#20471)", () => {
const errorMessage = "Lorem ipsum dolor sit amet, consectetur adip";
isEE && setTokenFeatures("all");
H.isEE && H.setTokenFeatures("all");
cy.intercept(
{
method: "GET",
pathname: "/api/database",
query: isEE
query: H.isEE
? {
exclude_uneditable_details: "true",
}
@@ -583,7 +570,7 @@ describe("scenarios > admin > databases > exceptions", () => {
describe("scenarios > admin > databases > sample database", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.intercept("PUT", "/api/database/*").as("databaseUpdate");
});
@@ -628,7 +615,7 @@ describe("scenarios > admin > databases > sample database", () => {
cy.findByLabelText("Choose when syncs and scans happen").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Hourly").click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("Daily").click({ force: true });
});
@@ -644,7 +631,7 @@ describe("scenarios > admin > databases > sample database", () => {
.within(() => {
cy.findByText("Daily").click();
});
popover().findByText("Weekly").click();
H.popover().findByText("Weekly").click();
cy.button("Save changes").click();
cy.wait("@databaseUpdate").then(({ response: { body } }) => {
@@ -690,7 +677,7 @@ describe("scenarios > admin > databases > sample database", () => {
// model
cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { type: "model" });
// Create a segment through API
createSegment({
H.createSegment({
name: "Small orders",
description: "All orders with a total under $100.",
table_id: ORDERS_ID,
@@ -735,7 +722,7 @@ describe("scenarios > admin > databases > sample database", () => {
.within(() => {
cy.button("Discard saved field values").click();
});
modal().within(() => {
H.modal().within(() => {
cy.findByRole("heading").should(
"have.text",
"Discard saved field values",
@@ -751,7 +738,7 @@ describe("scenarios > admin > databases > sample database", () => {
cy.wait("@usage_info");
});
modal().within(() => {
H.modal().within(() => {
cy.button("Delete this content and the DB connection")
.as("deleteButton")
.should("be.disabled");
@@ -822,7 +809,7 @@ describe("scenarios > admin > databases > sample database", () => {
cy.button("Remove this database").click();
cy.wait("@loadDatabaseUsageInfo");
modal().within(() => {
H.modal().within(() => {
cy.findByLabelText(/Delete \d+ saved questions?/).click();
cy.findByLabelText(/Delete \d+ model?/).click();
cy.findByTestId("database-name-confirmation-input").type(
@@ -867,12 +854,12 @@ describe("scenarios > admin > databases > sample database", () => {
});
});
describeWithSnowplow("add database card", () => {
H.describeWithSnowplow("add database card", () => {
beforeEach(() => {
resetSnowplow();
restore();
H.resetSnowplow();
H.restore();
cy.signInAsAdmin();
enableTracking();
H.enableTracking();
});
it("should track the click on the card", () => {
@@ -885,7 +872,7 @@ describeWithSnowplow("add database card", () => {
cy.get("@addDatabaseCard").findByText("Add a database").click();
cy.location("pathname").should("eq", "/admin/databases/create");
expectGoodSnowplowEvent({
H.expectGoodSnowplowEvent({
event: "database_add_clicked",
triggered_from: "db-list",
});

View File

@@ -1,3 +1,4 @@
import { H } from "e2e/support";
import {
SAMPLE_DB_ID,
SAMPLE_DB_SCHEMA_ID,
@@ -5,28 +6,6 @@ import {
} from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import { ORDERS_QUESTION_ID } from "e2e/support/cypress_sample_instance_data";
import {
createSegment,
entityPickerModal,
entityPickerModalTab,
filter,
getNotebookStep,
modal,
openOrdersTable,
openReviewsTable,
openTable,
popover,
resetTestTable,
restore,
resyncDatabase,
saveQuestion,
startNewQuestion,
summarize,
visitAlias,
visitQuestion,
visitQuestionAdhoc,
withDatabase,
} from "e2e/support/helpers";
const { ORDERS, ORDERS_ID, PRODUCTS, REVIEWS, REVIEWS_ID, PRODUCTS_ID } =
SAMPLE_DATABASE;
@@ -51,7 +30,7 @@ describe("scenarios > admin > datamodel > field > field type", () => {
function setFieldType({ oldValue, newValue } = {}) {
getFieldType(oldValue).click();
popover().within(() => {
H.popover().within(() => {
cy.findByText(oldValue).closest(".ReactVirtualized__Grid").scrollTo(0, 0); // HACK: scroll to the top of the list. Ideally we should probably disable AccordionList virtualization
searchFieldType(newValue);
cy.findByText(newValue).click();
@@ -61,7 +40,7 @@ describe("scenarios > admin > datamodel > field > field type", () => {
function checkNoFieldType({ oldValue, newValue } = {}) {
getFieldType(oldValue).click();
popover().within(() => {
H.popover().within(() => {
searchFieldType(newValue);
cy.findByText(newValue).should("not.exist");
});
@@ -85,13 +64,13 @@ describe("scenarios > admin > datamodel > field > field type", () => {
function setFKTargetField(field) {
cy.findByText("Select a target").click();
popover().contains(field).click();
H.popover().contains(field).click();
}
beforeEach(() => {
cy.intercept("GET", "/api/table/*/query_metadata*").as("metadata");
restore();
H.restore();
cy.signInAsAdmin();
ordersColumns.forEach(column => {
@@ -104,7 +83,7 @@ describe("scenarios > admin > datamodel > field > field type", () => {
});
it("should let you change the type to 'No semantic type'", () => {
visitAlias("@ORDERS_PRODUCT_ID_URL");
H.visitAlias("@ORDERS_PRODUCT_ID_URL");
cy.wait(["@metadata", "@metadata"]);
setFieldType({ oldValue: "Foreign Key", newValue: "No semantic type" });
@@ -118,7 +97,7 @@ describe("scenarios > admin > datamodel > field > field type", () => {
});
it("should let you change the type to 'Foreign Key' and choose the target field", () => {
visitAlias("@ORDERS_QUANTITY_URL");
H.visitAlias("@ORDERS_QUANTITY_URL");
cy.wait("@metadata");
setFieldType({ oldValue: "Quantity", newValue: "Foreign Key" });
@@ -137,7 +116,7 @@ describe("scenarios > admin > datamodel > field > field type", () => {
});
it("should not let you change the type to 'Number' (metabase#16781)", () => {
visitAlias("@ORDERS_PRODUCT_ID_URL");
H.visitAlias("@ORDERS_PRODUCT_ID_URL");
cy.wait(["@metadata", "@metadata"]);
checkNoFieldType({ oldValue: "Foreign Key", newValue: "Number" });
@@ -146,7 +125,7 @@ describe("scenarios > admin > datamodel > field > field type", () => {
describe("scenarios > admin > datamodel > field", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
["CREATED_AT", "PRODUCT_ID", "QUANTITY"].forEach(name => {
@@ -160,10 +139,10 @@ describe("scenarios > admin > datamodel > field", () => {
});
describe("Name and Description", () => {
before(restore);
before(H.restore);
it("lets you change field name and description", () => {
visitAlias("@ORDERS_CREATED_AT_URL");
H.visitAlias("@ORDERS_CREATED_AT_URL");
cy.get('input[name="display_name"]').as("display_name");
cy.get('input[name="description"]').as("description");
@@ -193,10 +172,10 @@ describe("scenarios > admin > datamodel > field", () => {
describe("Formatting", () => {
it("should allow you to change field formatting", () => {
visitAlias("@ORDERS_QUANTITY_URL");
H.visitAlias("@ORDERS_QUANTITY_URL");
cy.findByRole("link", { name: "Formatting" }).click();
cy.findByLabelText("Style").click();
popover().findByText("Percent").click();
H.popover().findByText("Percent").click();
cy.wait("@fieldUpdate");
cy.findByRole("list", { name: "undo-list" })
.findByText("Updated Quantity")
@@ -205,10 +184,10 @@ describe("scenarios > admin > datamodel > field", () => {
});
describe("Visibility", () => {
before(restore);
before(H.restore);
it("lets you change field visibility", () => {
visitAlias("@ORDERS_CREATED_AT_URL");
H.visitAlias("@ORDERS_CREATED_AT_URL");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains("Everywhere").click();
@@ -223,10 +202,10 @@ describe("scenarios > admin > datamodel > field", () => {
});
describe("Filtering on this field", () => {
before(restore);
before(H.restore);
it("lets you change to 'Search box'", () => {
visitAlias("@ORDERS_QUANTITY_URL");
H.visitAlias("@ORDERS_QUANTITY_URL");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains("A list of all values").click();
@@ -241,10 +220,10 @@ describe("scenarios > admin > datamodel > field", () => {
});
describe("Display Values", () => {
before(restore);
before(H.restore);
it("lets you change to 'Use foreign key' and change the target for field with fk", () => {
visitAlias("@ORDERS_PRODUCT_ID_URL");
H.visitAlias("@ORDERS_PRODUCT_ID_URL");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains("Use original value").click();
@@ -265,10 +244,10 @@ describe("scenarios > admin > datamodel > field", () => {
const dbId = 2;
const remappedNullValue = "nothin";
restore("withSqlite");
H.restore("withSqlite");
cy.signInAsAdmin();
withDatabase(
H.withDatabase(
dbId,
({ NUMBER_WITH_NULLS: { NUM }, NUMBER_WITH_NULLS_ID }) => {
cy.request("GET", `/api/database/${dbId}/schemas`).then(
@@ -286,14 +265,14 @@ describe("scenarios > admin > datamodel > field", () => {
.closest("section")
.findByText("Use original value")
.click();
popover().findByText("Custom mapping").click();
H.popover().findByText("Custom mapping").click();
cy.findByDisplayValue("null").clear().type(remappedNullValue);
cy.button("Save").click();
cy.button("Saved!").should("be.visible");
cy.log("Make sure custom mapping appears in QB");
openTable({ database: dbId, table: NUMBER_WITH_NULLS_ID });
H.openTable({ database: dbId, table: NUMBER_WITH_NULLS_ID });
cy.get("[data-testid=cell-data]").should(
"contain",
remappedNullValue,
@@ -313,10 +292,10 @@ describe("Unfold JSON", () => {
}
beforeEach(() => {
resetTestTable({ type: "postgres", table: "many_data_types" });
restore("postgres-writable");
H.resetTestTable({ type: "postgres", table: "many_data_types" });
H.restore("postgres-writable");
cy.signInAsAdmin();
resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: "many_data_types" });
H.resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: "many_data_types" });
});
it("lets you enable/disable 'Unfold JSON' for JSON columns", () => {
@@ -336,7 +315,7 @@ describe("Unfold JSON", () => {
});
getUnfoldJsonContent().findByText(/Yes/i).click();
popover().within(() => {
H.popover().within(() => {
cy.findByText(/No/i).click();
});
@@ -373,7 +352,7 @@ describe("scenarios > admin > datamodel > hidden tables (metabase#9759)", () =>
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
// Toggle the table to be hidden as admin user
@@ -397,9 +376,9 @@ describe("scenarios > admin > datamodel > hidden tables (metabase#9759)", () =>
cy.contains("Orders").should("not.exist");
// It shouldn't show in a new question data picker
startNewQuestion();
entityPickerModal().within(() => {
entityPickerModalTab("Tables").click();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.contains("Products").should("exist");
cy.contains("Orders").should("not.exist");
});
@@ -415,7 +394,7 @@ describe("scenarios > admin > datamodel > metadata", () => {
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.intercept("PUT", "/api/field/*").as("fieldUpdate");
@@ -426,7 +405,7 @@ describe("scenarios > admin > datamodel > metadata", () => {
`/admin/datamodel/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${ORDERS_ID}/field/${ORDERS.PRODUCT_ID}/general`,
).as("ORDERS_PRODUCT_ID_URL");
visitAlias("@ORDERS_PRODUCT_ID_URL");
H.visitAlias("@ORDERS_PRODUCT_ID_URL");
cy.findByPlaceholderText("PRODUCT_ID")
.clear()
@@ -435,7 +414,7 @@ describe("scenarios > admin > datamodel > metadata", () => {
cy.wait("@fieldUpdate");
openOrdersTable({ limit: 5 });
H.openOrdersTable({ limit: 5 });
cy.findAllByTestId("header-cell").should("contain", "Remapped Product ID");
});
@@ -445,7 +424,7 @@ describe("scenarios > admin > datamodel > metadata", () => {
`/admin/datamodel/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${ORDERS_ID}`,
).as("ORDERS_TABLE_URL");
visitAlias("@ORDERS_TABLE_URL");
H.visitAlias("@ORDERS_TABLE_URL");
cy.findByDisplayValue("Product ID")
.clear()
@@ -454,7 +433,7 @@ describe("scenarios > admin > datamodel > metadata", () => {
cy.wait("@fieldUpdate");
openOrdersTable({ limit: 5 });
H.openOrdersTable({ limit: 5 });
cy.findAllByTestId("header-cell").should("contain", "Remapped Product ID");
});
@@ -472,7 +451,7 @@ describe("scenarios > admin > datamodel > metadata", () => {
cy.findByText("Use original value").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Use foreign key").click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("Title").click();
});
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -481,7 +460,7 @@ describe("scenarios > admin > datamodel > metadata", () => {
);
cy.log("Name of the product should be displayed instead of its ID");
openOrdersTable();
H.openOrdersTable();
cy.findAllByText("Awesome Concrete Shoes");
});
@@ -519,7 +498,7 @@ describe("scenarios > admin > datamodel > metadata", () => {
cy.findByText("Save").click();
cy.log("Numeric ratings should be remapped to custom strings");
openReviewsTable();
H.openReviewsTable();
Object.values(customMap).forEach(rating => {
cy.findAllByText(rating);
});
@@ -562,8 +541,8 @@ describe("scenarios > admin > datamodel > metadata", () => {
fk_target_field_id: ORDERS.CREATED_AT,
});
openReviewsTable({ mode: "notebook" });
summarize({ mode: "notebook" });
H.openReviewsTable({ mode: "notebook" });
H.summarize({ mode: "notebook" });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Count of rows").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -582,13 +561,13 @@ describe("scenarios > admin > datamodel > metadata", () => {
fk_target_field_id: PRODUCTS.ID,
});
openReviewsTable({ mode: "notebook" });
summarize({ mode: "notebook" });
getNotebookStep("summarize")
H.openReviewsTable({ mode: "notebook" });
H.summarize({ mode: "notebook" });
H.getNotebookStep("summarize")
.findByText("Pick a column to group by")
.click();
popover().within(() => {
H.popover().within(() => {
cy.findAllByTestId("dimension-list-item")
.eq(3)
.should("have.text", "Rating");
@@ -611,16 +590,16 @@ describe("scenarios > admin > datamodel > metadata", () => {
);
openOptionsForSection("Filtering on this field");
popover().findByText("Search box").click();
H.popover().findByText("Search box").click();
openOptionsForSection("Display values");
popover().findByText("Custom mapping").should("not.exist");
H.popover().findByText("Custom mapping").should("not.exist");
openOptionsForSection("Filtering on this field");
popover().findByText("A list of all values").click();
H.popover().findByText("A list of all values").click();
openOptionsForSection("Display values");
popover().findByText("Custom mapping");
H.popover().findByText("Custom mapping");
});
describe("column formatting options", () => {
@@ -656,7 +635,7 @@ describe("scenarios > admin > datamodel > metadata", () => {
});
// if you change the style to currency, currency settings should appear
popover().findByText("Currency").click();
H.popover().findByText("Currency").click();
cy.wait("@updateField");
cy.findByTestId("column-settings").within(() => {
@@ -678,7 +657,7 @@ describe("scenarios > admin > datamodel > metadata", () => {
cy.wait("@updateField");
visitQuestionAdhoc({
H.visitQuestionAdhoc({
dataset_query: {
database: SAMPLE_DB_ID,
query: {
@@ -696,7 +675,7 @@ describe("scenarios > admin > datamodel > metadata", () => {
describe("scenarios > admin > datamodel > segments", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.viewport(1400, 860);
});
@@ -713,7 +692,7 @@ describe("scenarios > admin > datamodel > segments", () => {
cy.button("New segment").click();
cy.findByTestId("segment-editor").findByText("Select a table").click();
entityPickerModal().within(() => {
H.entityPickerModal().within(() => {
cy.findByText("Orders").click();
});
@@ -722,7 +701,7 @@ describe("scenarios > admin > datamodel > segments", () => {
.click();
cy.log("Fails in v0.36.0 and v0.36.3. It exists in v0.35.4");
popover().findByText("Custom Expression");
H.popover().findByText("Custom Expression");
});
it("should show no segments", () => {
@@ -739,7 +718,7 @@ describe("scenarios > admin > datamodel > segments", () => {
beforeEach(() => {
// Create a segment through API
createSegment({
H.createSegment({
name: SEGMENT_NAME,
description: "All orders with a total under $100.",
table_id: ORDERS_ID,
@@ -808,10 +787,10 @@ describe("scenarios > admin > datamodel > segments", () => {
cy.findByTestId("filter-pill").should("have.text", "Orders < 100");
cy.findAllByTestId("cell-data").should("contain", "37.65");
summarize();
H.summarize();
cy.findAllByTestId("sidebar-right").button("Done").click();
cy.findByTestId("scalar-value").should("have.text", "13,005");
saveQuestion("Foo");
H.saveQuestion("Foo");
// Check list
cy.visit("/reference/segments/1/questions");
@@ -835,7 +814,7 @@ describe("scenarios > admin > datamodel > segments", () => {
.parent()
.find(".Icon-ellipsis")
.click();
popover().contains("Edit Segment").click();
H.popover().contains("Edit Segment").click();
// update the filter from "< 100" to "> 10"
cy.url().should("match", /segment\/1$/);
@@ -843,10 +822,10 @@ describe("scenarios > admin > datamodel > segments", () => {
cy.findByTestId("filter-pill")
.contains(/Total\s+is less than/)
.click();
popover().findByLabelText("Filter operator").click();
popover().contains("Greater than").click();
popover().findByPlaceholderText("Enter a number").type("{SelectAll}10");
popover().contains("Update filter").click();
H.popover().findByLabelText("Filter operator").click();
H.popover().contains("Greater than").click();
H.popover().findByPlaceholderText("Enter a number").type("{SelectAll}10");
H.popover().contains("Update filter").click();
// confirm that the preview updated
cy.findByTestId("segment-editor").contains("18758 rows");
@@ -872,9 +851,9 @@ describe("scenarios > admin > datamodel > segments", () => {
.parent()
.find(".Icon-ellipsis")
.click();
popover().findByText("Retire Segment").click();
modal().find("textarea").type("delete it");
modal().contains("button", "Retire").click();
H.popover().findByText("Retire Segment").click();
H.modal().find("textarea").type("delete it");
H.modal().contains("button", "Retire").click();
});
it("should show segment revision history (metabase#45577, metabase#45594)", () => {
@@ -901,7 +880,7 @@ describe("scenarios > admin > datamodel > segments", () => {
.filter(`:contains(${SEGMENT_NAME})`)
.icon("ellipsis")
.click();
popover().findByTextEnsureVisible("Revision History").click();
H.popover().findByTextEnsureVisible("Revision History").click();
cy.location("pathname").should(
"eq",
@@ -949,7 +928,7 @@ describe("scenarios > admin > databases > table", () => {
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -1026,8 +1005,8 @@ describe("scenarios > admin > databases > table", () => {
describe.skip("turning table visibility off shouldn't prevent editing related question (metabase#15947)", () => {
it("simple question (metabase#15947-1)", () => {
turnTableVisibilityOff(ORDERS_ID);
visitQuestion(ORDERS_QUESTION_ID);
filter();
H.visitQuestion(ORDERS_QUESTION_ID);
H.filter();
});
it("question with joins (metabase#15947-2)", () => {

View File

@@ -1,23 +1,10 @@
import { H } from "e2e/support";
import {
SAMPLE_DB_ID,
SAMPLE_DB_SCHEMA_ID,
USER_GROUPS,
} from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
describeEE,
entityPickerModal,
entityPickerModalTab,
moveDnDKitElement,
openOrdersTable,
openProductsTable,
openReviewsTable,
openTable,
popover,
restore,
setTokenFeatures,
startNewQuestion,
} from "e2e/support/helpers";
const {
ORDERS,
@@ -51,7 +38,7 @@ describe("scenarios > admin > datamodel > editor", () => {
describe("table settings", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -63,9 +50,9 @@ describe("scenarios > admin > datamodel > editor", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Updated Table display_name").should("be.visible");
startNewQuestion();
entityPickerModal().within(() => {
entityPickerModalTab("Tables").click();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText("People").should("be.visible");
cy.findByText("New orders").should("be.visible");
});
@@ -110,9 +97,9 @@ describe("scenarios > admin > datamodel > editor", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("5 Hidden Tables").should("be.visible");
startNewQuestion();
entityPickerModal().within(() => {
entityPickerModalTab("Tables").click();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText("People").should("be.visible");
cy.findByText("Orders").should("not.exist");
});
@@ -124,9 +111,9 @@ describe("scenarios > admin > datamodel > editor", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("4 Hidden Tables").should("be.visible");
startNewQuestion();
entityPickerModal().within(() => {
entityPickerModalTab("Tables").click();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText("People").should("be.visible");
cy.findByText("Orders").should("be.visible");
});
@@ -142,7 +129,7 @@ describe("scenarios > admin > datamodel > editor", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Updated Tax").should("be.visible");
openOrdersTable();
H.openOrdersTable();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("New tax").should("be.visible");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -191,7 +178,7 @@ describe("scenarios > admin > datamodel > editor", () => {
it("should allow changing the field visibility", () => {
visitTableMetadata();
getFieldSection("TAX").findByText("Everywhere").click();
popover().findByText("Do not include").click();
H.popover().findByText("Do not include").click();
cy.wait("@updateField");
getFieldSection("TAX").findByText("Do not include").should("be.visible");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -199,7 +186,7 @@ describe("scenarios > admin > datamodel > editor", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Updated Tax").should("be.visible");
openOrdersTable();
H.openOrdersTable();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Total").should("be.visible");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -219,7 +206,7 @@ describe("scenarios > admin > datamodel > editor", () => {
searchAndSelectValue("Canadian Dollar");
cy.wait("@updateField");
openOrdersTable();
H.openOrdersTable();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Tax (CA$)").should("be.visible");
});
@@ -227,7 +214,7 @@ describe("scenarios > admin > datamodel > editor", () => {
it("should allow changing the field foreign key target", () => {
visitTableMetadata();
getFieldSection("USER_ID").findByText("People → ID").click();
popover().findByText("Products → ID").click();
H.popover().findByText("Products → ID").click();
cy.wait("@updateField");
getFieldSection("USER_ID")
.findByText("Products → ID")
@@ -235,10 +222,14 @@ describe("scenarios > admin > datamodel > editor", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Updated User ID").should("be.visible");
openTable({ database: SAMPLE_DB_ID, table: ORDERS_ID, mode: "notebook" });
H.openTable({
database: SAMPLE_DB_ID,
table: ORDERS_ID,
mode: "notebook",
});
cy.icon("join_left_outer").click();
entityPickerModal().within(() => {
entityPickerModalTab("Tables").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText("Products").click();
});
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -248,7 +239,7 @@ describe("scenarios > admin > datamodel > editor", () => {
it("should allow sorting fields as in the database", () => {
visitTableMetadata({ tableId: PRODUCTS_ID });
setTableOrder("Database");
openProductsTable();
H.openProductsTable();
assertTableHeader([
"ID",
"Ean",
@@ -264,7 +255,7 @@ describe("scenarios > admin > datamodel > editor", () => {
it("should allow sorting fields alphabetically", () => {
visitTableMetadata({ tableId: PRODUCTS_ID });
setTableOrder("Alphabetical");
openProductsTable();
H.openProductsTable();
assertTableHeader([
"Category",
"Created At",
@@ -280,7 +271,7 @@ describe("scenarios > admin > datamodel > editor", () => {
it("should allow sorting fields smartly", () => {
visitTableMetadata({ tableId: PRODUCTS_ID });
setTableOrder("Smart");
openProductsTable();
H.openProductsTable();
assertTableHeader([
"ID",
"Created At",
@@ -296,11 +287,11 @@ describe("scenarios > admin > datamodel > editor", () => {
it("should allow sorting fields in the custom order", () => {
visitTableMetadata({ tableId: PRODUCTS_ID });
//moveField(0, 200);
moveDnDKitElement(cy.findAllByTestId("grabber").first(), {
H.moveDnDKitElement(cy.findAllByTestId("grabber").first(), {
vertical: 200,
});
cy.wait("@updateFieldOrder");
openProductsTable();
H.openProductsTable();
assertTableHeader([
"Ean",
"ID",
@@ -332,7 +323,7 @@ describe("scenarios > admin > datamodel > editor", () => {
describe("field settings", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -344,7 +335,7 @@ describe("scenarios > admin > datamodel > editor", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Updated Tax").should("be.visible");
openOrdersTable();
H.openOrdersTable();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("New tax").should("be.visible");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -372,14 +363,14 @@ describe("scenarios > admin > datamodel > editor", () => {
visitFieldMetadata({ fieldId: ORDERS.TAX });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Everywhere").click();
popover().findByText("Do not include").click();
H.popover().findByText("Do not include").click();
cy.wait("@updateField");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Do not include").should("be.visible");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Updated Tax").should("be.visible");
openOrdersTable();
H.openOrdersTable();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Total").should("be.visible");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -402,7 +393,7 @@ describe("scenarios > admin > datamodel > editor", () => {
searchAndSelectValue("Canadian Dollar");
cy.wait("@updateField");
openOrdersTable();
H.openOrdersTable();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Tax (CA$)").should("be.visible");
});
@@ -411,17 +402,21 @@ describe("scenarios > admin > datamodel > editor", () => {
visitFieldMetadata({ fieldId: ORDERS.USER_ID });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("People → ID").click();
popover().findByText("Products → ID").click();
H.popover().findByText("Products → ID").click();
cy.wait("@updateField");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Products → ID").should("be.visible");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Updated User ID").should("be.visible");
openTable({ database: SAMPLE_DB_ID, table: ORDERS_ID, mode: "notebook" });
H.openTable({
database: SAMPLE_DB_ID,
table: ORDERS_ID,
mode: "notebook",
});
cy.icon("join_left_outer").click();
entityPickerModal().within(() => {
entityPickerModalTab("Tables").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText("Products").click();
});
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -435,22 +430,22 @@ describe("scenarios > admin > datamodel > editor", () => {
cy.log(
"Ensure that Coercion strategy has been humanized (metabase#44723)",
);
popover().should("not.contain.text", "Coercion");
popover().findByText("UNIX seconds → Datetime").click();
H.popover().should("not.contain.text", "Coercion");
H.popover().findByText("UNIX seconds → Datetime").click();
cy.wait("@updateField");
openTable({ database: SAMPLE_DB_ID, table: FEEDBACK_ID });
H.openTable({ database: SAMPLE_DB_ID, table: FEEDBACK_ID });
cy.findAllByTestId("cell-data")
.contains("December 31, 1969, 4:00 PM")
.should("have.length.greaterThan", 0);
});
});
describeEE("data model permissions", () => {
H.describeEE("data model permissions", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
});
it("should allow changing the table name with data model permissions only", () => {
@@ -466,9 +461,9 @@ describe("scenarios > admin > datamodel > editor", () => {
cy.signOut();
cy.signInAsNormalUser();
startNewQuestion();
entityPickerModal().within(() => {
entityPickerModalTab("Tables").click();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText("People").should("be.visible");
cy.findByText("New orders").should("be.visible");
});
@@ -488,7 +483,7 @@ describe("scenarios > admin > datamodel > editor", () => {
cy.findByText("Updated Tax").should("be.visible");
cy.signInAsNormalUser();
openOrdersTable();
H.openOrdersTable();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("New tax").should("be.visible");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -507,7 +502,7 @@ describe("scenarios > admin > datamodel > editor", () => {
cy.findByText("Updated Total").should("be.visible");
cy.signInAsNormalUser();
openOrdersTable();
H.openOrdersTable();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("New total").should("be.visible");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -523,7 +518,7 @@ describe("scenarios > admin > datamodel > editor", () => {
visitFieldMetadata({ fieldId: ORDERS.USER_ID });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("People → ID").click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("Reviews → ID").should("not.exist");
cy.findByText("Products → ID").click();
});
@@ -534,10 +529,14 @@ describe("scenarios > admin > datamodel > editor", () => {
cy.findByText("Updated User ID").should("be.visible");
cy.signInAsNormalUser();
openTable({ database: SAMPLE_DB_ID, table: ORDERS_ID, mode: "notebook" });
H.openTable({
database: SAMPLE_DB_ID,
table: ORDERS_ID,
mode: "notebook",
});
cy.icon("join_left_outer").click();
entityPickerModal().within(() => {
entityPickerModalTab("Tables").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText("Products").click();
});
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -553,12 +552,12 @@ describe("scenarios > admin > datamodel > editor", () => {
visitFieldMetadata({ tableId: REVIEWS_ID, fieldId: REVIEWS.PRODUCT_ID });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Use original value").click();
popover().findByText("Use foreign key").click();
popover().findByText("Title").click();
H.popover().findByText("Use foreign key").click();
H.popover().findByText("Title").click();
cy.wait("@updateFieldDimension");
cy.signInAsNormalUser();
openReviewsTable({ limit: 1 });
H.openReviewsTable({ limit: 1 });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Rustic Paper Wallet").should("be.visible");
});
@@ -570,7 +569,7 @@ describe("scenarios > admin > datamodel > editor", () => {
visitFieldMetadata({ tableId: REVIEWS_ID, fieldId: REVIEWS.PRODUCT_ID });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Use original value").click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("Use original value").should("be.visible");
cy.findByText("Use foreign key").should("not.exist");
});
@@ -583,7 +582,7 @@ describe("scenarios > admin > datamodel > editor", () => {
visitFieldMetadata({ tableId: REVIEWS_ID, fieldId: REVIEWS.RATING });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Use original value").click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("Use original value").should("be.visible");
cy.findByText("Custom mapping").should("not.exist");
});
@@ -592,7 +591,7 @@ describe("scenarios > admin > datamodel > editor", () => {
visitFieldMetadata({ tableId: REVIEWS_ID, fieldId: REVIEWS.RATING });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Use original value").click();
popover().findByText("Custom mapping").click();
H.popover().findByText("Custom mapping").click();
cy.wait("@updateFieldDimension");
cy.signIn("none");
@@ -606,7 +605,7 @@ describe("scenarios > admin > datamodel > editor", () => {
describe("databases without schemas", { tags: ["@external"] }, () => {
beforeEach(() => {
restore("mysql-8");
H.restore("mysql-8");
cy.signInAsAdmin();
});
@@ -626,7 +625,7 @@ describe("scenarios > admin > datamodel > editor", () => {
schemaId: MYSQL_DB_SCHEMA_ID,
});
getFieldSection("TAX").findByText("Everywhere").click();
popover().findByText("Do not include").click();
H.popover().findByText("Do not include").click();
cy.wait("@updateField");
getFieldSection("TAX").findByText("Do not include").should("be.visible");
});
@@ -681,7 +680,7 @@ const clearAndBlurInput = oldValue => {
};
const searchAndSelectValue = (newValue, searchText = newValue) => {
popover().within(() => {
H.popover().within(() => {
cy.findByRole("grid").scrollTo("top", { ensureScrollable: false });
cy.findByPlaceholderText("Find...").type(searchText, { delay: 50 });
cy.findByText(newValue).click();
@@ -694,7 +693,7 @@ const getFieldSection = fieldName => {
const setTableOrder = order => {
cy.findByLabelText("Sort").click();
popover().findByText(order).click();
H.popover().findByText(order).click();
cy.wait("@updateTable");
};

View File

@@ -1,24 +1,13 @@
import { H } from "e2e/support";
import { SAMPLE_DB_ID, SAMPLE_DB_SCHEMA_ID } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
appBar,
commandPalette,
commandPaletteButton,
navigationSidebar,
openReviewsTable,
popover,
restore,
summarize,
tableHeaderClick,
visitAlias,
} from "e2e/support/helpers";
const { PEOPLE_ID, PEOPLE, REVIEWS, REVIEWS_ID, ORDERS, ORDERS_ID } =
SAMPLE_DATABASE;
describe("issue 17768", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.request("PUT", `/api/field/${REVIEWS.ID}`, {
@@ -38,13 +27,13 @@ describe("issue 17768", () => {
});
it("should not show binning options for an entity key, regardless of its underlying type (metabase#17768)", () => {
openReviewsTable({ mode: "notebook" });
H.openReviewsTable({ mode: "notebook" });
summarize({ mode: "notebook" });
H.summarize({ mode: "notebook" });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Pick a column to group by").click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("ID")
.closest("[data-element-id=list-section]")
.realHover()
@@ -56,7 +45,7 @@ describe("issue 17768", () => {
describe("issue 18384", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
// Hide Reviews table
@@ -89,7 +78,7 @@ describe("issue 21984", () => {
beforeEach(() => {
cy.intercept("GET", "/api/table/*/query_metadata?**").as("tableMetadata");
restore();
H.restore();
cy.signInAsAdmin();
cy.visit(reviewsDataModelPage);
@@ -106,8 +95,8 @@ describe("issue 21984", () => {
cy.findByText("Reviews").should("not.exist");
});
commandPaletteButton().click();
commandPalette().within(() => {
H.commandPaletteButton().click();
H.commandPalette().within(() => {
cy.findByText("Recent items").should("not.exist");
});
});
@@ -115,7 +104,7 @@ describe("issue 21984", () => {
describe("issue 15542", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.wrap(
@@ -126,7 +115,7 @@ describe("issue 15542", () => {
function openOrdersTable() {
// Navigate without reloading the page
navigationSidebar().findByText("Databases").click({
H.navigationSidebar().findByText("Databases").click({
// force the click because the sidebar might be closed but
// that is not what we are testing here.
force: true,
@@ -143,10 +132,10 @@ describe("issue 15542", () => {
function openOrdersProductIdSettings() {
// Navigate without reloading the page
appBar().icon("gear").click();
popover().findByText("Admin settings").click();
H.appBar().icon("gear").click();
H.popover().findByText("Admin settings").click();
appBar().findByText("Table Metadata").click();
H.appBar().findByText("Table Metadata").click();
cy.findByText("Orders").click();
cy.findByTestId("column-PRODUCT_ID").icon("gear").click();
@@ -161,24 +150,24 @@ describe("issue 15542", () => {
// helpers because they use cy.visit under the hood and that reloads the page,
// clearing the in-browser cache, which is what we are testing here.
visitAlias("@ORDERS_PRODUCT_ID_URL");
H.visitAlias("@ORDERS_PRODUCT_ID_URL");
select("Plain input box").click();
popover().findByText("A list of all values").click();
H.popover().findByText("A list of all values").click();
select("Use original value").click();
popover().findByText("Use foreign key").click();
popover().findByText("Title").click();
H.popover().findByText("Use foreign key").click();
H.popover().findByText("Title").click();
cy.wait("@fieldDimensionUpdate");
exitAdmin();
openOrdersTable();
tableHeaderClick("Product ID");
popover().findByText("Filter by this column").click();
H.tableHeaderClick("Product ID");
H.popover().findByText("Filter by this column").click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("1").should("not.exist");
cy.findByText("Rustic Paper Wallet").should("be.visible");
});
@@ -186,15 +175,15 @@ describe("issue 15542", () => {
openOrdersProductIdSettings();
select("Use foreign key").click();
popover().findByText("Use original value").click();
H.popover().findByText("Use original value").click();
exitAdmin();
openOrdersTable();
tableHeaderClick("Product ID");
popover().findByText("Filter by this column").click();
H.tableHeaderClick("Product ID");
H.popover().findByText("Filter by this column").click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("1").should("be.visible");
cy.findByText("Rustic Paper Wallet").should("not.exist");
});

View File

@@ -1,10 +1,4 @@
import {
describeEE,
queryWritableDB,
resetTestTable,
restore,
setTokenFeatures,
} from "e2e/support/helpers";
import { H } from "e2e/support";
import {
TEST_TABLE,
@@ -57,14 +51,14 @@ const testCacheStrategy = ({
});
const expectedCacheDuration = getExpectedCacheDuration(strategy);
queryWritableDB(`UPDATE ${TEST_TABLE} SET my_text='Old Value';`);
H.queryWritableDB(`UPDATE ${TEST_TABLE} SET my_text='Old Value';`);
// first visit causes us to cache it
visitItem();
cy.findByTestId("visualization-root").findByText("Old Value");
// value changes in the DB
queryWritableDB(`UPDATE ${TEST_TABLE} SET my_text='New Value';`);
H.queryWritableDB(`UPDATE ${TEST_TABLE} SET my_text='New Value';`);
// check that old value is still cached
advanceServerClockBy(expectedCacheDuration - 1000);
@@ -100,16 +94,18 @@ const testDoNotCachePolicy = ({
});
}
queryWritableDB(`UPDATE ${TEST_TABLE} SET my_text='Value 2';`).then(() => {
visitItem();
cy.findByTestId("visualization-root").findByText("Value 2");
queryWritableDB(`UPDATE ${TEST_TABLE} SET my_text='Value 3';`).then(
() => {
visitItem();
cy.findByTestId("visualization-root").findByText("Value 3");
},
);
});
H.queryWritableDB(`UPDATE ${TEST_TABLE} SET my_text='Value 2';`).then(
() => {
visitItem();
cy.findByTestId("visualization-root").findByText("Value 2");
H.queryWritableDB(`UPDATE ${TEST_TABLE} SET my_text='Value 3';`).then(
() => {
visitItem();
cy.findByTestId("visualization-root").findByText("Value 3");
},
);
},
);
});
};
@@ -126,14 +122,14 @@ describe(
"Cache invalidation for dashboards and questions",
{ tags: "@external" },
() => {
describeEE("ee", () => {
H.describeEE("ee", () => {
beforeEach(() => {
resetServerTime();
interceptPerformanceRoutes();
resetTestTable({ type: "postgres", table: TEST_TABLE });
restore("postgres-writable");
H.resetTestTable({ type: "postgres", table: TEST_TABLE });
H.restore("postgres-writable");
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
});
describe("adaptive and duration strategies", () => {
@@ -268,8 +264,8 @@ describe(
beforeEach(() => {
resetServerTime();
interceptPerformanceRoutes();
resetTestTable({ type: "postgres", table: TEST_TABLE });
restore("postgres-writable");
H.resetTestTable({ type: "postgres", table: TEST_TABLE });
H.restore("postgres-writable");
cy.signInAsAdmin();
cy.visit("/admin/performance");
cacheStrategyForm();

View File

@@ -1,11 +1,6 @@
import _ from "underscore";
import {
describeEE,
popover,
restore,
setTokenFeatures,
} from "e2e/support/helpers";
import { H } from "e2e/support";
import type { ScheduleComponentType } from "metabase/components/Schedule/constants";
import type { CacheableModel } from "metabase-types/api";
@@ -23,12 +18,12 @@ import {
* that configuring the schedule strategy causes the cache to be invalidated at
* the appointed time. Nor do they check that the cron expression retrieved
* from the API is displayed in the UI. */
describeEE("scenarios > admin > performance > schedule strategy", () => {
H.describeEE("scenarios > admin > performance > schedule strategy", () => {
beforeEach(() => {
restore();
H.restore();
interceptPerformanceRoutes();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
});
/** An object describing the values to enter in the schedule strategy configuration form. */
@@ -104,7 +99,7 @@ describeEE("scenarios > admin > performance > schedule strategy", () => {
} else {
getScheduleComponent(componentType).click();
popover().within(() => {
H.popover().within(() => {
cy.findByText(optionToClick).click();
});
}

View File

@@ -1,13 +1,8 @@
import { H } from "e2e/support";
import {
ORDERS_COUNT_QUESTION_ID,
ORDERS_QUESTION_ID,
} from "e2e/support/cypress_sample_instance_data";
import {
describeEE,
restore,
setTokenFeatures,
visitQuestion,
} from "e2e/support/helpers";
import {
interceptPerformanceRoutes,
@@ -30,7 +25,7 @@ import {
describe("scenarios > admin > performance > strategy form", () => {
describe("oss", { tags: "@OSS" }, () => {
beforeEach(() => {
restore();
H.restore();
interceptPerformanceRoutes();
cy.signInAsAdmin();
cy.visit("/admin");
@@ -116,12 +111,12 @@ describe("scenarios > admin > performance > strategy form", () => {
});
});
describeEE("ee", () => {
H.describeEE("ee", () => {
beforeEach(() => {
restore();
H.restore();
interceptPerformanceRoutes();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
});
it("has the right tabs", () => {
@@ -366,7 +361,7 @@ describe("scenarios > admin > performance > strategy form", () => {
.contains(
"No dashboards or questions have their own caching policies yet.",
);
visitQuestion(ORDERS_QUESTION_ID);
H.visitQuestion(ORDERS_QUESTION_ID);
openSidebarCacheStrategyForm("question");
durationRadioButton().click();
cy.findByLabelText(/Cache results for this many hours/).type("99");
@@ -393,7 +388,7 @@ describe("scenarios > admin > performance > strategy form", () => {
.contains(
"No dashboards or questions have their own caching policies yet.",
);
visitQuestion(ORDERS_QUESTION_ID);
H.visitQuestion(ORDERS_QUESTION_ID);
openSidebarCacheStrategyForm("question");
durationRadioButton().click();
cy.findByLabelText(/Cache results for this many hours/).type("99");
@@ -407,7 +402,7 @@ describe("scenarios > admin > performance > strategy form", () => {
adaptiveRadioButton().click();
saveCacheStrategyForm({ strategyType: "ttl", model: "database" });
visitQuestion(ORDERS_COUNT_QUESTION_ID);
H.visitQuestion(ORDERS_COUNT_QUESTION_ID);
openSidebarCacheStrategyForm("question");
durationRadioButton().click();
cy.findByLabelText(/Cache results for this many hours/).type("24");

View File

@@ -1,26 +1,17 @@
import { H } from "e2e/support";
import { WRITABLE_DB_ID } from "e2e/support/cypress_data";
import {
createQuestion,
describeEE,
onlyOnOSS,
queryWritableDB,
resetTestTable,
restore,
resyncDatabase,
setTokenFeatures,
} from "e2e/support/helpers";
import { createNativeQuestion } from "../../../support/helpers/api/createNativeQuestion";
const SCOREBOARD_TABLE = "scoreboard_actions";
const COLORS_TABLE = "colors27745";
describeEE("query validator", { tags: "@external" }, () => {
H.describeEE("query validator", { tags: "@external" }, () => {
describe("feature disbaled", () => {
beforeEach(() => {
restore("postgres-writable");
H.restore("postgres-writable");
cy.signInAsAdmin();
setTokenFeatures("none");
H.setTokenFeatures("none");
});
it("Should not show the page in troubleshooting or the setting in general", () => {
@@ -38,9 +29,9 @@ describeEE("query validator", { tags: "@external" }, () => {
describe("feature enabled", () => {
beforeEach(() => {
restore("postgres-writable");
H.restore("postgres-writable");
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
});
it("enable query analysis setting", () => {
@@ -64,10 +55,10 @@ describeEE("query validator", { tags: "@external" }, () => {
});
it("picks up inactive and unknown fields and tables", () => {
resetTestTable({ type: "postgres", table: SCOREBOARD_TABLE });
resetTestTable({ type: "postgres", table: COLORS_TABLE });
H.resetTestTable({ type: "postgres", table: SCOREBOARD_TABLE });
H.resetTestTable({ type: "postgres", table: COLORS_TABLE });
resyncDatabase({
H.resyncDatabase({
dbId: WRITABLE_DB_ID,
});
@@ -110,7 +101,7 @@ describeEE("query validator", { tags: "@external" }, () => {
field => field.name === "team_name",
);
createQuestion({
H.createQuestion({
name: "Structured inactive field",
query: {
"source-table": scoreboardTable.id,
@@ -134,7 +125,7 @@ describeEE("query validator", { tags: "@external" }, () => {
field => field.name === "name",
);
createQuestion({
H.createQuestion({
name: "Structured inactive table",
query: {
"source-table": colorsTable.id,
@@ -148,13 +139,13 @@ describeEE("query validator", { tags: "@external" }, () => {
},
);
queryWritableDB(
H.queryWritableDB(
`ALTER TABLE ${SCOREBOARD_TABLE} RENAME COLUMN team_name TO team_name_`,
);
queryWritableDB(`DROP TABLE ${COLORS_TABLE}`);
H.queryWritableDB(`DROP TABLE ${COLORS_TABLE}`);
resyncDatabase({
H.resyncDatabase({
dbId: WRITABLE_DB_ID,
});
@@ -189,8 +180,8 @@ describeEE("query validator", { tags: "@external" }, () => {
describe("OSS", { tags: "@OSS" }, () => {
beforeEach(() => {
onlyOnOSS();
restore();
H.onlyOnOSS();
H.restore();
cy.signInAsAdmin();
});

View File

@@ -1,24 +1,14 @@
import {
appBar,
describeEE,
main,
modal,
onlyOnEE,
onlyOnOSS,
restore,
setTokenFeatures,
setupMetabaseCloud,
} from "e2e/support/helpers";
import { H } from "e2e/support";
describe("scenarios > admin > troubleshooting > help", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
// Unskip when mocking Cloud in Cypress is fixed (#18289)
it.skip("should add the support link when running Metabase Cloud", () => {
setupMetabaseCloud();
H.setupMetabaseCloud();
cy.visit("/admin/troubleshooting/help");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -30,9 +20,9 @@ describe("scenarios > admin > troubleshooting > help", () => {
describe("scenarios > admin > troubleshooting > help", { tags: "@OSS" }, () => {
beforeEach(() => {
onlyOnOSS();
H.onlyOnOSS();
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -52,11 +42,11 @@ describe("scenarios > admin > troubleshooting > help", { tags: "@OSS" }, () => {
});
});
describeEE("scenarios > admin > troubleshooting > help (EE)", () => {
H.describeEE("scenarios > admin > troubleshooting > help (EE)", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
});
it("should link `Get Help` to help-premium", () => {
@@ -140,7 +130,7 @@ describe("scenarios > admin > troubleshooting > tasks", () => {
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
// The only reliable way to reproduce this issue is by stubing page responses!
@@ -223,7 +213,7 @@ describe("admin > tools > erroring questions ", { tags: "@quarantine" }, () => {
cy.findByText("Save").click();
modal().within(() => {
H.modal().within(() => {
cy.button("Save").click();
});
}
@@ -238,11 +228,11 @@ describe("admin > tools > erroring questions ", { tags: "@quarantine" }, () => {
describe.skip("when feature enabled", () => {
beforeEach(() => {
onlyOnEE();
H.onlyOnEE();
restore();
H.restore();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
cy.intercept("POST", "/api/dataset").as("dataset");
});
@@ -326,20 +316,20 @@ describe("admin > tools > erroring questions ", { tags: "@quarantine" }, () => {
describe("when feature disabled", () => {
beforeEach(() => {
onlyOnEE();
H.onlyOnEE();
restore();
H.restore();
cy.signInAsAdmin();
});
it("should not show tools -> errors", () => {
cy.visit("/admin");
appBar().findByText("Tools").should("not.exist");
H.appBar().findByText("Tools").should("not.exist");
cy.visit("/admin/tools/errors");
main().within(() => {
H.main().within(() => {
cy.findByText("Questions that errored when last run").should(
"not.exist",
);

View File

@@ -1,13 +1,6 @@
import { H } from "e2e/support";
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
getBinningButtonForDimension,
openTable,
popover,
restore,
summarize,
visitQuestionAdhoc,
} from "e2e/support/helpers";
const { ORDERS_ID, ORDERS, PEOPLE_ID, PEOPLE, PRODUCTS_ID, PRODUCTS } =
SAMPLE_DATABASE;
@@ -102,7 +95,7 @@ const LONGITUDE_BUCKETS = [
describe("scenarios > binning > binning options", () => {
beforeEach(() => {
cy.intercept("POST", "/api/dataset").as("dataset");
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -193,7 +186,7 @@ describe("scenarios > binning > binning options", () => {
context("via time series footer (metabase#11183)", () => {
// TODO: enable again when metabase#35546 is completed
it.skip("should render time series binning options correctly", () => {
openTable({ table: ORDERS_ID });
H.openTable({ table: ORDERS_ID });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Created At").click();
@@ -278,8 +271,8 @@ describe("scenarios > binning > binning options", () => {
});
function chooseInitialBinningOption({ table, column, mode = null } = {}) {
openTable({ table, mode });
summarize({ mode });
H.openTable({ table, mode });
H.summarize({ mode });
if (mode === "notebook") {
cy.findByText("Count of rows").click();
@@ -294,9 +287,9 @@ function chooseInitialBinningOptionForExplicitJoin({
baseTableQuery,
column,
} = {}) {
visitQuestionAdhoc({ dataset_query: baseTableQuery });
H.visitQuestionAdhoc({ dataset_query: baseTableQuery });
summarize();
H.summarize();
cy.findByTestId("sidebar-right").within(() => {
cy.findByText("Count"); // Test fails without this because of some weird race condition
@@ -305,7 +298,7 @@ function chooseInitialBinningOptionForExplicitJoin({
}
function openBinningListForDimension(column, binning) {
getBinningButtonForDimension({ name: column, isSelected: true })
H.getBinningButtonForDimension({ name: column, isSelected: true })
.should("contain", binning)
.click();
}
@@ -321,7 +314,7 @@ function getAllOptions({ options, isSelected, shouldExpandList } = {}) {
// Custom question has two popovers open.
// The binning options are in the latest (last) one.
// Using `.last()` works even when only one popover is open so it covers both scenarios.
popover()
H.popover()
.last()
.within(() => {
if (shouldExpandList) {

View File

@@ -1,28 +1,12 @@
import { H } from "e2e/support";
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
cartesianChartCircle,
changeBinningForDimension,
chartPathWithFillColor,
entityPickerModal,
entityPickerModalTab,
getBinningButtonForDimension,
getNotebookStep,
openOrdersTable,
popover,
restore,
rightSidebar,
startNewQuestion,
summarize,
visitQuestionAdhoc,
visualize,
} from "e2e/support/helpers";
const { ORDERS, ORDERS_ID } = SAMPLE_DATABASE;
describe("binning related reproductions", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -32,9 +16,9 @@ describe("binning related reproductions", () => {
native: { query: "select * from products limit 5" },
});
startNewQuestion();
entityPickerModal().within(() => {
entityPickerModalTab("Saved questions").click();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Saved questions").click();
cy.findByText("16327").click();
});
@@ -56,7 +40,7 @@ describe("binning related reproductions", () => {
});
it("should be able to update the bucket size / granularity on a field that has sorting applied to it (metabase#16770)", () => {
visitQuestionAdhoc({
H.visitQuestionAdhoc({
dataset_query: {
database: SAMPLE_DB_ID,
query: {
@@ -74,9 +58,9 @@ describe("binning related reproductions", () => {
display: "line",
});
summarize();
H.summarize();
changeBinningForDimension({
H.changeBinningForDimension({
name: "Created At",
fromBinning: "by month",
toBinning: "Year",
@@ -104,32 +88,32 @@ describe("binning related reproductions", () => {
{ loadMetadata: true },
);
startNewQuestion();
entityPickerModal().within(() => {
entityPickerModalTab("Saved questions").click();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Saved questions").click();
cy.findByText("17975").click();
});
getNotebookStep("summarize")
H.getNotebookStep("summarize")
.findByText("Pick a function or metric")
.click();
popover().findByText("Count of rows").click();
getNotebookStep("summarize")
H.popover().findByText("Count of rows").click();
H.getNotebookStep("summarize")
.findByText("Pick a column to group by")
.click();
popover().findByText("CREATED_AT").click();
H.popover().findByText("CREATED_AT").click();
cy.findByRole("button", { name: "Sort" }).click();
popover().findByText("CREATED_AT: Month").click();
H.popover().findByText("CREATED_AT: Month").click();
getNotebookStep("summarize").findByText("CREATED_AT: Month").click();
popover()
H.getNotebookStep("summarize").findByText("CREATED_AT: Month").click();
H.popover()
.findByRole("option", { name: "CREATED_AT" })
.findByLabelText("Temporal bucket")
.click();
popover().last().findByText("Quarter").click();
H.popover().last().findByText("Quarter").click();
getNotebookStep("sort").findByText("CREATED_AT: Quarter");
H.getNotebookStep("sort").findByText("CREATED_AT: Quarter");
});
it("should render binning options when joining on the saved native question (metabase#18646)", () => {
@@ -141,31 +125,31 @@ describe("binning related reproductions", () => {
{ loadMetadata: true },
);
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
cy.icon("join_left_outer").click();
entityPickerModal().within(() => {
entityPickerModalTab("Saved questions").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Saved questions").click();
cy.findByText("18646").click();
});
popover().findByText("Product ID").click();
H.popover().findByText("Product ID").click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("ID").click();
});
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Summarize").click();
popover().findByText("Count of rows").click();
H.popover().findByText("Count of rows").click();
getNotebookStep("summarize")
H.getNotebookStep("summarize")
.findByText("Pick a column to group by")
.click();
popover().findByText("18646").click();
H.popover().findByText("18646").click();
popover().within(() => {
H.popover().within(() => {
cy.findByRole("option", { name: /CREATED_AT/ })
.findByText("by month")
.should("exist");
@@ -174,12 +158,12 @@ describe("binning related reproductions", () => {
});
});
getNotebookStep("summarize").findByText(
H.getNotebookStep("summarize").findByText(
"18646 - Product → Created At: Month",
);
visualize();
cartesianChartCircle();
H.visualize();
H.cartesianChartCircle();
});
it("should display date granularity on Summarize when opened from saved question (metabase#10441, metabase#11439)", () => {
@@ -190,23 +174,23 @@ describe("binning related reproductions", () => {
// it is essential for this repro to find question following these exact steps
// (for example, visiting `/collection/root` would yield different result)
startNewQuestion();
entityPickerModal().within(() => {
entityPickerModalTab("Saved questions").click();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Saved questions").click();
cy.findByText("11439").click();
});
visualize();
summarize();
H.visualize();
H.summarize();
rightSidebar().within(() => {
H.rightSidebar().within(() => {
cy.findAllByRole("listitem", { name: "Created At" })
.eq(0)
.findByLabelText("Temporal bucket")
.click();
});
popover().within(() => {
H.popover().within(() => {
cy.button("More…").click();
cy.findByText("Hour of day").should("exist");
});
@@ -237,7 +221,7 @@ describe("binning related reproductions", () => {
cy.button("Done").click();
});
summarize();
H.summarize();
cy.findByTestId("pinned-dimensions")
.should("contain", "Created At")
@@ -279,13 +263,13 @@ describe("binning related reproductions", () => {
it("should work for simple mode", () => {
openSummarizeOptions("Simple mode");
changeBinningForDimension({
H.changeBinningForDimension({
name: "Average of Subtotal",
fromBinning: "Auto bin",
toBinning: "10 bins",
});
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
});
it("should work for notebook mode", () => {
@@ -298,15 +282,15 @@ describe("binning related reproductions", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Pick a column to group by").click();
changeBinningForDimension({
H.changeBinningForDimension({
name: "Average of Subtotal",
fromBinning: "Auto bin",
toBinning: "10 bins",
});
visualize();
H.visualize();
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
});
});
@@ -329,14 +313,14 @@ describe("binning related reproductions", () => {
},
});
startNewQuestion();
H.startNewQuestion();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Saved Questions").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("SQL Binning").click();
visualize();
summarize();
H.visualize();
H.summarize();
});
it("should render number auto binning correctly (metabase#16670)", () => {
@@ -355,7 +339,7 @@ describe("binning related reproductions", () => {
});
it("should render time series auto binning default bucket correctly (metabase#16671)", () => {
getBinningButtonForDimension({ name: "CREATED_AT" }).should(
H.getBinningButtonForDimension({ name: "CREATED_AT" }).should(
"have.text",
"by month",
);
@@ -379,14 +363,14 @@ describe("binning related reproductions", () => {
});
function openSummarizeOptions(questionType) {
startNewQuestion();
entityPickerModal().within(() => {
entityPickerModalTab("Saved questions").click();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Saved questions").click();
cy.findByText("16379").click();
});
if (questionType === "Simple mode") {
visualize();
summarize();
H.visualize();
H.summarize();
}
}

View File

@@ -1,27 +1,20 @@
import {
chartPathWithFillColor,
echartsContainer,
openPeopleTable,
popover,
restore,
summarize,
} from "e2e/support/helpers";
import { H } from "e2e/support";
import { LONGITUDE_OPTIONS } from "./shared/constants";
describe("scenarios > binning > correctness > longitude", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
openPeopleTable();
summarize();
H.openPeopleTable();
H.summarize();
openPopoverFromDefaultBucketSize("Longitude", "Auto bin");
});
Object.entries(LONGITUDE_OPTIONS).forEach(
([bucketSize, { selected, representativeValues }]) => {
it(`should return correct values for ${bucketSize}`, () => {
popover().within(() => {
H.popover().within(() => {
cy.findByText(bucketSize).click();
});
@@ -33,7 +26,7 @@ describe("scenarios > binning > correctness > longitude", () => {
cy.findByText("Done").click();
getTitle(`Count by Longitude: ${selected}`);
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
assertOnXYAxisLabels();
assertOnXAxisTicks(representativeValues);
@@ -42,7 +35,7 @@ describe("scenarios > binning > correctness > longitude", () => {
);
it("Don't bin", () => {
popover().within(() => {
H.popover().within(() => {
cy.findByText("Don't bin").click();
});
@@ -80,13 +73,13 @@ function getTitle(title) {
}
function assertOnXYAxisLabels() {
echartsContainer().get("text").contains("Count");
echartsContainer().get("text").contains("Longitude");
H.echartsContainer().get("text").contains("Count");
H.echartsContainer().get("text").contains("Longitude");
}
function assertOnXAxisTicks(values) {
if (values) {
echartsContainer().within(() => {
H.echartsContainer().within(() => {
values.forEach(value => {
cy.findByText(value);
});

View File

@@ -1,11 +1,5 @@
import { H } from "e2e/support";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
getBinningButtonForDimension,
popover,
restore,
rightSidebar,
summarize,
} from "e2e/support/helpers";
import { TIME_OPTIONS } from "./shared/constants";
@@ -26,14 +20,14 @@ const questionDetails = {
*/
describe("scenarios > binning > correctness > time series", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.intercept("POST", "/api/dataset").as("dataset");
cy.createQuestion(questionDetails, { visitQuestion: true });
summarize();
H.summarize();
openPopoverFromDefaultBucketSize("Created At", "by month");
});
@@ -41,7 +35,7 @@ describe("scenarios > binning > correctness > time series", () => {
Object.entries(TIME_OPTIONS).forEach(
([bucketSize, { selected, isHiddenByDefault, representativeValues }]) => {
it(`should return correct values for ${bucketSize}`, () => {
popover().within(() => {
H.popover().within(() => {
if (isHiddenByDefault) {
cy.button("More…").click();
}
@@ -49,12 +43,12 @@ describe("scenarios > binning > correctness > time series", () => {
cy.wait("@dataset");
});
getBinningButtonForDimension({
H.getBinningButtonForDimension({
name: "Created At",
isSelected: true,
}).should("have.text", selected);
rightSidebar().button("Done").click();
H.rightSidebar().button("Done").click();
getTitle(`Count by Created At: ${bucketSize}`);
@@ -68,7 +62,7 @@ describe("scenarios > binning > correctness > time series", () => {
});
function openPopoverFromDefaultBucketSize(name, bucket) {
getBinningButtonForDimension({ name })
H.getBinningButtonForDimension({ name })
.should("have.text", bucket)
.click({ force: true });
}

View File

@@ -1,17 +1,5 @@
import { H } from "e2e/support";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
cartesianChartCircle,
changeBinningForDimension,
chartPathWithFillColor,
echartsContainer,
entityPickerModal,
entityPickerModalTab,
restore,
startNewQuestion,
summarize,
tableHeaderClick,
visualize,
} from "e2e/support/helpers";
const { ORDERS_ID, ORDERS, PEOPLE_ID, PEOPLE, PRODUCTS_ID, PRODUCTS } =
SAMPLE_DATABASE;
@@ -23,7 +11,7 @@ const { ORDERS_ID, ORDERS, PEOPLE_ID, PEOPLE, PRODUCTS_ID, PRODUCTS } =
*/
describe("scenarios > binning > from a saved QB question with explicit joins", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.createQuestion({
name: "QB Binning",
@@ -67,19 +55,19 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
context("via simple mode", () => {
beforeEach(() => {
startNewQuestion();
H.startNewQuestion();
entityPickerModal().within(() => {
entityPickerModalTab("Saved questions").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Saved questions").click();
cy.findByText("QB Binning").click();
});
visualize();
summarize();
H.visualize();
H.summarize();
});
it("should work for time series", () => {
changeBinningForDimension({
H.changeBinningForDimension({
name: "People → Birth Date",
fromBinning: "by month",
toBinning: "Year",
@@ -97,7 +85,7 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
cy.findByText("Quarter").click();
cy.wait("@dataset");
echartsContainer()
H.echartsContainer()
.get("text")
.should("contain", "Q1 1968")
.and("contain", "Q1 1978")
@@ -106,7 +94,7 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
});
it("should work for number", () => {
changeBinningForDimension({
H.changeBinningForDimension({
name: "Products → Price",
fromBinning: "Auto bin",
toBinning: "50 bins",
@@ -119,7 +107,7 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
});
it("should work for longitude", () => {
changeBinningForDimension({
H.changeBinningForDimension({
name: "People → Longitude",
fromBinning: "Auto bin",
toBinning: "Bin every 20 degrees",
@@ -134,10 +122,10 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
context("via notebook mode", () => {
beforeEach(() => {
startNewQuestion();
H.startNewQuestion();
entityPickerModal().within(() => {
entityPickerModalTab("Saved questions").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Saved questions").click();
cy.findByText("QB Binning").click();
});
@@ -150,7 +138,7 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
});
it("should work for time series", () => {
changeBinningForDimension({
H.changeBinningForDimension({
name: "People → Birth Date",
fromBinning: "by month",
toBinning: "Year",
@@ -169,7 +157,7 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
cy.findByText("Quarter").click();
cy.wait("@dataset");
echartsContainer()
H.echartsContainer()
.get("text")
.should("contain", "Q1 1965")
.and("contain", "Q1 1972")
@@ -177,7 +165,7 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
});
it("should work for number", () => {
changeBinningForDimension({
H.changeBinningForDimension({
name: "Products → Price",
fromBinning: "Auto bin",
toBinning: "50 bins",
@@ -191,7 +179,7 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
});
it("should work for longitude", () => {
changeBinningForDimension({
H.changeBinningForDimension({
name: "People → Longitude",
fromBinning: "Auto bin",
toBinning: "Bin every 20 degrees",
@@ -207,18 +195,18 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
context("via column popover", () => {
beforeEach(() => {
startNewQuestion();
H.startNewQuestion();
entityPickerModal().within(() => {
entityPickerModalTab("Saved questions").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Saved questions").click();
cy.findByText("QB Binning").click();
});
visualize();
H.visualize();
});
it("should work for time series", () => {
tableHeaderClick("People → Birth Date");
H.tableHeaderClick("People → Birth Date");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Distribution").click();
@@ -228,14 +216,14 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
assertOnXYAxisLabels({ xLabel: "People → Birth Date", yLabel: "Count" });
echartsContainer()
H.echartsContainer()
.get("text", { timeout: 1000 })
.should("contain", "January 1968")
.and("contain", "January 1978")
.and("contain", "January 1988")
.and("contain", "January 1998");
cartesianChartCircle();
H.cartesianChartCircle();
// Make sure time series footer works as well
cy.findByTestId("timeseries-bucket-button").contains("Month").click();
@@ -246,7 +234,7 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Count by People → Birth Date: Quarter");
echartsContainer()
H.echartsContainer()
.get("text")
.should("contain", "Q1 1965")
.and("contain", "Q1 1972")
@@ -257,7 +245,7 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
});
it("should work for number", () => {
tableHeaderClick("Products → Price");
H.tableHeaderClick("Products → Price");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Distribution").click();
@@ -272,11 +260,11 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("25");
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
});
it("should work for longitude", () => {
tableHeaderClick("People → Longitude");
H.tableHeaderClick("People → Longitude");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Distribution").click();
@@ -294,15 +282,15 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("160° W");
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
});
});
});
function assertOnXYAxisLabels({ xLabel, yLabel } = {}) {
echartsContainer().get("text").contains(xLabel);
H.echartsContainer().get("text").contains(xLabel);
echartsContainer().get("text").contains(yLabel);
H.echartsContainer().get("text").contains(yLabel);
}
function waitAndAssertOnRequest(requestAlias) {
@@ -317,22 +305,22 @@ function assertQueryBuilderState({
mode = null,
values,
} = {}) {
mode === "notebook" ? visualize() : waitAndAssertOnRequest("@dataset");
mode === "notebook" ? H.visualize() : waitAndAssertOnRequest("@dataset");
const visualizationSelector = columnType === "time" ? "circle" : "bar";
if (visualizationSelector === "circle") {
cartesianChartCircle();
H.cartesianChartCircle();
} else {
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
}
cy.findByText(title);
echartsContainer().get("text").should("contain", "Count");
H.echartsContainer().get("text").should("contain", "Count");
values &&
echartsContainer().within(() => {
H.echartsContainer().within(() => {
values.forEach(value => {
cy.findByText(value);
});

View File

@@ -1,12 +1,5 @@
import { H } from "e2e/support";
import { ORDERS_QUESTION_ID } from "e2e/support/cypress_sample_instance_data";
import {
changeBinningForDimension,
popover,
restore,
summarize,
visitQuestion,
visualize,
} from "e2e/support/helpers";
/**
* The list of issues this spec covers:
@@ -16,7 +9,7 @@ import {
describe("scenarios > binning > from a saved QB question using implicit joins", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.intercept("POST", "/api/dataset").as("dataset");
@@ -24,12 +17,12 @@ describe("scenarios > binning > from a saved QB question using implicit joins",
context("via simple question", () => {
beforeEach(() => {
visitQuestion(ORDERS_QUESTION_ID);
summarize();
H.visitQuestion(ORDERS_QUESTION_ID);
H.summarize();
});
it("should work for time series", () => {
changeBinningForDimension({
H.changeBinningForDimension({
name: "Birth Date",
fromBinning: "by month",
toBinning: "Year",
@@ -51,7 +44,7 @@ describe("scenarios > binning > from a saved QB question using implicit joins",
});
it("should work for number", () => {
changeBinningForDimension({
H.changeBinningForDimension({
name: "Price",
fromBinning: "Auto bin",
toBinning: "50 bins",
@@ -64,7 +57,7 @@ describe("scenarios > binning > from a saved QB question using implicit joins",
});
it("should work for longitude", () => {
changeBinningForDimension({
H.changeBinningForDimension({
name: "Longitude",
fromBinning: "Auto bin",
toBinning: "Bin every 20 degrees",
@@ -80,13 +73,13 @@ describe("scenarios > binning > from a saved QB question using implicit joins",
context("via custom question", () => {
beforeEach(() => {
cy.visit(`/question/${ORDERS_QUESTION_ID}/notebook`);
summarize({ mode: "notebook" });
H.summarize({ mode: "notebook" });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Count of rows").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Pick a column to group by").click();
// Click "Order" accordion to collapse it and expose the other tables
popover().findByText("Orders").click();
H.popover().findByText("Orders").click();
});
it("should work for time series", () => {
@@ -94,7 +87,7 @@ describe("scenarios > binning > from a saved QB question using implicit joins",
cy.findByText("User").click();
cy.findByPlaceholderText("Find...").type("birth");
changeBinningForDimension({
H.changeBinningForDimension({
name: "Birth Date",
fromBinning: "by month",
toBinning: "Year",
@@ -120,7 +113,7 @@ describe("scenarios > binning > from a saved QB question using implicit joins",
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Product").click();
changeBinningForDimension({
H.changeBinningForDimension({
name: "Price",
fromBinning: "Auto bin",
toBinning: "50 bins",
@@ -138,7 +131,7 @@ describe("scenarios > binning > from a saved QB question using implicit joins",
cy.findByText("User").click();
cy.findByPlaceholderText("Find...").type("longitude");
changeBinningForDimension({
H.changeBinningForDimension({
name: "Longitude",
fromBinning: "Auto bin",
toBinning: "Bin every 20 degrees",
@@ -162,7 +155,7 @@ function waitAndAssertOnRequest(requestAlias) {
function assertQueryBuilderState({ title, mode = null, values } = {}) {
const [firstValue, lastValue] = values;
mode === "notebook" ? visualize() : waitAndAssertOnRequest("@dataset");
mode === "notebook" ? H.visualize() : waitAndAssertOnRequest("@dataset");
cy.findByText(title);
cy.get("[data-testid=cell-data]")

View File

@@ -1,20 +1,11 @@
import { H } from "e2e/support";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
cartesianChartCircle,
changeBinningForDimension,
chartPathWithFillColor,
openTable,
restore,
summarize,
tableHeaderClick,
visualize,
} from "e2e/support/helpers";
const { ORDERS_ID, PEOPLE_ID } = SAMPLE_DATABASE;
describe("scenarios > binning > binning options", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -29,7 +20,7 @@ describe("scenarios > binning > binning options", () => {
getTitle("Count by Total: 50 bins");
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("70");
});
@@ -44,7 +35,7 @@ describe("scenarios > binning > binning options", () => {
getTitle("Count by Created At: Quarter");
cartesianChartCircle();
H.cartesianChartCircle();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Q1 2023");
});
@@ -59,7 +50,7 @@ describe("scenarios > binning > binning options", () => {
getTitle("Count by Longitude: 20°");
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("180° W");
});
@@ -77,7 +68,7 @@ describe("scenarios > binning > binning options", () => {
getTitle("Count by Total: 50 bins");
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("70");
});
@@ -93,7 +84,7 @@ describe("scenarios > binning > binning options", () => {
getTitle("Count by Created At: Quarter");
cartesianChartCircle();
H.cartesianChartCircle();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Q1 2023");
});
@@ -109,7 +100,7 @@ describe("scenarios > binning > binning options", () => {
getTitle("Count by Longitude: 20°");
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("180° W");
});
@@ -117,40 +108,40 @@ describe("scenarios > binning > binning options", () => {
context("via column popover", () => {
it("should work for number", () => {
openTable({ table: ORDERS_ID });
tableHeaderClick("Total");
H.openTable({ table: ORDERS_ID });
H.tableHeaderClick("Total");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Distribution").click();
getTitle("Count by Total: Auto binned");
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("60");
});
it("should work for time series", () => {
openTable({ table: ORDERS_ID });
tableHeaderClick("Created At");
H.openTable({ table: ORDERS_ID });
H.tableHeaderClick("Created At");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Distribution").click();
getTitle("Count by Created At: Month");
cartesianChartCircle();
H.cartesianChartCircle();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("January 2023");
});
it("should work for longitude/latitude", () => {
openTable({ table: PEOPLE_ID });
tableHeaderClick("Longitude");
H.openTable({ table: PEOPLE_ID });
H.tableHeaderClick("Longitude");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Distribution").click();
getTitle("Count by Longitude: Auto binned");
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("170° W");
});
@@ -164,22 +155,22 @@ function chooseInitialBinningOption({
bucketSize,
mode = null,
} = {}) {
openTable({ table, mode });
summarize({ mode });
H.openTable({ table, mode });
H.summarize({ mode });
if (mode === "notebook") {
cy.findByText("Count of rows").click();
cy.findByText("Pick a column to group by").click();
changeBinningForDimension({
H.changeBinningForDimension({
name: column,
fromBinning: defaultBucket,
toBinning: bucketSize,
});
visualize();
H.visualize();
} else {
changeBinningForDimension({
H.changeBinningForDimension({
name: column,
fromBinning: defaultBucket,
toBinning: bucketSize,

View File

@@ -1,11 +1,5 @@
import { H } from "e2e/support";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
chartPathWithFillColor,
getNotebookStep,
openNotebook,
popover,
restore,
} from "e2e/support/helpers";
const { ORDERS, ORDERS_ID } = SAMPLE_DATABASE;
@@ -20,7 +14,7 @@ const CREATED_AT_BREAKOUT = [
describe("issue 23851", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.intercept("POST", "/api/dataset").as("dataset");
});
@@ -41,9 +35,9 @@ describe("issue 23851", () => {
{ visitQuestion: true },
);
chartPathWithFillColor("#509EE3").should("have.length", 7);
chartPathWithFillColor("#509EE3").eq(5).click();
popover().findByText("See these Orders").click();
H.chartPathWithFillColor("#509EE3").should("have.length", 7);
H.chartPathWithFillColor("#509EE3").eq(5).click();
H.popover().findByText("See these Orders").click();
cy.wait("@dataset");
@@ -52,8 +46,8 @@ describe("issue 23851", () => {
"Created At: Day of week is equal to 6",
);
cy.get("[data-testid=cell-data]").should("contain", "109.22");
openNotebook();
getNotebookStep("filter")
H.openNotebook();
H.getNotebookStep("filter")
.findByText("Created At: Day of week is equal to 6")
.should("exist");
});

View File

@@ -1,6 +1,6 @@
import { H } from "e2e/support";
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import { restore } from "e2e/support/helpers";
const { PRODUCTS_ID, PRODUCTS } = SAMPLE_DATABASE;
@@ -35,7 +35,7 @@ const BASE_QUERY = {
describe("issues 34688 and 34690", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsNormalUser();
});

View File

@@ -1,17 +1,5 @@
import { H } from "e2e/support";
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import {
cartesianChartCircle,
changeBinningForDimension,
chartPathWithFillColor,
echartsContainer,
openTable,
restore,
snapshot,
summarize,
tableHeaderClick,
visitQuestionAdhoc,
visualize,
} from "e2e/support/helpers";
const questionDetails = {
name: "SQL Binning",
@@ -25,7 +13,7 @@ let questionId;
describe("scenarios > binning > from a saved sql question", () => {
before(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.createNativeQuestion(questionDetails, {
@@ -35,27 +23,27 @@ describe("scenarios > binning > from a saved sql question", () => {
cy.get("@questionId").then(id => (questionId = id));
snapshot("binningSql");
H.snapshot("binningSql");
});
beforeEach(() => {
cy.intercept("POST", "/api/dataset").as("dataset");
restore("binningSql");
H.restore("binningSql");
cy.signInAsAdmin();
});
context("via simple question", () => {
beforeEach(() => {
openTable({
H.openTable({
database: SAMPLE_DB_ID,
table: `card__${questionId}`,
mode: "notebook",
});
visualize();
H.visualize();
cy.findByTextEnsureVisible("LONGITUDE");
summarize();
H.summarize();
});
it("should work for time series", () => {
@@ -63,7 +51,7 @@ describe("scenarios > binning > from a saved sql question", () => {
* If `result_metadata` is not loaded (SQL question is not run before saving),
* the granularity is much finer and one can see "by minute" as the default bucket (metabase#16671).
*/
changeBinningForDimension({
H.changeBinningForDimension({
name: "CREATED_AT",
fromBinning: "by month",
toBinning: "Year",
@@ -73,11 +61,11 @@ describe("scenarios > binning > from a saved sql question", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Count by CREATED_AT: Year");
cartesianChartCircle();
H.cartesianChartCircle();
});
it("should work for number", () => {
changeBinningForDimension({
H.changeBinningForDimension({
name: "TOTAL",
fromBinning: "Auto bin",
toBinning: "50 bins",
@@ -87,11 +75,11 @@ describe("scenarios > binning > from a saved sql question", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Count by TOTAL: 50 bins");
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
});
it("should work for longitude", () => {
changeBinningForDimension({
H.changeBinningForDimension({
name: "LONGITUDE",
fromBinning: "Auto bin",
toBinning: "Bin every 10 degrees",
@@ -101,13 +89,13 @@ describe("scenarios > binning > from a saved sql question", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Count by LONGITUDE: 10°");
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
});
});
context("via custom question", () => {
beforeEach(() => {
visitQuestionAdhoc(
H.visitQuestionAdhoc(
{
dataset_query: {
database: SAMPLE_DB_ID,
@@ -127,7 +115,7 @@ describe("scenarios > binning > from a saved sql question", () => {
});
it("should work for time series", () => {
changeBinningForDimension({
H.changeBinningForDimension({
name: "CREATED_AT",
fromBinning: "by month",
toBinning: "Year",
@@ -136,15 +124,15 @@ describe("scenarios > binning > from a saved sql question", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Count by CREATED_AT: Year");
visualize(response => {
H.visualize(response => {
assertOnResponse(response);
});
cartesianChartCircle();
H.cartesianChartCircle();
});
it("should work for number", () => {
changeBinningForDimension({
H.changeBinningForDimension({
name: "TOTAL",
fromBinning: "Auto bin",
toBinning: "50 bins",
@@ -153,15 +141,15 @@ describe("scenarios > binning > from a saved sql question", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Count by TOTAL: 50 bins");
visualize(response => {
H.visualize(response => {
assertOnResponse(response);
});
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
});
it("should work for longitude", () => {
changeBinningForDimension({
H.changeBinningForDimension({
name: "LONGITUDE",
fromBinning: "Auto bin",
toBinning: "Bin every 10 degrees",
@@ -170,17 +158,17 @@ describe("scenarios > binning > from a saved sql question", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Count by LONGITUDE: 10°");
visualize(response => {
H.visualize(response => {
assertOnResponse(response);
});
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
});
});
context("via column popover", () => {
beforeEach(() => {
openTable({
H.openTable({
database: SAMPLE_DB_ID,
table: `card__${questionId}`,
});
@@ -188,14 +176,14 @@ describe("scenarios > binning > from a saved sql question", () => {
});
it("should work for time series", () => {
tableHeaderClick("CREATED_AT");
H.tableHeaderClick("CREATED_AT");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Distribution").click();
assertOnXYAxisLabels({ xLabel: "CREATED_AT", yLabel: "Count" });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Count by CREATED_AT: Month");
cartesianChartCircle();
H.cartesianChartCircle();
// Open a popover with bucket options from the time series footer
cy.findByTestId("timeseries-bucket-button").contains("Month").click();
@@ -209,25 +197,25 @@ describe("scenarios > binning > from a saved sql question", () => {
});
it("should work for number", () => {
tableHeaderClick("TOTAL");
H.tableHeaderClick("TOTAL");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Distribution").click();
assertOnXYAxisLabels({ xLabel: "TOTAL", yLabel: "Count" });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Count by TOTAL: Auto binned");
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
});
it("should work for longitude", () => {
tableHeaderClick("LONGITUDE");
H.tableHeaderClick("LONGITUDE");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Distribution").click();
assertOnXYAxisLabels({ xLabel: "LONGITUDE", yLabel: "Count" });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Count by LONGITUDE: Auto binned");
chartPathWithFillColor("#509EE3");
H.chartPathWithFillColor("#509EE3");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("170° W");
});
@@ -235,9 +223,9 @@ describe("scenarios > binning > from a saved sql question", () => {
});
function assertOnXYAxisLabels({ xLabel, yLabel } = {}) {
echartsContainer().get("text").contains(xLabel);
H.echartsContainer().get("text").contains(xLabel);
echartsContainer().get("text").contains(yLabel);
H.echartsContainer().get("text").contains(yLabel);
}
function waitAndAssertOnRequest(requestAlias) {

View File

@@ -1,375 +1,392 @@
import dayjs from "dayjs";
import { assocIn } from "icepick";
import { P, isMatching } from "ts-pattern";
import { H } from "e2e/support";
import { SAMPLE_DB_TABLES } from "e2e/support/cypress_data";
const { STATIC_ORDERS_ID } = SAMPLE_DB_TABLES;
import {
FIRST_COLLECTION_ID,
ORDERS_QUESTION_ID,
} from "e2e/support/cypress_sample_instance_data";
import {
createCollection,
describeEE,
describeWithSnowplowEE,
enableTracking,
expectGoodSnowplowEvent,
expectNoBadSnowplowEvents,
getCollectionActions,
main,
modal,
navigationSidebar,
onlyOnOSS,
popover,
resetSnowplow,
restore,
setTokenFeatures,
undo,
visitCollection,
} from "e2e/support/helpers";
describe("scenarios > collections > clean up", () => {
beforeEach(() => {
restore();
H.restore();
});
describe("oss", { tags: "@OSS" }, () => {
beforeEach(() => {
onlyOnOSS();
H.onlyOnOSS();
cy.signInAsAdmin();
});
it("feature should not be available in OSS", () => {
visitCollection(FIRST_COLLECTION_ID);
H.visitCollection(FIRST_COLLECTION_ID);
cleanUpAlert().should("not.exist");
collectionMenu().click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("Clean things up").should("not.exist");
});
});
});
describeEE("action menu", () => {
it("should show in proper contexts", () => {
cy.signInAsAdmin();
setTokenFeatures("all");
H.describeEE("ee", () => {
H.describeEE("action menu", () => {
it("should show in proper contexts", () => {
cy.signInAsAdmin();
H.setTokenFeatures("all");
cy.log("should not show in custom analytics collections");
visitCollection("root");
navigationSidebar().within(() => {
cy.findByText("Usage analytics").click();
cy.findByText("Custom reports").click();
});
collectionMenu().click();
popover().within(() => {
cy.findByText("Clean things up").should("not.exist");
});
cy.log(
"should show in a normal collection that user has write access to",
);
visitCollection(FIRST_COLLECTION_ID);
collectionMenu().click();
popover().within(() => {
cy.findByText("Clean things up").should("exist");
});
cy.log("should not show in custom analytics collections");
popover().within(() => {
cy.findByText("Move to trash").click();
});
modal().within(() => {
cy.findByText("Move to trash").click();
});
cy.findByTestId("archive-banner").should("exist");
getCollectionActions().should("not.exist");
cy.log("empty collection");
createCollection({ name: "Empty" }).then(({ body: { id } }) => {
visitCollection(id);
cy.log("should not show in custom analytics collections");
H.visitCollection("root");
H.navigationSidebar().within(() => {
cy.findByText("Usage analytics").click();
cy.findByText("Custom reports").click();
});
collectionMenu().click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("Clean things up").should("not.exist");
});
});
});
it("should not show to users who do not have write permissions to a collection", () => {
cy.signIn("readonly");
visitCollection(FIRST_COLLECTION_ID);
collectionMenu().should("not.exist");
});
});
describeWithSnowplowEE("clean up collection modal", () => {
beforeEach(() => {
resetSnowplow();
cy.signInAsAdmin();
setTokenFeatures("all");
enableTracking();
});
afterEach(() => {
expectNoBadSnowplowEvents();
});
it("should be able to clean up stale items", () => {
seedMainTestData().then(seedData => {
const firstAlphabeticalName = "Bulk dashboard 1";
const lastAlphabeticalName = "Bulk question 9";
cy.log("should be able to navigate to clean up modal");
visitCollection(seedData.collection.id);
selectCleanThingsUpCollectionAction();
cy.url().should("include", "cleanup");
cy.log("should render all items of current collection");
assertStaleItemCount(seedData.totalStaleItemCount);
cy.log("should be able to filter to fewer items");
setDateFilter("1 year");
assertNoPagination();
cy.log("should be able to recursively show stale items");
setDateFilter("6 months");
recursiveFilter().click();
assertStaleItemCount(seedData.recursiveTotalItemCount);
cy.log("pagination should work as expected");
pagination().within(() => {
cy.findByText("1 - 10").should("exist");
});
cleanUpModal().within(() => {
cy.findByText(lastAlphabeticalName).should("not.exist");
});
pagination().within(() => {
cy.findAllByTestId("next-page-btn").click();
cy.findByText("11 - 19").should("exist");
});
cleanUpModal().within(() => {
cy.findByText(lastAlphabeticalName).should("exist");
});
pagination().within(() => {
cy.findAllByTestId("previous-page-btn").click();
cy.findByText("1 - 10").should("exist");
cy.findAllByTestId("next-page-btn").click();
cy.findByText("11 - 19").should("exist");
cy.log(
"should show in a normal collection that user has write access to",
);
H.visitCollection(FIRST_COLLECTION_ID);
collectionMenu().click();
H.popover().within(() => {
cy.findByText("Clean things up").should("exist");
});
cy.log("pagination should reset when the date filter changes");
setDateFilter("3 months");
pagination().within(() => {
cy.findByText("1 - 10").should("exist");
cy.findAllByTestId("next-page-btn").click();
cy.findByText("11 - 19").should("exist");
cy.log("should not show in custom analytics collections");
H.popover().within(() => {
cy.findByText("Move to trash").click();
});
// pagination should reset when the recursive filter changes
recursiveFilter().click();
pagination().within(() => {
cy.findByText("1 - 10").should("exist");
H.modal().within(() => {
cy.findByText("Move to trash").click();
});
recursiveFilter().click();
cy.findByTestId("archive-banner").should("exist");
H.getCollectionActions().should("not.exist");
cy.log("should be able to sort items by name and last used at columns");
cleanUpModal().within(() => {
cy.get("table").within(() => {
cy.findByText(firstAlphabeticalName).should("exist");
cy.findByText(lastAlphabeticalName).should("not.exist");
cy.findByText(/Name/).click();
cy.findByText(firstAlphabeticalName).should("not.exist");
cy.findByText(lastAlphabeticalName).should("exist");
cy.findByText(/Name/).click();
cy.findByText(firstAlphabeticalName).should("exist");
cy.findByText(lastAlphabeticalName).should("not.exist");
cy.log("should not show in empty collections");
H.createCollection({ name: "Empty" }).then(({ body: { id } }) => {
H.visitCollection(id);
collectionMenu().click();
H.popover().within(() => {
cy.findByText("Clean things up").should("not.exist");
});
});
cy.log("should be able to move stale items to the trash");
recursiveFilter().click();
assertStaleItemCount(seedData.totalStaleItemCount);
cy.log("should not show in sample collections");
H.createCollection({ name: "Fake sample collection" }).then(
({ body: { id } }) => {
// make sure that it has one stale item
bulkCreateQuestions(1, { collection_id: id }).then(([question]) => {
makeItemsStale(
[question.id],
"card",
dayjs()
.startOf("day")
.subtract(6, "months")
.format("YYYY-MM-DD"),
);
selectAllItems();
moveToTrash();
assertNoPagination();
// trip is_sample flag
cy.intercept("GET", `/api/collection/${id}`, req => {
req.on("response", res => {
res.send(assocIn(res.body, ["is_sample"], true));
});
});
expectGoodSnowplowEvent(
event =>
isMatching(
{
event: "moved-to-trash",
event_detail: P.union("dashboard", "question"),
target_id: P.number,
triggered_from: "cleanup_modal",
duration_ms: P.number,
result: "success",
},
event,
),
10,
// assert we don't show clean up option
H.visitCollection(id);
collectionMenu().click();
H.popover().within(() => {
cy.findByText("Clean things up").should("not.exist");
});
cleanUpAlert().should("not.exist");
});
},
);
});
// Because cutoff_date will be relative to the current date, we simply check
// that it exists and is a string. Snowplow will assert that it is in the correct
// format
expectGoodSnowplowEvent(
event =>
event &&
event.event === "stale_items_archived" &&
event.collection_id === seedData.collection.id &&
event.total_items_archived === 10 &&
typeof event.cutoff_date === "string",
);
it("should not show to users who do not have write permissions to a collection", () => {
cy.signIn("readonly");
H.visitCollection(FIRST_COLLECTION_ID);
collectionMenu().should("not.exist");
});
});
undo();
assertStaleItemCount(seedData.totalStaleItemCount);
H.describeWithSnowplowEE("clean up collection modal", () => {
beforeEach(() => {
H.resetSnowplow();
cy.signInAsAdmin();
H.setTokenFeatures("all");
H.enableTracking();
});
selectAllItems();
moveToTrash();
assertNoPagination();
afterEach(() => {
H.expectNoBadSnowplowEvents();
});
selectAllItems();
moveToTrash();
it("should be able to clean up stale items", () => {
seedMainTestData().then(seedData => {
const firstAlphabeticalName = "Bulk dashboard 1";
const lastAlphabeticalName = "Bulk question 9";
closeCleanUpModal();
cy.url().should("not.include", "cleanup");
cy.log("should be able to navigate to clean up modal");
H.visitCollection(seedData.collection.id);
selectCleanThingsUpCollectionAction();
cy.url().should("include", "cleanup");
cy.log(
"collection items view should reflect the actions taken in the clean up modal",
);
main().within(() => {
cy.get("tr").should(
"have.length",
seedData.notStaleItemCount +
1 + // child collection
1, // header row
cy.log("should render all items of current collection");
assertStaleItemCount(seedData.totalStaleItemCount);
cy.log("should be able to filter to fewer items");
setDateFilter("1 year");
assertNoPagination();
cy.log("should be able to recursively show stale items");
setDateFilter("6 months");
recursiveFilter().click();
assertStaleItemCount(seedData.recursiveTotalItemCount);
cy.log("pagination should work as expected");
pagination().within(() => {
cy.findByText("1 - 10").should("exist");
});
cleanUpModal().within(() => {
cy.findByText(lastAlphabeticalName).should("not.exist");
});
pagination().within(() => {
cy.findAllByTestId("next-page-btn").click();
cy.findByText("11 - 19").should("exist");
});
cleanUpModal().within(() => {
cy.findByText(lastAlphabeticalName).should("exist");
});
pagination().within(() => {
cy.findAllByTestId("previous-page-btn").click();
cy.findByText("1 - 10").should("exist");
cy.findAllByTestId("next-page-btn").click();
cy.findByText("11 - 19").should("exist");
});
cy.log("pagination should reset when the date filter changes");
setDateFilter("3 months");
pagination().within(() => {
cy.findByText("1 - 10").should("exist");
cy.findAllByTestId("next-page-btn").click();
cy.findByText("11 - 19").should("exist");
});
// pagination should reset when the recursive filter changes
recursiveFilter().click();
pagination().within(() => {
cy.findByText("1 - 10").should("exist");
});
recursiveFilter().click();
cy.log(
"should be able to sort items by name and last used at columns",
);
cleanUpModal().within(() => {
cy.get("table").within(() => {
cy.findByText(firstAlphabeticalName).should("exist");
cy.findByText(lastAlphabeticalName).should("not.exist");
cy.findByText(/Name/).click();
cy.findByText(firstAlphabeticalName).should("not.exist");
cy.findByText(lastAlphabeticalName).should("exist");
cy.findByText(/Name/).click();
cy.findByText(firstAlphabeticalName).should("exist");
cy.findByText(lastAlphabeticalName).should("not.exist");
});
});
cy.log("should be able to move stale items to the trash");
recursiveFilter().click();
assertStaleItemCount(seedData.totalStaleItemCount);
selectAllItems();
moveToTrash();
assertNoPagination();
H.expectGoodSnowplowEvent(
event =>
isMatching(
{
event: "moved-to-trash",
event_detail: P.union("dashboard", "question"),
target_id: P.number,
triggered_from: "cleanup_modal",
duration_ms: P.number,
result: "success",
},
event,
),
10,
);
// Because cutoff_date will be relative to the current date, we simply check
// that it exists and is a string. Snowplow will assert that it is in the correct
// format
H.expectGoodSnowplowEvent(
event =>
event &&
event.event === "stale_items_archived" &&
event.collection_id === seedData.collection.id &&
event.total_items_archived === 10 &&
typeof event.cutoff_date === "string",
);
H.undo();
assertStaleItemCount(seedData.totalStaleItemCount);
selectAllItems();
moveToTrash();
assertNoPagination();
selectAllItems();
moveToTrash();
closeCleanUpModal();
cy.url().should("not.include", "cleanup");
cy.log(
"collection items view should reflect the actions taken in the clean up modal",
);
H.main().within(() => {
cy.get("tr").should(
"have.length",
seedData.notStaleItemCount +
1 + // child collection
1, // header row
);
});
makeItemStale(ORDERS_QUESTION_ID, "card");
cy.findByRole("navigation").findByText("Our analytics").click();
selectCleanThingsUpCollectionAction();
cy.url().should("include", "cleanup");
selectAllItems();
moveToTrash();
// Ensure that stale items in Our Analytics are maked with a null collection id
H.expectGoodSnowplowEvent(
event =>
event &&
event.event === "stale_items_archived" &&
event.collection_id === null &&
event.total_items_archived === 1 &&
typeof event.cutoff_date === "string",
);
});
makeItemStale(ORDERS_QUESTION_ID, "card");
cy.findByRole("navigation").findByText("Our analytics").click();
selectCleanThingsUpCollectionAction();
cy.url().should("include", "cleanup");
selectAllItems();
moveToTrash();
// Ensure that stale items in Our Analytics are maked with a null collection id
expectGoodSnowplowEvent(
event =>
event &&
event.event === "stale_items_archived" &&
event.collection_id === null &&
event.total_items_archived === 1 &&
typeof event.cutoff_date === "string",
);
});
});
it("show empty and error states correctly", () => {
cy.log("should handle empty state");
cy.intercept("GET", "/api/ee/stale/**?**").as("stale-items");
it("show empty and error states correctly", () => {
cy.log("should handle empty state");
cy.intercept("GET", "/api/ee/stale/**?**").as("stale-items");
// visit collection w/ items but no stale items
createCollection({ name: "Not empty w/ not stale items" })
.then(({ body: { id } }) => id)
.as("collectionId");
// visit collection w/ items but no stale items
H.createCollection({ name: "Not empty w/ not stale items" })
.then(({ body: { id } }) => id)
.as("collectionId");
cy.get("@collectionId").then(id => {
return bulkCreateQuestions(2, { collection_id: id }).then(() => {
visitCollection(id);
cy.get("@collectionId").then(id => {
return bulkCreateQuestions(2, { collection_id: id }).then(() => {
H.visitCollection(id);
});
});
cy.log("should render a table w/ contents");
H.main().within(() => {
cy.findByText("Type");
cy.findByText("Name");
});
selectCleanThingsUpCollectionAction();
cy.wait("@stale-items");
cleanUpModal().within(() => {
emptyState().should("exist");
});
cy.log("should handle error state");
cy.intercept("GET", "/api/ee/stale/**?**", {
statusCode: 500,
}).as("stale-items");
setDateFilter("1 year");
cy.wait("@stale-items");
errorState().should("exist");
});
cy.log("should render a table w/ contents");
main().within(() => {
cy.findByText("Type");
cy.findByText("Name");
});
selectCleanThingsUpCollectionAction();
cy.wait("@stale-items");
cleanUpModal().within(() => {
emptyState().should("exist");
});
cy.log("should handle error state");
cy.intercept("GET", "/api/ee/stale/**?**", {
statusCode: 500,
}).as("stale-items");
setDateFilter("1 year");
cy.wait("@stale-items");
errorState().should("exist");
});
});
describeEE("clean up collection alert", () => {
beforeEach(() => {
cy.signInAsAdmin();
setTokenFeatures("all");
});
it("should show admins clean up alert if there's something to clean up in a collection", () => {
cy.log("should not show alert if there's nothing stale");
cy.intercept("GET", "/api/ee/stale/*").as("staleItems");
visitCollection(FIRST_COLLECTION_ID);
cy.wait("@staleItems");
cleanUpAlert().should("not.exist");
H.describeEE("clean up collection alert", () => {
beforeEach(() => {
cy.signInAsAdmin();
H.setTokenFeatures("all");
});
// seed slightly stale content
cy.createQuestion({
name: "Not stale enough",
collection_id: FIRST_COLLECTION_ID,
query: { "source-table": STATIC_ORDERS_ID },
}).then(req => {
makeItemsStale(
[req.body.id],
"card",
dayjs().startOf("day").subtract(2, "months").format("YYYY-MM-DD"),
);
it("should show admins clean up alert if there's something to clean up in a collection", () => {
cy.log("should not show alert if there's nothing stale");
cy.intercept("GET", "/api/ee/stale/*").as("staleItems");
H.visitCollection(FIRST_COLLECTION_ID);
cy.log("should not be shown with 2 month stale content");
// seed slightly stale content
cy.createQuestion({
name: "Not stale enough",
collection_id: FIRST_COLLECTION_ID,
query: { "source-table": STATIC_ORDERS_ID },
}).then(req => {
makeItemsStale(
[req.body.id],
"card",
dayjs().startOf("day").subtract(2, "months").format("YYYY-MM-DD"),
);
cy.log("should not be shown with 2 month stale content");
cy.reload();
cleanUpAlert().should("not.exist");
});
// seed stale enough content
cy.createQuestion({
name: "Stale enough",
collection_id: FIRST_COLLECTION_ID,
query: { "source-table": STATIC_ORDERS_ID },
}).then(req => {
makeItemsStale(
[req.body.id],
"card",
dayjs().startOf("day").subtract(3, "months").format("YYYY-MM-DD"),
);
cy.log("should be shown with 3 month stale content");
cy.reload();
cleanUpAlert()
.should("exist")
.findByText(/Get rid of unused content/)
.click();
cy.url().should("include", "cleanup");
closeCleanUpModal();
});
cy.log("should not show alert if user is not admin");
cy.signOut();
cy.signInAsNormalUser();
cy.reload();
cy.wait("@staleItems");
cleanUpAlert().should("not.exist");
});
// seed stale enough content
cy.createQuestion({
name: "Stale enough",
collection_id: FIRST_COLLECTION_ID,
query: { "source-table": STATIC_ORDERS_ID },
}).then(req => {
makeItemsStale(
[req.body.id],
"card",
dayjs().startOf("day").subtract(3, "months").format("YYYY-MM-DD"),
);
cy.log("should be shown with 3 month stale content");
cy.reload();
cleanUpAlert()
.should("exist")
.findByText(/Get rid of unused content/)
.click();
cy.url().should("include", "cleanup");
closeCleanUpModal();
});
cy.log("should not show alert if user is not admin");
cy.signOut();
cy.signInAsNormalUser();
cy.reload();
cy.wait("@staleItems");
cleanUpAlert().should("not.exist");
});
});
});
// elements
const collectionMenu = () => getCollectionActions().icon("ellipsis");
const collectionMenu = () => H.getCollectionActions().icon("ellipsis");
const cleanUpModal = () => cy.findAllByTestId("cleanup-collection-modal");
const closeCleanUpModal = () =>
cy.findAllByTestId("cleanup-collection-modal-close-btn").click();
@@ -383,9 +400,9 @@ const errorState = () => cy.findByText(/An error occurred/);
// actions
const selectCleanThingsUpCollectionAction = () => {
getCollectionActions().should("exist");
H.getCollectionActions().should("exist");
collectionMenu().should("exist").click();
popover()
H.popover()
.should("exist")
.within(() => {
cy.findByText("Clean things up").click();
@@ -393,7 +410,7 @@ const selectCleanThingsUpCollectionAction = () => {
};
const setDateFilter = timeSpan => {
dateFilter().click();
popover().within(() => {
H.popover().within(() => {
cy.findByText(timeSpan).click();
});
};
@@ -503,7 +520,7 @@ function seedMainTestData() {
//
// also create a child collection with a few stale items so that we can test for
// the recursive filter
createCollection({ name: "Clean up test" })
H.createCollection({ name: "Clean up test" })
.then(({ body }) => {
collection = body;
return body;
@@ -534,7 +551,7 @@ function seedMainTestData() {
});
cy.get("@cleanUpCollection").then(({ id }) => {
createCollection({ name: "Child clean up test", parent_id: id })
H.createCollection({ name: "Child clean up test", parent_id: id })
.then(({ body: collection }) => collection)
.as("cleanUpChildCollection");
});

View File

@@ -1,17 +1,10 @@
import { H } from "e2e/support";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
ORDERS_COUNT_QUESTION_ID,
ORDERS_DASHBOARD_ID,
ORDERS_QUESTION_ID,
} from "e2e/support/cypress_sample_instance_data";
import {
dragAndDrop,
getPinnedSection,
openPinnedItemMenu,
openUnpinnedItemMenu,
popover,
restore,
} from "e2e/support/helpers";
const { ORDERS, ORDERS_ID, PEOPLE } = SAMPLE_DATABASE;
@@ -76,7 +69,7 @@ const SQL_QUESTION_DETAILS_WITH_DEFAULT_VALUE = {
describe("scenarios > collection pinned items overview", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.intercept("POST", "/api/card/**/query").as("getCardQuery");
@@ -85,11 +78,11 @@ describe("scenarios > collection pinned items overview", () => {
it("should be able to pin a dashboard", () => {
openRootCollection();
openUnpinnedItemMenu(DASHBOARD_NAME);
popover().findByText("Pin this").click();
H.openUnpinnedItemMenu(DASHBOARD_NAME);
H.popover().findByText("Pin this").click();
cy.wait("@getPinnedItems");
getPinnedSection().within(() => {
H.getPinnedSection().within(() => {
cy.icon("dashboard").should("be.visible");
cy.findByText("A dashboard").should("be.visible");
cy.findByText(DASHBOARD_NAME).click();
@@ -99,11 +92,11 @@ describe("scenarios > collection pinned items overview", () => {
it("should be able to pin a question", () => {
openRootCollection();
openUnpinnedItemMenu(QUESTION_NAME);
popover().findByText("Pin this").click();
H.openUnpinnedItemMenu(QUESTION_NAME);
H.popover().findByText("Pin this").click();
cy.wait(["@getPinnedItems", "@getCardQuery"]);
getPinnedSection().within(() => {
H.getPinnedSection().within(() => {
cy.findByText("18,760").should("be.visible");
cy.findByText(QUESTION_NAME).click();
cy.url().should("include", `/question/${ORDERS_COUNT_QUESTION_ID}`);
@@ -118,7 +111,7 @@ describe("scenarios > collection pinned items overview", () => {
openRootCollection();
cy.wait("@getCardQuery");
getPinnedSection().within(() => {
H.getPinnedSection().within(() => {
cy.findByText(PIVOT_QUESTION_DETAILS.name).should("be.visible");
cy.findByText("Created At: Month").should("be.visible");
cy.findByText("Count").should("be.visible");
@@ -129,11 +122,11 @@ describe("scenarios > collection pinned items overview", () => {
cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { type: "model" });
openRootCollection();
openUnpinnedItemMenu(MODEL_NAME);
popover().findByText("Pin this").click();
H.openUnpinnedItemMenu(MODEL_NAME);
H.popover().findByText("Pin this").click();
cy.wait("@getPinnedItems");
getPinnedSection().within(() => {
H.getPinnedSection().within(() => {
cy.icon("model").should("be.visible");
cy.findByText(MODEL_NAME).should("be.visible");
cy.findByText("A model").click();
@@ -147,11 +140,11 @@ describe("scenarios > collection pinned items overview", () => {
});
openRootCollection();
openPinnedItemMenu(DASHBOARD_NAME);
popover().findByText("Unpin").click();
H.openPinnedItemMenu(DASHBOARD_NAME);
H.popover().findByText("Unpin").click();
cy.wait("@getPinnedItems");
getPinnedSection().should("not.exist");
H.getPinnedSection().should("not.exist");
});
it("should be able to move a pinned dashboard", () => {
@@ -160,8 +153,8 @@ describe("scenarios > collection pinned items overview", () => {
});
openRootCollection();
openPinnedItemMenu(DASHBOARD_NAME);
popover().findByText("Move").click();
H.openPinnedItemMenu(DASHBOARD_NAME);
H.popover().findByText("Move").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(`Move "${DASHBOARD_NAME}"?`).should("be.visible");
@@ -173,8 +166,8 @@ describe("scenarios > collection pinned items overview", () => {
});
openRootCollection();
openPinnedItemMenu(DASHBOARD_NAME);
popover().findByText("Duplicate").click();
H.openPinnedItemMenu(DASHBOARD_NAME);
H.popover().findByText("Duplicate").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(`Duplicate "${DASHBOARD_NAME}" and its questions`).should(
@@ -188,11 +181,11 @@ describe("scenarios > collection pinned items overview", () => {
});
openRootCollection();
openPinnedItemMenu(DASHBOARD_NAME);
popover().findByText("Move to trash").click();
H.openPinnedItemMenu(DASHBOARD_NAME);
H.popover().findByText("Move to trash").click();
cy.wait("@getPinnedItems");
getPinnedSection().should("not.exist");
H.getPinnedSection().should("not.exist");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(DASHBOARD_NAME).should("not.exist");
});
@@ -203,11 +196,11 @@ describe("scenarios > collection pinned items overview", () => {
});
openRootCollection();
openPinnedItemMenu(QUESTION_NAME);
popover().findByText("Don’t show visualization").click();
H.openPinnedItemMenu(QUESTION_NAME);
H.popover().findByText("Don’t show visualization").click();
cy.wait("@getPinnedItems");
getPinnedSection().within(() => {
H.getPinnedSection().within(() => {
cy.findByText("18,760").should("not.exist");
cy.findByText("A question").should("be.visible");
cy.findByText(QUESTION_NAME).click();
@@ -222,11 +215,11 @@ describe("scenarios > collection pinned items overview", () => {
});
openRootCollection();
openPinnedItemMenu(QUESTION_NAME);
popover().findByText("Show visualization").click();
H.openPinnedItemMenu(QUESTION_NAME);
H.popover().findByText("Show visualization").click();
cy.wait(["@getPinnedItems", "@getCardQuery"]);
getPinnedSection().within(() => {
H.getPinnedSection().within(() => {
cy.findByText(QUESTION_NAME).should("be.visible");
cy.findByText("18,760").should("be.visible");
});
@@ -241,7 +234,7 @@ describe("scenarios > collection pinned items overview", () => {
);
openRootCollection();
getPinnedSection().within(() => {
H.getPinnedSection().within(() => {
cy.findByText(SQL_QUESTION_DETAILS_WITH_DEFAULT_VALUE.name).should(
"be.visible",
);
@@ -257,7 +250,7 @@ describe("scenarios > collection pinned items overview", () => {
);
openRootCollection();
getPinnedSection().within(() => {
H.getPinnedSection().within(() => {
cy.findByText(SQL_QUESTION_DETAILS_WITH_DEFAULT_VALUE.name).should(
"be.visible",
);
@@ -287,7 +280,7 @@ describe("scenarios > collection pinned items overview", () => {
// for example, this test would not have caught https://github.com/metabase/metabase/issues/30614
// even libraries like https://github.com/dmtrKovalenko/cypress-real-events rely on firing events
// on specific elements rather than truly simulating mouse movements across the screen
dragAndDrop("draggingViz", "pinnedItems");
H.dragAndDrop("draggingViz", "pinnedItems");
cy.findByTestId("collection-table")
.findByText("Orders, Count, Grouped by Created At (year)")

View File

@@ -1,32 +1,17 @@
import { H } from "e2e/support";
import {
ADMIN_PERSONAL_COLLECTION_ID,
FIRST_COLLECTION_ID,
ORDERS_COUNT_QUESTION_ID,
ORDERS_QUESTION_ID,
} from "e2e/support/cypress_sample_instance_data";
import {
assertPermissionTable,
describeEE,
entityPickerModal,
entityPickerModalTab,
getCollectionActions,
modal,
modifyPermission,
moveOpenedCollectionTo,
navigationSidebar,
openCollectionMenu,
popover,
restore,
setTokenFeatures,
startNewQuestion,
} from "e2e/support/helpers";
describe("issue 20911", () => {
const COLLECTION_ACCESS_PERMISSION_INDEX = 0;
const FIRST_COLLECTION = "First collection";
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.intercept("GET", "/api/collection/graph").as("getGraph");
});
@@ -34,7 +19,7 @@ describe("issue 20911", () => {
it("should allow to change sub-collections permissions after access change (metabase#20911)", () => {
cy.visit("/collection/root/permissions");
cy.wait("@getGraph");
assertPermissionTable([
H.assertPermissionTable([
["Administrators", "Curate"],
["All Users", "No access"],
["collection", "Curate"],
@@ -42,32 +27,32 @@ describe("issue 20911", () => {
["nosql", "No access"],
["readonly", "View"],
]);
modifyPermission(
H.modifyPermission(
"collection",
COLLECTION_ACCESS_PERMISSION_INDEX,
"No access",
false,
);
modifyPermission(
H.modifyPermission(
"collection",
COLLECTION_ACCESS_PERMISSION_INDEX,
"No access",
true,
);
modal().within(() => {
H.modal().within(() => {
cy.button("Save").click();
});
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
cy.findByText(FIRST_COLLECTION).click();
});
getCollectionActions().within(() => {
H.getCollectionActions().within(() => {
cy.icon("ellipsis").click();
});
popover().within(() => {
H.popover().within(() => {
cy.icon("lock").click();
});
assertPermissionTable([
H.assertPermissionTable([
["Administrators", "Curate"],
["All Users", "No access"],
["collection", "No access"],
@@ -107,7 +92,7 @@ describe("issue 24660", () => {
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
createParentCollectionAndMoveQuestionToIt(ORDERS_QUESTION_ID);
@@ -115,9 +100,9 @@ describe("issue 24660", () => {
});
it("should properly show contents of different collections with the same name (metabase#24660)", () => {
startNewQuestion();
entityPickerModal().within(() => {
entityPickerModalTab("Saved questions").click();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Saved questions").click();
cy.findAllByText(collectionName).first().click();
cy.findByText(questions[ORDERS_QUESTION_ID]).should("exist");
@@ -126,11 +111,11 @@ describe("issue 24660", () => {
});
});
describeEE("issue 30235", () => {
H.describeEE("issue 30235", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
});
it("should allow to turn to official collection after moving it from personal to root parent collection (metabase#30235)", () => {
@@ -142,11 +127,11 @@ describeEE("issue 30235", () => {
}).then(({ body: { id } }) => {
cy.visit(`/collection/${id}`);
moveOpenedCollectionTo("Our analytics");
H.moveOpenedCollectionTo("Our analytics");
openCollectionMenu();
H.openCollectionMenu();
popover().within(() => {
H.popover().within(() => {
cy.findByText("Make collection official").should("be.visible");
cy.findByText("Edit permissions").should("be.visible");
});

View File

@@ -1,6 +1,7 @@
import { assocIn } from "icepick";
import _ from "underscore";
import { H } from "e2e/support";
import { SAMPLE_DB_ID, USERS, USER_GROUPS } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
@@ -11,28 +12,6 @@ import {
SECOND_COLLECTION_ID,
THIRD_COLLECTION_ID,
} from "e2e/support/cypress_sample_instance_data";
import {
closeNavigationSidebar,
createCollection,
createQuestion,
dragAndDrop,
entityPickerModal,
entityPickerModalItem,
entityPickerModalTab,
getPinnedSection,
moveOpenedCollectionTo,
navigationSidebar,
openCollectionItemMenu,
openCollectionMenu,
openNavigationSidebar,
openOrdersTable,
openUnpinnedItemMenu,
pickEntity,
popover,
restore,
sidesheet,
visitCollection,
} from "e2e/support/helpers";
import { displaySidebarChildOf } from "./helpers/e2e-collections-sidebar.js";
@@ -42,7 +21,7 @@ const { ORDERS, ORDERS_ID, FEEDBACK_ID } = SAMPLE_DATABASE;
describe("scenarios > collection defaults", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.intercept("GET", "/api/**/items?pinned_state*").as("getPinnedItems");
cy.intercept("GET", "/api/collection/tree**").as("getTree");
@@ -78,7 +57,7 @@ describe("scenarios > collection defaults", () => {
.click();
});
pickEntity({
H.pickEntity({
path: ["Our analytics", `Collection ${COLLECTIONS_COUNT}`],
select: true,
tab: "Collections",
@@ -97,7 +76,7 @@ describe("scenarios > collection defaults", () => {
it("should navigate effortlessly through collections tree", () => {
visitRootCollection();
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
cy.log(
"should allow a user to expand a collection without navigating to it",
);
@@ -135,9 +114,9 @@ describe("scenarios > collection defaults", () => {
"navigating directly to a collection should expand it and show its children",
);
visitCollection(SECOND_COLLECTION_ID);
H.visitCollection(SECOND_COLLECTION_ID);
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
cy.findByText("Second collection");
cy.findByText("Third collection");
@@ -159,10 +138,10 @@ describe("scenarios > collection defaults", () => {
},
);
visitCollection(THIRD_COLLECTION_ID);
H.visitCollection(THIRD_COLLECTION_ID);
// 1. Expand so that deeply nested collection is showing
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
displaySidebarChildOf("Fourth collection");
});
@@ -183,19 +162,19 @@ describe("scenarios > collection defaults", () => {
"should be able to toggle collections sidebar when switched to mobile screen size",
);
navigationSidebar().should("have.attr", "aria-hidden", "true");
openNavigationSidebar();
H.navigationSidebar().should("have.attr", "aria-hidden", "true");
H.openNavigationSidebar();
closeNavigationSidebar();
navigationSidebar().should("have.attr", "aria-hidden", "true");
H.closeNavigationSidebar();
H.navigationSidebar().should("have.attr", "aria-hidden", "true");
cy.log(
"should close collections sidebar when collection is clicked in mobile screen size",
);
openNavigationSidebar();
H.openNavigationSidebar();
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
cy.findByText("First collection").click();
});
@@ -204,7 +183,7 @@ describe("scenarios > collection defaults", () => {
"First collection",
);
navigationSidebar().should("have.attr", "aria-hidden", "true");
H.navigationSidebar().should("have.attr", "aria-hidden", "true");
});
});
@@ -213,7 +192,7 @@ describe("scenarios > collection defaults", () => {
description: "[link](https://metabase.com)",
});
visitCollection(FIRST_COLLECTION_ID);
H.visitCollection(FIRST_COLLECTION_ID);
cy.get("table").within(() => {
cy.findByText("Second collection")
@@ -234,7 +213,7 @@ describe("scenarios > collection defaults", () => {
description: "[link](https://metabase.com)",
});
visitCollection(FIRST_COLLECTION_ID);
H.visitCollection(FIRST_COLLECTION_ID);
cy.log("Description visible in collection caption");
cy.findByTestId("collection-caption")
@@ -249,7 +228,7 @@ describe("scenarios > collection defaults", () => {
cy.log("Let's edit the description");
toggleSidesheet();
sidesheet().within(() => {
H.sidesheet().within(() => {
cy.findByTestId("editable-text").click().type("edited ");
cy.realPress("Tab");
cy.findByLabelText("Close").click();
@@ -264,7 +243,7 @@ describe("scenarios > collection defaults", () => {
cy.log("The edited description is visible in the sidesheet");
toggleSidesheet();
sidesheet().within(() => {
H.sidesheet().within(() => {
cy.findByTestId("editable-text").should("have.text", "edited link");
});
});
@@ -323,7 +302,7 @@ describe("scenarios > collection defaults", () => {
describe("Collection related issues reproductions", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -366,7 +345,7 @@ describe("scenarios > collection defaults", () => {
},
});
cy.signIn("none");
createQuestion(
H.createQuestion(
{
name: "Foo Question",
query: {
@@ -385,11 +364,11 @@ describe("scenarios > collection defaults", () => {
});
cy.findByTestId("qb-header").icon("ellipsis").click();
popover().findByText("Move").click();
entityPickerModalItem(1, "Collection B").should("exist");
entityPickerModalItem(2, "Collection E").should("exist");
H.popover().findByText("Move").click();
H.entityPickerModalItem(1, "Collection B").should("exist");
H.entityPickerModalItem(2, "Collection E").should("exist");
entityPickerModal().should(
H.entityPickerModal().should(
"not.contain.text",
"You don't have permissions to do that.",
);
@@ -410,14 +389,14 @@ describe("scenarios > collection defaults", () => {
it("should be able to drag an item to the root collection (metabase#16498)", () => {
moveItemToCollection("Orders", "First collection");
visitCollection(FIRST_COLLECTION_ID);
H.visitCollection(FIRST_COLLECTION_ID);
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Orders").as("dragSubject");
navigationSidebar().findByText("Our analytics").as("dropTarget");
H.navigationSidebar().findByText("Our analytics").as("dropTarget");
dragAndDrop("dragSubject", "dropTarget");
H.dragAndDrop("dragSubject", "dropTarget");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Moved question");
@@ -470,7 +449,7 @@ describe("scenarios > collection defaults", () => {
it("should see a child collection in a sidebar even with revoked access to its parents (metabase#14114, metabase#16555, metabase#20716)", () => {
cy.visit("/");
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
cy.findByText("Our analytics").should("not.exist");
cy.findByText("Parent").should("not.exist");
cy.findByText("Child");
@@ -484,15 +463,15 @@ describe("scenarios > collection defaults", () => {
});
it("should be able to choose a child collection when saving a question (metabase#14052)", () => {
openOrdersTable();
H.openOrdersTable();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Save").click();
// Click to choose which collection should this question be saved to
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(revokedUsersPersonalCollectionName).click();
pickEntity({ path: [revokedUsersPersonalCollectionName] });
pickEntity({ path: ["Collections", "Child"] });
entityPickerModal().button("Select").should("be.enabled");
H.pickEntity({ path: [revokedUsersPersonalCollectionName] });
H.pickEntity({ path: ["Collections", "Child"] });
H.entityPickerModal().button("Select").should("be.enabled");
cy.log("Reported failing from v0.34.3");
cy.findByTestId("entity-picker-modal")
.findByText("Parent")
@@ -513,11 +492,11 @@ describe("scenarios > collection defaults", () => {
openEllipsisMenuFor("Orders");
popover().within(() => {
H.popover().within(() => {
cy.findByText("Move").click();
});
entityPickerModal().within(() => {
H.entityPickerModal().within(() => {
cy.findByRole("tab", { name: /Collections/ }).click();
cy.findByText("Bobby Tables's Personal Collection").click();
cy.findByText(COLLECTION).click();
@@ -538,14 +517,14 @@ describe("scenarios > collection defaults", () => {
"when nested child collection is moved to the root collection (metabase#14482)",
);
visitCollection(SECOND_COLLECTION_ID);
H.visitCollection(SECOND_COLLECTION_ID);
openCollectionMenu();
popover().findByText("Move").click();
H.openCollectionMenu();
H.popover().findByText("Move").click();
// we need to do this manually because we need to await the correct number of api requests to keep this from flaking
entityPickerModal().within(() => {
H.entityPickerModal().within(() => {
cy.findByTestId("loading-indicator").should("not.exist");
cy.findByRole("tab", { name: /Collections/ }).click();
cy.wait([
@@ -564,10 +543,10 @@ describe("scenarios > collection defaults", () => {
cy.button("Move").click();
});
entityPickerModal().should("not.exist");
H.entityPickerModal().should("not.exist");
cy.wait("@getTree");
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
ensureCollectionHasNoChildren("First collection");
// Should be expanded automatically
@@ -580,10 +559,10 @@ describe("scenarios > collection defaults", () => {
"should show moved collection inside a folder tree structure (metabase#14280)",
);
moveOpenedCollectionTo(NEW_COLLECTION);
H.moveOpenedCollectionTo(NEW_COLLECTION);
cy.wait("@getTree");
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
ensureCollectionHasNoChildren("Second collection");
ensureCollectionIsExpanded(NEW_COLLECTION, {
@@ -598,12 +577,12 @@ describe("scenarios > collection defaults", () => {
statusCode: 500,
body: { message: "Ryan said no" },
});
openCollectionMenu();
popover().findByText("Move").click();
H.openCollectionMenu();
H.popover().findByText("Move").click();
entityPickerModal().within(() => {
entityPickerModalTab("Collections").click();
entityPickerModalItem(0, "Our analytics").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.entityPickerModalItem(0, "Our analytics").click();
cy.button("Move").click();
cy.log("Entity picker should show an error message");
cy.findByText("Ryan said no").should("exist");
@@ -616,9 +595,9 @@ describe("scenarios > collection defaults", () => {
cy.visit("/collection/root");
// Pin one item
openUnpinnedItemMenu("Orders, Count");
popover().findByText("Pin this").click();
getPinnedSection().within(() => {
H.openUnpinnedItemMenu("Orders, Count");
H.popover().findByText("Pin this").click();
H.getPinnedSection().within(() => {
cy.findByText("18,760");
});
@@ -684,7 +663,7 @@ describe("scenarios > collection defaults", () => {
cy.findByTestId("toast-card").button("Move").click();
entityPickerModal().within(() => {
H.entityPickerModal().within(() => {
cy.findByText("First collection").click();
cy.button("Move").click();
});
@@ -694,13 +673,13 @@ describe("scenarios > collection defaults", () => {
cy.findByTestId("toast-card").should("not.exist");
// Check that items were actually moved
navigationSidebar().findByText("First collection").click();
H.navigationSidebar().findByText("First collection").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Orders");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Undo").click();
navigationSidebar().findByText("Our analytics").click();
H.navigationSidebar().findByText("Our analytics").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Orders").should("be.visible");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -716,9 +695,9 @@ describe("scenarios > collection defaults", () => {
cy.log("from the collection header");
cy.findByTestId("collection-menu").icon("ellipsis").click();
popover().findByText("Move").click();
H.popover().findByText("Move").click();
entityPickerModal().within(() => {
H.entityPickerModal().within(() => {
cy.findByRole("tab", { name: /Collections/ }).click();
cy.log("parent collection should be selected");
findPickerItem("First collection").should(
@@ -738,9 +717,9 @@ describe("scenarios > collection defaults", () => {
openEllipsisMenuFor("Third collection");
});
popover().findByText("Move").click();
H.popover().findByText("Move").click();
entityPickerModal().within(() => {
H.entityPickerModal().within(() => {
cy.log("parent collection should be selected");
cy.findByRole("tab", { name: /Collections/ }).click();
findPickerItem("Second collection").should(
@@ -765,7 +744,7 @@ describe("scenarios > collection defaults", () => {
cy.findByTestId("toast-card").button("Move").click();
entityPickerModal().within(() => {
H.entityPickerModal().within(() => {
cy.log("should disable all moving collections");
findPickerItem("First collection").should("have.attr", "disabled");
findPickerItem("Another collection").should(
@@ -781,22 +760,22 @@ describe("scenarios > collection defaults", () => {
});
it("moving collections should disable moving into any of the moving collections in recents or search (metabase#45248)", () => {
createCollection({ name: "Outer collection 1" }).then(
H.createCollection({ name: "Outer collection 1" }).then(
({ body: { id: parentCollectionId } }) => {
cy.wrap(parentCollectionId).as("outerCollectionId");
createCollection({
H.createCollection({
name: "Inner collection 1",
parent_id: parentCollectionId,
}).then(({ body: { id: innerCollectionId } }) => {
cy.wrap(innerCollectionId).as("innerCollectionId");
});
createCollection({
H.createCollection({
name: "Inner collection 2",
parent_id: parentCollectionId,
});
},
);
createCollection({ name: "Outer collection 2" });
H.createCollection({ name: "Outer collection 2" });
// modify the inner collection so that it shows up in recents
cy.get("@innerCollectionId").then(innerCollectionId => {
@@ -809,13 +788,13 @@ describe("scenarios > collection defaults", () => {
cy.log("single move");
cy.findByTestId("collection-table").within(() => {
openCollectionItemMenu("Outer collection 1");
H.openCollectionItemMenu("Outer collection 1");
});
popover().findByText("Move").click();
H.popover().findByText("Move").click();
entityPickerModal().within(() => {
entityPickerModalTab("Recents").should(
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Recents").should(
"have.attr",
"data-active",
"true",
@@ -835,8 +814,8 @@ describe("scenarios > collection defaults", () => {
cy.findByTestId("toast-card").button("Move").click();
entityPickerModal().within(() => {
entityPickerModalTab("Recents").should(
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Recents").should(
"have.attr",
"data-active",
"true",
@@ -855,16 +834,16 @@ describe("scenarios > collection defaults", () => {
cy.visit("/");
// There is already a collection named "First collection" in the default snapshot
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
cy.findByText("First collection");
});
});
it("should create new collections within the current collection", () => {
visitCollection(THIRD_COLLECTION_ID);
H.visitCollection(THIRD_COLLECTION_ID);
cy.findByTestId("app-bar").findByText("New").click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("Collection").click();
});
@@ -879,7 +858,7 @@ describe("scenarios > collection defaults", () => {
describe("x-rays", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsNormalUser();
cy.intercept("GET", "/api/automagic-dashboards/model/*").as("dashboard");
});
@@ -889,7 +868,7 @@ describe("scenarios > collection defaults", () => {
cy.visit("/collection/root");
openEllipsisMenuFor("Orders");
popover().findByText("X-ray this").click();
H.popover().findByText("X-ray this").click();
cy.wait("@dashboard");
});
});
@@ -933,7 +912,7 @@ describe("scenarios > collection items listing", () => {
"getCollectionItems",
);
restore();
H.restore();
cy.signInAsAdmin();
});

View File

@@ -1,31 +1,15 @@
import { H } from "e2e/support";
import {
ORDERS_DASHBOARD_ID,
ORDERS_QUESTION_ID,
} from "e2e/support/cypress_sample_instance_data";
import {
describeEE,
modal,
newButton,
onlyOnEE,
onlyOnOSS,
openDashboardInfoSidebar,
openQuestionInfoSidesheet,
popover,
restore,
setTokenFeatures,
sidebar,
tableHeaderClick,
visitDashboard,
visitModel,
visitQuestion,
} from "e2e/support/helpers";
const ANALYTICS_COLLECTION_NAME = "Usage analytics";
const CUSTOM_REPORTS_COLLECTION_NAME = "Custom reports";
const PEOPLE_MODEL_NAME = "People";
const METRICS_DASHBOARD_NAME = "Metabase metrics";
describeEE("scenarios > Metabase Analytics Collection (AuditV2) ", () => {
H.describeEE("scenarios > Metabase Analytics Collection (AuditV2) ", () => {
describe("admin", () => {
beforeEach(() => {
cy.intercept("GET", "/api/field/*/values").as("fieldValues");
@@ -33,9 +17,9 @@ describeEE("scenarios > Metabase Analytics Collection (AuditV2) ", () => {
cy.intercept("POST", "api/card").as("saveCard");
cy.intercept("POST", "api/dashboard/*/copy").as("copyDashboard");
restore();
H.restore();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
});
it("allows admins to see the instance analytics collection content", () => {
@@ -60,15 +44,15 @@ describeEE("scenarios > Metabase Analytics Collection (AuditV2) ", () => {
() => {
cy.log("saving edited question");
getItemId(ANALYTICS_COLLECTION_NAME, PEOPLE_MODEL_NAME).then(id => {
visitModel(id);
H.visitModel(id);
});
tableHeaderClick("Last Name");
H.tableHeaderClick("Last Name");
popover().findByText("Filter by this column").click();
H.popover().findByText("Filter by this column").click();
cy.wait("@fieldValues");
popover().findByText("Tableton").click();
popover().button("Add filter").click();
H.popover().findByText("Tableton").click();
H.popover().button("Add filter").click();
cy.wait("@datasetQuery");
@@ -92,14 +76,14 @@ describeEE("scenarios > Metabase Analytics Collection (AuditV2) ", () => {
cy.log("saving copied question");
getItemId(ANALYTICS_COLLECTION_NAME, PEOPLE_MODEL_NAME).then(id => {
visitModel(id);
H.visitModel(id);
});
cy.findByTestId("qb-header").icon("ellipsis").click();
popover().findByText("Duplicate").click();
H.popover().findByText("Duplicate").click();
modal().within(() => {
H.modal().within(() => {
cy.findByTextEnsureVisible("Custom reports");
cy.button("Duplicate").click();
});
@@ -108,20 +92,20 @@ describeEE("scenarios > Metabase Analytics Collection (AuditV2) ", () => {
expect(response.statusCode).to.eq(200);
});
modal()
H.modal()
.button(/Duplicate/i)
.should("not.exist");
modal().button("Not now").click();
H.modal().button("Not now").click();
cy.log("saving copied dashboard");
getItemId(ANALYTICS_COLLECTION_NAME, "Person overview").then(id => {
visitDashboard(id);
H.visitDashboard(id);
});
cy.findByTestId("dashboard-header").findByText("Make a copy").click();
modal().within(() => {
H.modal().within(() => {
cy.findByTextEnsureVisible("Custom reports");
cy.button("Duplicate").click();
});
@@ -155,7 +139,7 @@ describeEE("scenarios > Metabase Analytics Collection (AuditV2) ", () => {
}
});
popover().within(() => {
H.popover().within(() => {
cy.findByText("Bookmark").should("be.visible");
cy.findByText("Move to trash").should("not.exist");
cy.findByText("Move").should("not.exist");
@@ -179,7 +163,7 @@ describeEE("scenarios > Metabase Analytics Collection (AuditV2) ", () => {
}
});
popover().within(() => {
H.popover().within(() => {
cy.findByText("Bookmark").should("be.visible");
cy.findByText("Move to trash").should("not.exist");
cy.findByText("Move").should("not.exist");
@@ -189,7 +173,7 @@ describeEE("scenarios > Metabase Analytics Collection (AuditV2) ", () => {
it("should not allow editing analytics content (metabase#36228)", () => {
// dashboard
getItemId(ANALYTICS_COLLECTION_NAME, METRICS_DASHBOARD_NAME).then(id => {
visitDashboard(id);
H.visitDashboard(id);
});
cy.findByTestId("dashboard-header").within(() => {
@@ -199,12 +183,12 @@ describeEE("scenarios > Metabase Analytics Collection (AuditV2) ", () => {
// model
getItemId(ANALYTICS_COLLECTION_NAME, PEOPLE_MODEL_NAME).then(id => {
visitModel(id);
H.visitModel(id);
});
cy.findByTestId("qb-header").icon("ellipsis").click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("Duplicate").should("be.visible");
cy.findByText("Edit query definition").should("not.exist");
});
@@ -212,10 +196,10 @@ describeEE("scenarios > Metabase Analytics Collection (AuditV2) ", () => {
it("should not leak instance analytics database into SQL query builder (metabase#44856)", () => {
getItemId(ANALYTICS_COLLECTION_NAME, PEOPLE_MODEL_NAME).then(id => {
visitModel(id);
H.visitModel(id);
});
newButton("SQL query").click();
H.newButton("SQL query").click();
// sample DB should be the only one
cy.findByTestId("gui-builder-data")
@@ -225,21 +209,21 @@ describeEE("scenarios > Metabase Analytics Collection (AuditV2) ", () => {
it("should not leak instance analytics database into permissions editor (metabase#44856)", () => {
getItemId(ANALYTICS_COLLECTION_NAME, PEOPLE_MODEL_NAME).then(id => {
visitModel(id);
H.visitModel(id);
});
// it's important that we do this manually, as this will only reproduce if theres no page load
cy.findByTestId("app-bar").icon("gear").click();
popover().findByText("Admin settings").click();
H.popover().findByText("Admin settings").click();
cy.findByLabelText("Navigation bar").findByText("Permissions").click();
sidebar().findByText("Administrators").click();
H.sidebar().findByText("Administrators").click();
cy.findByTestId("permission-table")
.findByText(/internal metabase database/i)
.should("not.exist");
sidebar().findByText("Databases").click();
H.sidebar().findByText("Databases").click();
sidebar()
H.sidebar()
.findByText(/internal metabase database/i)
.should("not.exist");
});
@@ -252,9 +236,9 @@ describeEE("scenarios > Metabase Analytics Collection (AuditV2) ", () => {
cy.intercept("POST", "api/card").as("saveCard");
cy.intercept("POST", "api/dashboard/*/copy").as("copyDashboard");
restore();
H.restore();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
});
it("should not allow editing analytics content (metabase#36228)", () => {
@@ -298,20 +282,20 @@ describeEE("scenarios > Metabase Analytics Collection (AuditV2) ", () => {
});
describe("question and dashboard links", () => {
describeEE("ee", () => {
H.describeEE("ee", () => {
beforeEach(() => {
onlyOnEE();
restore();
H.onlyOnEE();
H.restore();
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
});
it("should show an analytics link for questions", () => {
visitQuestion(ORDERS_QUESTION_ID);
H.visitQuestion(ORDERS_QUESTION_ID);
cy.intercept("GET", "/api/collection/**").as("collection");
openQuestionInfoSidesheet()
H.openQuestionInfoSidesheet()
.findByRole("link", { name: /Insights/ })
.click();
@@ -337,10 +321,10 @@ describe("question and dashboard links", () => {
});
it("should show an analytics link for dashboards", () => {
visitDashboard(ORDERS_DASHBOARD_ID);
H.visitDashboard(ORDERS_DASHBOARD_ID);
cy.intercept("GET", "/api/collection/**").as("collection");
openDashboardInfoSidebar()
H.openDashboardInfoSidebar()
.findByRole("link", { name: /Insights/ })
.click();
@@ -367,14 +351,14 @@ describe("question and dashboard links", () => {
it("should not show option for users with no access to Metabase Analytics", () => {
cy.signInAsNormalUser();
visitQuestion(ORDERS_QUESTION_ID);
H.visitQuestion(ORDERS_QUESTION_ID);
openQuestionInfoSidesheet()
H.openQuestionInfoSidesheet()
.findByRole("link", { name: /Insights/i })
.should("not.exist");
visitDashboard(ORDERS_DASHBOARD_ID);
openDashboardInfoSidebar()
H.visitDashboard(ORDERS_DASHBOARD_ID);
H.openDashboardInfoSidebar()
.findByRole("link", { name: /Insights/i })
.should("not.exist");
});
@@ -382,21 +366,21 @@ describe("question and dashboard links", () => {
describe("oss", { tags: "@OSS" }, () => {
beforeEach(() => {
onlyOnOSS();
restore();
H.onlyOnOSS();
H.restore();
cy.signInAsAdmin();
});
it("should never appear in OSS", () => {
visitQuestion(ORDERS_QUESTION_ID);
H.visitQuestion(ORDERS_QUESTION_ID);
openQuestionInfoSidesheet()
H.openQuestionInfoSidesheet()
.findByRole("link", { name: /Insights/i })
.should("not.exist");
visitDashboard(ORDERS_DASHBOARD_ID);
H.visitDashboard(ORDERS_DASHBOARD_ID);
openDashboardInfoSidebar()
H.openDashboardInfoSidebar()
.findByRole("link", { name: /Insights/i })
.should("not.exist");
});

View File

@@ -1,20 +1,8 @@
import { onlyOn } from "@cypress/skip-test";
import { H } from "e2e/support";
import { USERS } from "e2e/support/cypress_data";
import { FIRST_COLLECTION_ID } from "e2e/support/cypress_sample_instance_data.js";
import {
appBar,
entityPickerModal,
modal,
navigationSidebar,
openCollectionItemMenu,
openCollectionMenu,
openNativeEditor,
popover,
restore,
setTokenFeatures,
sidebar,
} from "e2e/support/helpers";
import { displaySidebarChildOf } from "./helpers/e2e-collections-sidebar.js";
@@ -27,7 +15,7 @@ const PERMISSIONS = {
describe("collection permissions", () => {
beforeEach(() => {
cy.intercept("GET", "/api/search*").as("search");
restore();
H.restore();
});
describe("item management", () => {
@@ -43,11 +31,11 @@ describe("collection permissions", () => {
describe("create dashboard", () => {
it("should offer to save dashboard to a currently opened collection", () => {
cy.visit("/collection/root");
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
displaySidebarChildOf("First collection");
cy.findByText("Second collection").click();
});
appBar().within(() => {
H.appBar().within(() => {
cy.icon("add").click();
});
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -62,10 +50,10 @@ describe("collection permissions", () => {
cy.visit("/collection/root");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Orders in a dashboard").click();
appBar().within(() => {
H.appBar().within(() => {
cy.icon("add").click();
});
popover().findByText("Dashboard").click();
H.popover().findByText("Dashboard").click();
cy.findByLabelText(/Which collection/).findByText(
"Our analytics",
);
@@ -141,15 +129,15 @@ describe("collection permissions", () => {
describe("archive page", () => {
it("should show archived items (metabase#15080, metabase#16617)", () => {
cy.visit("collection/root");
openCollectionItemMenu("Orders");
popover().within(() => {
H.openCollectionItemMenu("Orders");
H.popover().within(() => {
cy.findByText("Move to trash").click();
});
cy.findByTestId("toast-undo").within(() => {
cy.findByText("Trashed question");
cy.icon("close").click();
});
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
cy.findByText("Trash").click();
});
cy.location("pathname").should("eq", "/trash");
@@ -182,18 +170,18 @@ describe("collection permissions", () => {
cy.visit(`/collection/${THIRD_COLLECTION_ID}`);
});
openCollectionMenu();
popover().within(() =>
H.openCollectionMenu();
H.popover().within(() =>
// eslint-disable-next-line no-unscoped-text-selectors -- linter erroring for no reason
cy.findByText("Move to trash").click(),
);
modal().findByText("Move to trash").click();
H.modal().findByText("Move to trash").click();
cy.wait("@editCollection");
cy.findByTestId("archive-banner").should("exist");
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
cy.findByText("First collection");
cy.findByText("Second collection");
cy.findByText("Third collection").should("not.exist");
@@ -214,7 +202,7 @@ describe("collection permissions", () => {
cy.findByTestId("archive-banner").should("not.exist");
// But unarchived collection is now visible in the sidebar
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
cy.findByText("Third collection");
});
});
@@ -271,11 +259,11 @@ describe("collection permissions", () => {
collection => collection.slug === "third_collection",
);
cy.visit(`/collection/${THIRD_COLLECTION_ID}`);
openCollectionMenu();
popover().within(() =>
H.openCollectionMenu();
H.popover().within(() =>
cy.findByText("Move to trash").click(),
);
modal().findByText("Cancel").click();
H.modal().findByText("Cancel").click();
cy.location("pathname").should(
"eq",
`/collection/${THIRD_COLLECTION_ID}-third-collection`,
@@ -289,8 +277,8 @@ describe("collection permissions", () => {
function archiveUnarchive(item, expectedEntityName) {
cy.visit("/collection/root");
openCollectionItemMenu(item);
popover().within(() => {
H.openCollectionItemMenu(item);
H.popover().within(() => {
cy.findByText("Move to trash").click();
});
cy.findByText(item).should("not.exist");
@@ -321,8 +309,8 @@ describe("collection permissions", () => {
it("should be offered to duplicate dashboard in collections they have `read` access to", () => {
const { first_name, last_name } = USERS[user];
cy.visit("/collection/root");
openCollectionItemMenu("Orders in a dashboard");
popover().findByText("Duplicate").click();
H.openCollectionItemMenu("Orders in a dashboard");
H.popover().findByText("Duplicate").click();
cy.findByTestId("collection-picker-button").should(
"have.text",
`${first_name} ${last_name}'s Personal Collection`,
@@ -358,7 +346,7 @@ describe("collection permissions", () => {
cy.findByText("Dashboard").click();
// Coming from the root collection, the initial offered collection will be "Our analytics" (read-only access)
modal().within(() => {
H.modal().within(() => {
cy.findByText(
`${first_name} ${last_name}'s Personal Collection`,
).click();
@@ -388,7 +376,7 @@ describe("collection permissions", () => {
it("should offer to save items to 'Our analytics' if user has a 'curate' access to it", () => {
cy.signIn("normal");
openNativeEditor().type("select * from people");
H.openNativeEditor().type("select * from people");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Save").click();
@@ -397,7 +385,7 @@ describe("collection permissions", () => {
it("should load the collection permissions admin pages", () => {
cy.signInAsAdmin();
setTokenFeatures("all");
H.setTokenFeatures("all");
cy.intercept("GET", "/api/collection/graph").as("permissionsGraph");
cy.intercept("GET", "/api/permissions/group").as("permissionsGroups");
@@ -419,7 +407,7 @@ describe("collection permissions", () => {
);
cy.findByTestId("permission-table");
sidebar().findByText("Usage analytics").click();
H.sidebar().findByText("Usage analytics").click();
cy.findByTestId("permissions-editor").findByText(
"Permissions for Usage analytics",
);
@@ -432,12 +420,12 @@ function clickButton(name) {
}
function pinItem(item) {
openCollectionItemMenu(item);
popover().icon("pin").click();
H.openCollectionItemMenu(item);
H.popover().icon("pin").click();
}
function exposeChildrenFor(collectionName) {
navigationSidebar()
H.navigationSidebar()
.findByText(collectionName)
.parentsUntil("[data-testid=sidebar-collection-link-root]")
.find(".Icon-chevronright")
@@ -447,9 +435,9 @@ function exposeChildrenFor(collectionName) {
function move(item) {
cy.visit("/collection/root");
openCollectionItemMenu(item);
popover().findByText("Move").click();
entityPickerModal().within(() => {
H.openCollectionItemMenu(item);
H.popover().findByText("Move").click();
H.entityPickerModal().within(() => {
cy.findByText(`Move "${item}"?`);
// Let's move it into a nested collection
cy.findByText("First collection").click();
@@ -472,9 +460,9 @@ function move(item) {
function duplicate(item) {
cy.visit("/collection/root");
openCollectionItemMenu(item);
H.openCollectionItemMenu(item);
cy.findByText("Duplicate").click();
modal()
H.modal()
.as("modal")
.within(() => {
clickButton("Duplicate");

View File

@@ -1,22 +1,14 @@
import { H } from "e2e/support";
import { USERS } from "e2e/support/cypress_data";
import {
ADMIN_PERSONAL_COLLECTION_ID,
NORMAL_USER_ID,
NO_DATA_PERSONAL_COLLECTION_ID,
} from "e2e/support/cypress_sample_instance_data";
import {
getCollectionActions,
modal,
navigationSidebar,
openCollectionMenu,
openNewCollectionItemFlowFor,
popover,
restore,
} from "e2e/support/helpers";
describe("personal collections", () => {
beforeEach(() => {
restore();
H.restore();
});
describe("admin", () => {
@@ -64,10 +56,10 @@ describe("personal collections", () => {
cy.findAllByRole("tree")
.contains("Your personal collection")
.should("be.visible");
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
cy.icon("ellipsis").click();
});
popover().findByText("Other users' personal collections").click();
H.popover().findByText("Other users' personal collections").click();
cy.location("pathname").should("eq", "/collection/users");
cy.findByTestId("browsercrumbs").findByText(/All personal collections/i);
Object.values(USERS).forEach(user => {
@@ -88,7 +80,7 @@ describe("personal collections", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Your personal collection").click();
getCollectionActions().within(() => {
H.getCollectionActions().within(() => {
cy.icon("ellipsis").should("not.exist");
});
@@ -103,12 +95,12 @@ describe("personal collections", () => {
// });
// Go to the newly created sub-collection "Foo"
navigationSidebar().findByText("Foo").click();
H.navigationSidebar().findByText("Foo").click();
// It should be possible to edit sub-collections' details, but not its permissions
cy.findByDisplayValue("Foo").should("be.enabled");
openCollectionMenu();
popover().within(() => {
H.openCollectionMenu();
H.popover().within(() => {
cy.findByText("Edit permissions").should("not.exist");
});
@@ -122,7 +114,7 @@ describe("personal collections", () => {
// Go to random user's personal collection
cy.visit(`/collection/${NO_DATA_PERSONAL_COLLECTION_ID}`);
getCollectionActions().within(() => {
H.getCollectionActions().within(() => {
cy.icon("ellipsis").should("not.exist");
});
});
@@ -151,7 +143,7 @@ describe("personal collections", () => {
// Create initial collection inside the personal collection and navigate to it
addNewCollection("Foo");
navigationSidebar().as("sidebar").findByText("Foo").click();
H.navigationSidebar().as("sidebar").findByText("Foo").click();
});
it("should be able to edit collection(s) inside personal collection", () => {
@@ -168,10 +160,10 @@ describe("personal collections", () => {
"should be able to archive collection(s) inside personal collection (metabase#15343)",
);
openCollectionMenu();
H.openCollectionMenu();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
popover().within(() => cy.findByText("Move to trash").click());
modal().findByRole("button", { name: "Move to trash" }).click();
H.popover().within(() => cy.findByText("Move to trash").click());
H.modal().findByRole("button", { name: "Move to trash" }).click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Trashed collection");
cy.get("@sidebar").findByText("Foo").should("not.exist");
@@ -182,7 +174,7 @@ describe("personal collections", () => {
});
function addNewCollection(name) {
openNewCollectionItemFlowFor("collection");
H.openNewCollectionItemFlowFor("collection");
cy.findByPlaceholderText("My new fantastic collection").type(name, {
delay: 0,
});

View File

@@ -1,20 +1,10 @@
import { onlyOn } from "@cypress/skip-test";
import { H } from "e2e/support";
import {
ORDERS_DASHBOARD_ID,
ORDERS_QUESTION_ID,
} from "e2e/support/cypress_sample_instance_data";
import {
editDashboard,
openQuestionsSidebar,
questionInfoButton,
restore,
saveDashboard,
sidebar,
sidesheet,
visitDashboard,
visitQuestion,
} from "e2e/support/helpers";
const PERMISSIONS = {
curate: ["admin", "normal", "nodata"],
@@ -26,7 +16,7 @@ describe("revision history", () => {
beforeEach(() => {
cy.intercept("POST", "/api/revision/revert").as("revert");
restore();
H.restore();
});
describe("reproductions", () => {
@@ -36,14 +26,14 @@ describe("revision history", () => {
it("shouldn't render revision history steps when there was no diff (metabase#1926)", () => {
cy.createDashboard().then(({ body }) => {
visitDashboard(body.id);
editDashboard();
H.visitDashboard(body.id);
H.editDashboard();
});
// Save the dashboard without any changes made to it (TODO: we should probably disable "Save" button in the first place)
saveDashboard({ awaitRequest: false });
editDashboard();
saveDashboard({ awaitRequest: false });
H.saveDashboard({ awaitRequest: false });
H.editDashboard();
H.saveDashboard({ awaitRequest: false });
openRevisionHistory();
@@ -78,14 +68,14 @@ describe("revision history", () => {
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
cy.createDashboard().then(({ body }) => {
visitDashboard(body.id);
editDashboard();
H.visitDashboard(body.id);
H.editDashboard();
});
openQuestionsSidebar();
sidebar().findByText("Orders, Count").click();
H.openQuestionsSidebar();
H.sidebar().findByText("Orders, Count").click();
cy.wait("@cardQuery");
saveDashboard();
H.saveDashboard();
// this is dirty, but seems like the only reliable way
// to wait until SET_DASHBOARD_EDITING is dispatched,
@@ -94,7 +84,7 @@ describe("revision history", () => {
cy.wait(100);
openRevisionHistory();
sidesheet().within(() => {
H.sidesheet().within(() => {
cy.findByRole("tab", { name: "History" }).click();
cy.findByText(/added a card/)
.siblings("button")
@@ -105,7 +95,7 @@ describe("revision history", () => {
// skipped because it's super flaky in CI
it.skip("should be able to revert a dashboard (metabase#15237)", () => {
visitDashboard(ORDERS_DASHBOARD_ID);
H.visitDashboard(ORDERS_DASHBOARD_ID);
openRevisionHistory();
clickRevert(/created this/);
@@ -135,7 +125,7 @@ describe("revision history", () => {
it("should be able to access the question's revision history via the revision history button in the header of the query builder", () => {
cy.skipOn(user === "nodata");
visitQuestion(ORDERS_QUESTION_ID);
H.visitQuestion(ORDERS_QUESTION_ID);
cy.findByTestId("revision-history-button").click();
cy.findByRole("tab", { name: "History" }).click();
@@ -154,9 +144,9 @@ describe("revision history", () => {
it("should be able to revert the question via the action button found in the saved question timeline", () => {
cy.skipOn(user === "nodata");
visitQuestion(ORDERS_QUESTION_ID);
H.visitQuestion(ORDERS_QUESTION_ID);
questionInfoButton().click();
H.questionInfoButton().click();
cy.findByRole("tab", { name: "History" }).click();
// Last revert is the original state
@@ -178,13 +168,13 @@ describe("revision history", () => {
it("should not see question nor dashboard revert buttons (metabase#13229)", () => {
cy.signIn(user);
visitDashboard(ORDERS_DASHBOARD_ID);
H.visitDashboard(ORDERS_DASHBOARD_ID);
openRevisionHistory();
cy.findAllByRole("button", { name: "Revert" }).should(
"not.exist",
);
visitQuestion(ORDERS_QUESTION_ID);
H.visitQuestion(ORDERS_QUESTION_ID);
cy.findByRole("button", { name: /Edited .*/ }).click();
cy.findAllByRole("button", { name: "Revert" }).should(
@@ -210,7 +200,7 @@ function openRevisionHistory() {
cy.findByRole("tab", { name: "History" }).click();
cy.wait("@revisionHistory");
sidesheet().within(() => {
H.sidesheet().within(() => {
cy.findByText("History");
cy.findByTestId("dashboard-history-list").should("be.visible");
});

View File

@@ -1,40 +1,16 @@
import { P, isMatching } from "ts-pattern";
import { H } from "e2e/support";
import {
FIRST_COLLECTION_ID,
ORDERS_COUNT_QUESTION_ID,
ORDERS_QUESTION_ID,
READ_ONLY_PERSONAL_COLLECTION_ID,
} from "e2e/support/cypress_sample_instance_data";
import {
createNativeQuestion as _createNativeQuestion,
createQuestion as _createQuestion,
archiveQuestion,
describeWithSnowplow,
entityPickerModal,
entityPickerModalTab,
expectGoodSnowplowEvent,
expectNoBadSnowplowEvents,
main,
modal,
modifyPermission,
navigationSidebar,
openNavigationSidebar,
popover,
resetSnowplow,
restore,
selectSidebarItem,
sharingMenuButton,
sidebar,
undo,
visitCollection,
visitDashboard,
visitQuestion,
} from "e2e/support/helpers";
describe("scenarios > collections > trash", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -51,7 +27,7 @@ describe("scenarios > collections > trash", () => {
cy.visit("/");
cy.log("should show trash at bottom of the side navbar");
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
cy.findAllByTestId("sidebar-collection-link-root")
.last()
.as("sidebar-trash-link")
@@ -74,7 +50,7 @@ describe("scenarios > collections > trash", () => {
"trashed items in collection should not have option to move to trash",
);
toggleEllipsisMenuFor("Collection A");
popover().within(() => {
H.popover().within(() => {
cy.findByText("Move to trash").should("not.exist");
cy.findByText("Restore").should("exist");
cy.findByText("Delete permanently").should("not.exist");
@@ -100,9 +76,9 @@ describe("scenarios > collections > trash", () => {
cy.findByText("New").click();
});
popover().findByText("Question").click();
entityPickerModal().within(() => {
entityPickerModalTab("Models").click();
H.popover().findByText("Question").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Models").click();
cy.findByText("Our analytics").should("exist");
cy.findByText("Trash").should("not.exist");
cy.button("Close").click();
@@ -112,25 +88,25 @@ describe("scenarios > collections > trash", () => {
cy.findByLabelText("Navigation bar").within(() => {
cy.findByText("New").click();
});
popover().findByText("Dashboard").click();
modal().findByText("Our analytics").click();
entityPickerModal().within(() => {
H.popover().findByText("Dashboard").click();
H.modal().findByText("Our analytics").click();
H.entityPickerModal().within(() => {
cy.findByText("First collection").should("exist");
cy.findByText("Trash").should("not.exist");
});
cy.log("trash should not appear in collection permissions sidebar");
cy.visit("/admin/permissions/collections");
sidebar().findByText("Trash").should("not.exist");
H.sidebar().findByText("Trash").should("not.exist");
});
describeWithSnowplow("", () => {
H.describeWithSnowplow("", () => {
beforeEach(() => {
resetSnowplow();
H.resetSnowplow();
});
afterEach(() => {
expectNoBadSnowplowEvents();
H.expectNoBadSnowplowEvents();
});
it("should be able to trash & restore dashboards/collections/questions on entity page and from parent collection", () => {
@@ -153,8 +129,8 @@ describe("scenarios > collections > trash", () => {
cy.log("should be able to move to trash from collection view");
toggleEllipsisMenuFor(/Collection A/);
popover().findByText("Move to trash").click();
expectGoodSnowplowEvent(event =>
H.popover().findByText("Move to trash").click();
H.expectGoodSnowplowEvent(event =>
isMatching(
{
event: "moved-to-trash",
@@ -169,8 +145,8 @@ describe("scenarios > collections > trash", () => {
);
toggleEllipsisMenuFor("Dashboard A");
popover().findByText("Move to trash").click();
expectGoodSnowplowEvent(event =>
H.popover().findByText("Move to trash").click();
H.expectGoodSnowplowEvent(event =>
isMatching(
{
event: "moved-to-trash",
@@ -185,8 +161,8 @@ describe("scenarios > collections > trash", () => {
);
toggleEllipsisMenuFor("Question A");
popover().findByText("Move to trash").click();
expectGoodSnowplowEvent(event =>
H.popover().findByText("Move to trash").click();
H.expectGoodSnowplowEvent(event =>
isMatching(
{
event: "moved-to-trash",
@@ -203,18 +179,18 @@ describe("scenarios > collections > trash", () => {
cy.log(
"should be able to move to restore items from trash collection view",
);
navigationSidebar().findByText("Trash").click();
H.navigationSidebar().findByText("Trash").click();
toggleEllipsisMenuFor(/Collection A/);
popover().findByText("Restore").click();
H.popover().findByText("Restore").click();
ensureBookmarkVisible(/Collection A/);
toggleEllipsisMenuFor("Dashboard A");
popover().findByText("Restore").click();
H.popover().findByText("Restore").click();
ensureBookmarkVisible("Dashboard A");
toggleEllipsisMenuFor("Question A");
popover().findByText("Restore").click();
H.popover().findByText("Restore").click();
ensureBookmarkVisible("Question A");
cy.log("should be able to archive entities from their own views");
@@ -225,12 +201,12 @@ describe("scenarios > collections > trash", () => {
cy.findByText("Collection A").click();
});
cy.findByTestId("collection-menu").find(".Icon-ellipsis").click();
popover().findByText("Move to trash").click();
modal().within(() => {
H.popover().findByText("Move to trash").click();
H.modal().within(() => {
cy.findByText("Move this collection to trash?");
cy.findByText("Move to trash").click();
});
expectGoodSnowplowEvent(event =>
H.expectGoodSnowplowEvent(event =>
isMatching(
{
event: "moved-to-trash",
@@ -251,12 +227,12 @@ describe("scenarios > collections > trash", () => {
cy.findByText("Dashboard A").click();
});
cy.findByTestId("dashboard-header").icon("ellipsis").click();
popover().findByText("Move to trash").click();
modal().within(() => {
H.popover().findByText("Move to trash").click();
H.modal().within(() => {
cy.findByText("Move this dashboard to trash?");
cy.findByText("Move to trash").click();
});
expectGoodSnowplowEvent(event =>
H.expectGoodSnowplowEvent(event =>
isMatching(
{
event: "moved-to-trash",
@@ -281,12 +257,12 @@ describe("scenarios > collections > trash", () => {
cy.findByText("Question A").click();
});
cy.findByTestId("qb-header-action-panel").icon("ellipsis").click();
popover().findByText("Move to trash").click();
modal().within(() => {
H.popover().findByText("Move to trash").click();
H.modal().within(() => {
cy.findByText("Move this question to trash?");
cy.findByText("Move to trash").click();
});
expectGoodSnowplowEvent(event =>
H.expectGoodSnowplowEvent(event =>
isMatching(
{
event: "moved-to-trash",
@@ -322,11 +298,11 @@ describe("scenarios > collections > trash", () => {
cy.visit("/trash");
toggleEllipsisMenuFor("Collection A");
popover().findByText("Restore").should("exist");
H.popover().findByText("Restore").should("exist");
collectionTable().findByText("Collection A").click();
toggleEllipsisMenuFor("Collection B");
popover().findByText("Restore").should("not.exist");
H.popover().findByText("Restore").should("not.exist");
cy.log("only shows restore on entity page if in root trash collection");
cy.visit("/trash");
@@ -349,22 +325,22 @@ describe("scenarios > collections > trash", () => {
cy.log("can move from trash list");
cy.visit("/trash");
toggleEllipsisMenuFor("Collection A");
popover().findByText("Move").click();
modal().within(() => {
H.popover().findByText("Move").click();
H.modal().within(() => {
cy.findByText("First collection").click();
cy.findByText("Move").click();
});
toggleEllipsisMenuFor("Dashboard A");
popover().findByText("Move").click();
modal().within(() => {
H.popover().findByText("Move").click();
H.modal().within(() => {
cy.findByText("First collection").click();
cy.findByText("Move").click();
});
toggleEllipsisMenuFor("Question A");
popover().findByText("Move").click();
modal().within(() => {
H.popover().findByText("Move").click();
H.modal().within(() => {
cy.findByText("First collection").click();
cy.findByText("Move").click();
});
@@ -391,7 +367,7 @@ describe("scenarios > collections > trash", () => {
archiveBanner().within(() => {
cy.findByText("Move").click();
});
modal().within(() => {
H.modal().within(() => {
cy.findByText("First collection").click();
cy.findByText("Move").click();
});
@@ -404,7 +380,7 @@ describe("scenarios > collections > trash", () => {
archiveBanner().within(() => {
cy.findByText("Move").click();
});
modal().within(() => {
H.modal().within(() => {
cy.findByText("First collection").click();
cy.findByText("Move").click();
});
@@ -417,7 +393,7 @@ describe("scenarios > collections > trash", () => {
archiveBanner().within(() => {
cy.findByText("Move").click();
});
modal().within(() => {
H.modal().within(() => {
cy.findByText("First collection").click();
cy.findByText("Move").click();
});
@@ -455,7 +431,7 @@ describe("scenarios > collections > trash", () => {
cy.log("can delete from trash list");
toggleEllipsisMenuFor("Collection A");
// FUTURE: replace following two lines with commented out code when collections can be deleted
popover().findByText("Delete permanently").should("not.exist");
H.popover().findByText("Delete permanently").should("not.exist");
toggleEllipsisMenuFor("Collection A");
// popover().findByText("Delete permanently").click();
// modal().findByText("Delete Collection A permanently?").should("exist");
@@ -465,17 +441,17 @@ describe("scenarios > collections > trash", () => {
// });
toggleEllipsisMenuFor("Dashboard A");
popover().findByText("Delete permanently").click();
modal().findByText("Delete Dashboard A permanently?").should("exist");
modal().findByText("Delete permanently").click();
H.popover().findByText("Delete permanently").click();
H.modal().findByText("Delete Dashboard A permanently?").should("exist");
H.modal().findByText("Delete permanently").click();
collectionTable().within(() => {
cy.findByText("Dashboard A").should("not.exist");
});
toggleEllipsisMenuFor("Question A");
popover().findByText("Delete permanently").click();
modal().findByText("Delete Question A permanently?").should("exist");
modal().findByText("Delete permanently").click();
H.popover().findByText("Delete permanently").click();
H.modal().findByText("Delete Question A permanently?").should("exist");
H.modal().findByText("Delete permanently").click();
collectionTable().within(() => {
cy.findByText("Question A").should("not.exist");
});
@@ -498,8 +474,8 @@ describe("scenarios > collections > trash", () => {
cy.findByText("Dashboard B").click();
});
archiveBanner().findByText("Delete permanently").click();
modal().findByText("Delete Dashboard B permanently?").should("exist");
modal().findByText("Delete permanently").click();
H.modal().findByText("Delete Dashboard B permanently?").should("exist");
H.modal().findByText("Delete permanently").click();
collectionTable().within(() => {
cy.findByText("Dashboard B").should("not.exist");
});
@@ -508,8 +484,8 @@ describe("scenarios > collections > trash", () => {
cy.findByText("Question B").click();
});
archiveBanner().findByText("Delete permanently").click();
modal().findByText("Delete Question B permanently?").should("exist");
modal().findByText("Delete permanently").click();
H.modal().findByText("Delete Question B permanently?").should("exist");
H.modal().findByText("Delete permanently").click();
collectionTable().within(() => {
cy.findByText("Question B").should("not.exist");
});
@@ -559,7 +535,7 @@ describe("scenarios > collections > trash", () => {
cy.findByText("Move").should("not.be.disabled").click();
});
modal().within(() => {
H.modal().within(() => {
cy.findByText("First collection").click();
cy.findByText("Move").click();
});
@@ -570,7 +546,7 @@ describe("scenarios > collections > trash", () => {
cy.findByText("Question A").should("not.exist");
});
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
cy.findByText("First collection").click();
});
@@ -593,7 +569,7 @@ describe("scenarios > collections > trash", () => {
cy.findByText("Delete permanently").should("not.be.disabled").click();
});
modal().within(() => {
H.modal().within(() => {
cy.findByText("Delete 2 items permanently?");
cy.findByText("Delete permanently").click();
});
@@ -617,7 +593,7 @@ describe("scenarios > collections > trash", () => {
).as("question");
cy.get("@question").then(question => {
visitQuestion(question.id);
H.visitQuestion(question.id);
// should not have disabled actions in top navbar
cy.findAllByTestId("qb-header-action-panel").within(() => {
cy.findByText("Filter").should("not.exist");
@@ -625,7 +601,7 @@ describe("scenarios > collections > trash", () => {
cy.findByTestId("notebook-button").should("not.exist");
cy.icon("bookmark").should("not.exist");
cy.icon("ellipsis").should("not.exist");
sharingMenuButton().should("not.exist");
H.sharingMenuButton().should("not.exist");
});
// should not have disabled action in bottom footer
@@ -635,11 +611,11 @@ describe("scenarios > collections > trash", () => {
});
cy.get("@dashboard").then(dashboard => {
visitDashboard(dashboard.id);
H.visitDashboard(dashboard.id);
cy.findAllByTestId("dashboard-header").within(() => {
cy.icon("pencil").should("not.exist");
sharingMenuButton().should("not.exist");
H.sharingMenuButton().should("not.exist");
cy.icon("clock").should("not.exist");
cy.icon("bookmark").should("not.exist");
cy.icon("ellipsis").should("not.exist");
@@ -666,19 +642,23 @@ describe("scenarios > collections > trash", () => {
cy.visit("/admin/permissions/collections");
selectSidebarItem("Collection A");
H.selectSidebarItem("Collection A");
const COLLECTION_ACCESS_PERMISSION_INDEX = 0;
modifyPermission("All Users", COLLECTION_ACCESS_PERMISSION_INDEX, "View");
modifyPermission(
H.modifyPermission(
"All Users",
COLLECTION_ACCESS_PERMISSION_INDEX,
"View",
);
H.modifyPermission(
"collection",
COLLECTION_ACCESS_PERMISSION_INDEX,
"View",
);
modifyPermission("data", COLLECTION_ACCESS_PERMISSION_INDEX, "View");
H.modifyPermission("data", COLLECTION_ACCESS_PERMISSION_INDEX, "View");
cy.button("Save changes").click();
modal().within(() => {
H.modal().within(() => {
cy.findByText("Save permissions?");
cy.findByText("Are you sure you want to do this?");
cy.button("Yes").click();
@@ -690,7 +670,7 @@ describe("scenarios > collections > trash", () => {
cy.signInAsNormalUser();
cy.get("@collection").then(collection => {
visitCollection(collection.id);
H.visitCollection(collection.id);
archiveBanner().findByText("Restore").should("not.exist");
archiveBanner().findByText("Move").should("not.exist");
archiveBanner().findByText("Delete permanently").should("not.exist");
@@ -759,7 +739,7 @@ describe("scenarios > collections > trash", () => {
cy.intercept("GET", `/api/collection/${collection.id}`).as(
"getCollection",
);
visitCollection(collection.id);
H.visitCollection(collection.id);
cy.wait("@getCollection");
assertTrashSelectedInNavigationSidebar();
});
@@ -767,9 +747,9 @@ describe("scenarios > collections > trash", () => {
cy.log("Make sure trash is selected for a trashed dashboard");
cy.get("@dashboard").then(dashboard => {
cy.intercept("GET", `/api/dashboard/${dashboard.id}*`).as("getDashboard");
visitDashboard(dashboard.id);
H.visitDashboard(dashboard.id);
cy.wait("@getDashboard");
openNavigationSidebar();
H.openNavigationSidebar();
assertTrashSelectedInNavigationSidebar();
});
@@ -779,9 +759,9 @@ describe("scenarios > collections > trash", () => {
cy.intercept("POST", `/api/card/${question.id}/query`).as(
"getQuestionResult",
);
visitQuestion(question.id);
H.visitQuestion(question.id);
cy.wait("@getQuestionResult");
openNavigationSidebar();
H.openNavigationSidebar();
assertTrashSelectedInNavigationSidebar();
});
});
@@ -793,15 +773,15 @@ describe("scenarios > collections > trash", () => {
cy.visit("/trash");
dragAndDrop(
main().findByText("Dashboard A"),
navigationSidebar().findByText("Trash"),
H.main().findByText("Dashboard A"),
H.navigationSidebar().findByText("Trash"),
);
cy.wait(100); // small wait to make sure a network request could have gone out
// assert no update request went out
cy.get("@updateDashboard.all").should("have.length", 0);
cy.findByTestId("toast-undo").should("not.exist");
main(() => {
H.main(() => {
cy.findByText(/Deleted items will appear here/).should("not.exist");
cy.findByText("Dashboard A").should("exist");
});
@@ -813,19 +793,19 @@ describe("scenarios > collections > trash", () => {
cy.visit("/trash");
dragAndDrop(
main().findByText("Dashboard A"),
navigationSidebar().findByText("First collection"),
H.main().findByText("Dashboard A"),
H.navigationSidebar().findByText("First collection"),
);
cy.get("@updateDashboard.all").should("have.length", 1);
main()
H.main()
.findByText(/Deleted items will appear here/)
.should("exist");
cy.findByTestId("toast-undo").should("exist");
undo();
H.undo();
cy.get("@updateDashboard.all").should("have.length", 2);
main().within(() => {
H.main().within(() => {
cy.findByText(/Deleted items will appear here/).should("not.exist");
cy.findByText("Dashboard A").should("exist");
});
@@ -837,20 +817,20 @@ describe("scenarios > collections > trash", () => {
collection_id: FIRST_COLLECTION_ID,
});
cy.intercept("PUT", "/api/dashboard/**").as("updateDashboard");
visitCollection(FIRST_COLLECTION_ID);
H.visitCollection(FIRST_COLLECTION_ID);
dragAndDrop(
main().findByText("Dashboard A"),
navigationSidebar().findByText("Trash"),
H.main().findByText("Dashboard A"),
H.navigationSidebar().findByText("Trash"),
);
cy.get("@updateDashboard.all").should("have.length", 1);
main().findByText("Dashboard A").should("not.exist");
H.main().findByText("Dashboard A").should("not.exist");
cy.findByTestId("toast-undo").should("exist");
undo();
H.undo();
cy.get("@updateDashboard.all").should("have.length", 2);
main().within(() => {
H.main().within(() => {
cy.findByText("Dashboard A").should("exist");
});
});
@@ -901,16 +881,16 @@ function createCollection(collectionInfo, archive) {
}
function createQuestion(questionInfo, archive) {
return _createQuestion(questionInfo).then(({ body: question }) =>
Promise.all([question, archive && archiveQuestion(question.id)]).then(
return H.createQuestion(questionInfo).then(({ body: question }) =>
Promise.all([question, archive && H.archiveQuestion(question.id)]).then(
([question]) => question,
),
);
}
function createNativeQuestion(questionInfo, archive) {
return _createNativeQuestion(questionInfo).then(({ body: question }) =>
Promise.all([question, archive && archiveQuestion(question.id)]).then(
return H.createNativeQuestion(questionInfo).then(({ body: question }) =>
Promise.all([question, archive && H.archiveQuestion(question.id)]).then(
([question]) => question,
),
);
@@ -959,7 +939,7 @@ function selectItem(name) {
}
function assertTrashSelectedInNavigationSidebar() {
navigationSidebar().within(() => {
H.navigationSidebar().within(() => {
cy.findByText("Trash")
.parents("li")
.should("have.attr", "aria-selected", "true");

View File

@@ -1,27 +1,10 @@
import { H } from "e2e/support";
import { USER_GROUPS, WRITABLE_DB_ID } from "e2e/support/cypress_data";
import { FIRST_COLLECTION_ID } from "e2e/support/cypress_sample_instance_data";
import {
FIXTURE_PATH,
INVALID_CSV_FILES,
VALID_CSV_FILES,
describeWithSnowplow,
enableTracking,
enableUploads,
expectGoodSnowplowEvent,
expectNoBadSnowplowEvents,
headlessUpload,
modal,
popover,
queryWritableDB,
resetSnowplow,
restore,
setTokenFeatures,
uploadFile,
} from "e2e/support/helpers";
const { NOSQL_GROUP, ALL_USERS_GROUP } = USER_GROUPS;
describeWithSnowplow(
H.describeWithSnowplow(
"CSV Uploading",
{ tags: ["@external", "@actions"] },
() => {
@@ -32,20 +15,23 @@ describeWithSnowplow(
});
it("Can upload a CSV file to an empty postgres schema", () => {
const testFile = VALID_CSV_FILES[0];
const testFile = H.VALID_CSV_FILES[0];
const EMPTY_SCHEMA_NAME = "empty_uploads";
cy.intercept("PUT", "/api/setting").as("saveSettings");
cy.intercept("GET", "/api/database").as("databaseList");
restore("postgres-writable");
H.restore("postgres-writable");
cy.signInAsAdmin();
queryWritableDB(
H.queryWritableDB(
"DROP SCHEMA IF EXISTS empty_uploads CASCADE;",
"postgres",
);
queryWritableDB("CREATE SCHEMA IF NOT EXISTS empty_uploads;", "postgres");
H.queryWritableDB(
"CREATE SCHEMA IF NOT EXISTS empty_uploads;",
"postgres",
);
cy.request("POST", "/api/collection", {
name: "Uploads Collection",
@@ -58,12 +44,12 @@ describeWithSnowplow(
cy.findByLabelText("Upload Settings Form")
.findByText("Select a database")
.click();
popover().findByText("Writable Postgres12").click();
H.popover().findByText("Writable Postgres12").click();
cy.findByLabelText("Upload Settings Form")
.findByText("Select a schema")
.click();
popover().findByText(EMPTY_SCHEMA_NAME).click();
H.popover().findByText(EMPTY_SCHEMA_NAME).click();
cy.findByLabelText("Upload Settings Form")
.button("Enable uploads")
@@ -75,10 +61,10 @@ describeWithSnowplow(
const tableQuery = `SELECT * FROM information_schema.tables WHERE table_name LIKE '%${testFile.tableName}_%' ORDER BY table_name DESC LIMIT 1;`;
queryWritableDB(tableQuery, "postgres").then(result => {
H.queryWritableDB(tableQuery, "postgres").then(result => {
expect(result.rows.length).to.equal(1);
const tableName = result.rows[0].table_name;
queryWritableDB(
H.queryWritableDB(
`SELECT count(*) FROM ${EMPTY_SCHEMA_NAME}.${tableName};`,
"postgres",
).then(result => {
@@ -91,14 +77,14 @@ describeWithSnowplow(
);
cy.findByTestId("app-bar").button(/gear/).click();
popover().findByText("Admin settings").click();
H.popover().findByText("Admin settings").click();
cy.findByRole("link", { name: "Table Metadata" }).click();
cy.findByTestId("admin-metadata-header")
.findByText("Sample Database")
.click();
popover().findByText("Writable Postgres12").click();
H.popover().findByText("Writable Postgres12").click();
cy.findByTestId("admin-metadata-table-list").within(() => {
cy.findByText("1 Queryable Table").should("exist");
@@ -109,10 +95,10 @@ describeWithSnowplow(
["postgres", "mysql"].forEach(dialect => {
describe(`CSV Uploading (${dialect})`, () => {
beforeEach(() => {
restore(`${dialect}-writable`);
resetSnowplow();
H.restore(`${dialect}-writable`);
H.resetSnowplow();
cy.signInAsAdmin();
enableTracking();
H.enableTracking();
cy.request("POST", "/api/collection", {
name: "Uploads Collection",
@@ -120,28 +106,28 @@ describeWithSnowplow(
}).then(({ body: { id: collectionId } }) => {
cy.wrap(collectionId).as("collectionId");
});
enableUploads(dialect);
H.enableUploads(dialect);
});
afterEach(() => {
expectNoBadSnowplowEvents();
H.expectNoBadSnowplowEvents();
});
VALID_CSV_FILES.forEach(testFile => {
H.VALID_CSV_FILES.forEach(testFile => {
it(`Can upload ${testFile.fileName} to a collection`, () => {
uploadFileToCollection(testFile);
expectGoodSnowplowEvent({
H.expectGoodSnowplowEvent({
event: "csv_upload_successful",
});
const tableQuery = `SELECT * FROM information_schema.tables WHERE table_name LIKE '%${testFile.tableName}_%' ORDER BY table_name DESC LIMIT 1;`;
queryWritableDB(tableQuery, dialect).then(result => {
H.queryWritableDB(tableQuery, dialect).then(result => {
expect(result.rows.length).to.equal(1);
const tableName =
result.rows[0].table_name ?? result.rows[0].TABLE_NAME;
queryWritableDB(
H.queryWritableDB(
`SELECT count(*) as count FROM ${tableName};`,
dialect,
).then(result => {
@@ -153,17 +139,17 @@ describeWithSnowplow(
});
});
INVALID_CSV_FILES.forEach(testFile => {
H.INVALID_CSV_FILES.forEach(testFile => {
it(`Cannot upload ${testFile.fileName} to a collection`, () => {
uploadFileToCollection(testFile);
expectGoodSnowplowEvent({
H.expectGoodSnowplowEvent({
event: "csv_upload_failed",
});
const tableQuery = `SELECT * FROM information_schema.tables WHERE table_name LIKE '%${testFile.tableName}_%' ORDER BY table_name DESC LIMIT 1;`;
queryWritableDB(tableQuery, dialect).then(result => {
H.queryWritableDB(tableQuery, dialect).then(result => {
expect(result.rows.length).to.equal(0);
});
});
@@ -171,66 +157,66 @@ describeWithSnowplow(
describe("CSV appends", () => {
it("Can append a CSV file to an existing table", () => {
uploadFileToCollection(VALID_CSV_FILES[0]);
uploadFileToCollection(H.VALID_CSV_FILES[0]);
cy.findByTestId("view-footer").findByText(
`Showing ${VALID_CSV_FILES[0].rowCount} rows`,
`Showing ${H.VALID_CSV_FILES[0].rowCount} rows`,
);
uploadToExisting({
testFile: VALID_CSV_FILES[0],
testFile: H.VALID_CSV_FILES[0],
uploadMode: "append",
});
cy.findByTestId("view-footer").findByText(
`Showing ${VALID_CSV_FILES[0].rowCount * 2} rows`,
`Showing ${H.VALID_CSV_FILES[0].rowCount * 2} rows`,
);
});
it("Cannot append a CSV file to a table with a different schema", () => {
uploadFileToCollection(VALID_CSV_FILES[0]);
uploadFileToCollection(H.VALID_CSV_FILES[0]);
cy.findByTestId("view-footer").findByText(
`Showing ${VALID_CSV_FILES[0].rowCount} rows`,
`Showing ${H.VALID_CSV_FILES[0].rowCount} rows`,
);
uploadToExisting({
testFile: VALID_CSV_FILES[1],
testFile: H.VALID_CSV_FILES[1],
identicalSchema: false,
uploadMode: "append",
});
cy.findByTestId("view-footer").findByText(
`Showing ${VALID_CSV_FILES[0].rowCount} rows`,
`Showing ${H.VALID_CSV_FILES[0].rowCount} rows`,
);
});
});
describe("CSV replacement", () => {
it("Can replace data in an existing table", () => {
uploadFileToCollection(VALID_CSV_FILES[0]);
uploadFileToCollection(H.VALID_CSV_FILES[0]);
cy.findByTestId("view-footer").findByText(
`Showing ${VALID_CSV_FILES[0].rowCount} rows`,
`Showing ${H.VALID_CSV_FILES[0].rowCount} rows`,
);
uploadToExisting({
testFile: VALID_CSV_FILES[0],
testFile: H.VALID_CSV_FILES[0],
uploadMode: "replace",
});
cy.findByTestId("view-footer").findByText(
`Showing ${VALID_CSV_FILES[0].rowCount} rows`,
`Showing ${H.VALID_CSV_FILES[0].rowCount} rows`,
);
});
it("Cannot data in a table with a different schema", () => {
uploadFileToCollection(VALID_CSV_FILES[0]);
uploadFileToCollection(H.VALID_CSV_FILES[0]);
cy.findByTestId("view-footer").findByText(
`Showing ${VALID_CSV_FILES[0].rowCount} rows`,
`Showing ${H.VALID_CSV_FILES[0].rowCount} rows`,
);
uploadToExisting({
testFile: VALID_CSV_FILES[1],
testFile: H.VALID_CSV_FILES[1],
identicalSchema: false,
uploadMode: "replace",
});
cy.findByTestId("view-footer").findByText(
`Showing ${VALID_CSV_FILES[0].rowCount} rows`,
`Showing ${H.VALID_CSV_FILES[0].rowCount} rows`,
);
});
});
@@ -241,11 +227,11 @@ describeWithSnowplow(
describe("permissions", () => {
it("should not show you upload buttons if you are a sandboxed user", () => {
restore("postgres-12");
H.restore("postgres-12");
cy.signInAsAdmin();
setTokenFeatures("all");
enableUploads("postgres");
H.setTokenFeatures("all");
H.enableUploads("postgres");
//Deny access for all users to writable DB
cy.updatePermissionsGraph({
@@ -285,11 +271,11 @@ describe("permissions", () => {
"should show you upload buttons if you have unrestricted access to the upload schema",
{ tags: ["@external"] },
() => {
restore("postgres-12");
H.restore("postgres-12");
cy.signInAsAdmin();
setTokenFeatures("all");
enableUploads("postgres");
H.setTokenFeatures("all");
H.enableUploads("postgres");
cy.updatePermissionsGraph({
[ALL_USERS_GROUP]: {
@@ -324,19 +310,19 @@ describe("Upload Table Cleanup/Management", () => {
cy.intercept("GET", "/api/ee/upload-management/tables").as(
"getUploadTables",
);
restore("postgres-12");
H.restore("postgres-12");
cy.signInAsAdmin();
enableUploads("postgres");
setTokenFeatures("all");
H.enableUploads("postgres");
H.setTokenFeatures("all");
});
it("should allow a user to delete an upload table", () => {
headlessUpload(FIRST_COLLECTION_ID, VALID_CSV_FILES[0]);
headlessUpload(FIRST_COLLECTION_ID, VALID_CSV_FILES[0]);
headlessUpload(FIRST_COLLECTION_ID, VALID_CSV_FILES[0]);
H.headlessUpload(FIRST_COLLECTION_ID, H.VALID_CSV_FILES[0]);
H.headlessUpload(FIRST_COLLECTION_ID, H.VALID_CSV_FILES[0]);
H.headlessUpload(FIRST_COLLECTION_ID, H.VALID_CSV_FILES[0]);
headlessUpload(FIRST_COLLECTION_ID, VALID_CSV_FILES[1]);
headlessUpload(FIRST_COLLECTION_ID, VALID_CSV_FILES[1]);
H.headlessUpload(FIRST_COLLECTION_ID, H.VALID_CSV_FILES[1]);
H.headlessUpload(FIRST_COLLECTION_ID, H.VALID_CSV_FILES[1]);
cy.visit("/admin/settings/uploads");
cy.wait("@getUploadTables");
@@ -349,7 +335,7 @@ describe("Upload Table Cleanup/Management", () => {
cy.findAllByLabelText("trash icon").first().click();
});
modal().button("Delete").click();
H.modal().button("Delete").click();
cy.wait("@getUploadTables");
cy.findByTestId("undo-list").findByText(/1 table deleted/i);
@@ -364,7 +350,7 @@ describe("Upload Table Cleanup/Management", () => {
});
cy.findByTestId("toast-card").button("Delete").click();
modal().button("Delete").click();
H.modal().button("Delete").click();
cy.wait("@getUploadTables");
cy.findByTestId("undo-list").findByText(/2 tables deleted/i);
@@ -381,7 +367,7 @@ function uploadFileToCollection(testFile) {
cy.visit(`/collection/${collectionId}`),
);
uploadFile("#upload-input", "Uploads Collection", testFile);
H.uploadFile("#upload-input", "Uploads Collection", testFile);
if (testFile.valid) {
cy.get("main").within(() => cy.findByText("Uploads Collection"));
@@ -418,9 +404,9 @@ function uploadToExisting({
replace: "@replaceCSV",
};
popover().findByText(uploadOptions[uploadMode]).click();
H.popover().findByText(uploadOptions[uploadMode]).click();
cy.fixture(`${FIXTURE_PATH}/${testFile.fileName}`).then(file => {
cy.fixture(`${H.FIXTURE_PATH}/${testFile.fileName}`).then(file => {
cy.get("#upload-file-input").selectFile(
{
contents: Cypress.Buffer.from(file),
@@ -450,6 +436,6 @@ function uploadToExisting({
"Error uploading your file",
);
modal().findByText("Upload error details");
H.modal().findByText("Upload error details");
}
}

View File

@@ -1,9 +1,9 @@
import { withSampleDatabase } from "e2e/support/helpers";
import { H } from "e2e/support";
it("should generate metadata", () => {
cy.signInAsAdmin();
withSampleDatabase(SAMPLE_DATABASE => {
H.withSampleDatabase(SAMPLE_DATABASE => {
cy.writeFile("e2e/support/cypress_sample_database.json", SAMPLE_DATABASE);
});
});

View File

@@ -1,4 +1,4 @@
import { visualize } from "e2e/support/helpers";
import { H } from "e2e/support";
import {
fillAreaUnderLineChart,
newQuestion,
@@ -49,7 +49,7 @@ it("should create questions", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Category").click();
visualize();
H.visualize();
cy.get(".bar").should("have.length", 4);
@@ -107,7 +107,7 @@ it("should create questions", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Created At: Quarter");
visualize();
H.visualize();
cy.get("circle");
cy.findByTestId("viz-type-button").click();

View File

@@ -1,29 +1,5 @@
import { H } from "e2e/support";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
type DashboardDetails,
type StructuredQuestionDetails,
assertQueryBuilderRowCount,
assertTableData,
createDashboard,
createQuestion,
editDashboard,
enterCustomColumnDetails,
entityPickerModal,
entityPickerModalTab,
getDashboardCard,
getNotebookStep,
modal,
openNotebook,
popover,
queryBuilderHeader,
restore,
saveDashboard,
showDashboardCardActions,
sidebar,
tableHeaderClick,
visitDashboard,
visualize,
} from "e2e/support/helpers";
import type { CardId, DashboardParameterMapping } from "metabase-types/api";
import { createMockDashboardCard } from "metabase-types/api/mocks";
@@ -34,7 +10,7 @@ describe("scenarios > custom column > boolean functions", () => {
const expressionName = "Boolean column";
beforeEach(() => {
restore();
H.restore();
cy.signInAsNormalUser();
cy.intercept("POST", "/api/dataset").as("dataset");
cy.intercept("PUT", "/api/card/*").as("updateCard");
@@ -42,7 +18,7 @@ describe("scenarios > custom column > boolean functions", () => {
describe("expression editor", () => {
const stringQuestionColumns = ["Category"];
const stringQuestionDetails: StructuredQuestionDetails = {
const stringQuestionDetails: H.StructuredQuestionDetails = {
query: {
"source-table": PRODUCTS_ID,
fields: [["field", PRODUCTS.CATEGORY, null]],
@@ -52,7 +28,7 @@ describe("scenarios > custom column > boolean functions", () => {
};
const numberQuestionColumns = ["Total"];
const numberQuestionDetails: StructuredQuestionDetails = {
const numberQuestionDetails: H.StructuredQuestionDetails = {
query: {
"source-table": ORDERS_ID,
fields: [["field", ORDERS.TOTAL, null]],
@@ -62,7 +38,7 @@ describe("scenarios > custom column > boolean functions", () => {
};
const dateQuestionColumns = ["Created At"];
const dateQuestionDetails: StructuredQuestionDetails = {
const dateQuestionDetails: H.StructuredQuestionDetails = {
query: {
"source-table": ORDERS_ID,
fields: [["field", ORDERS.CREATED_AT, null]],
@@ -79,50 +55,50 @@ describe("scenarios > custom column > boolean functions", () => {
modifiedExpression,
modifiedExpressionRows,
}: {
questionDetails: StructuredQuestionDetails;
questionDetails: H.StructuredQuestionDetails;
questionColumns: string[];
newExpression: string;
newExpressionRows: string[][];
modifiedExpression: string;
modifiedExpressionRows: string[][];
}) {
createQuestion(questionDetails, { visitQuestion: true });
H.createQuestion(questionDetails, { visitQuestion: true });
cy.log("add a new custom column");
openNotebook();
getNotebookStep("data").button("Custom column").click();
enterCustomColumnDetails({
H.openNotebook();
H.getNotebookStep("data").button("Custom column").click();
H.enterCustomColumnDetails({
formula: newExpression,
name: expressionName,
});
popover().button("Done").click();
visualize();
H.popover().button("Done").click();
H.visualize();
cy.wait("@dataset");
assertTableData({
H.assertTableData({
columns: [...questionColumns, expressionName],
firstRows: newExpressionRows,
});
cy.log("modify an existing custom column");
openNotebook();
getNotebookStep("expression").findByText(expressionName).click();
enterCustomColumnDetails({
H.openNotebook();
H.getNotebookStep("expression").findByText(expressionName).click();
H.enterCustomColumnDetails({
formula: modifiedExpression,
name: expressionName,
});
popover().button("Update").click();
visualize();
H.popover().button("Update").click();
H.visualize();
cy.wait("@dataset");
assertTableData({
H.assertTableData({
columns: [...questionColumns, expressionName],
firstRows: modifiedExpressionRows,
});
cy.log("assert that the question can be saved");
queryBuilderHeader().button("Save").click();
modal().button("Save").click();
H.queryBuilderHeader().button("Save").click();
H.modal().button("Save").click();
cy.wait("@updateCard");
queryBuilderHeader().button("Save").should("not.exist");
H.queryBuilderHeader().button("Save").should("not.exist");
}
it("isNull", () => {
@@ -216,7 +192,7 @@ describe("scenarios > custom column > boolean functions", () => {
describe("query builder", () => {
describe("same stage", () => {
const questionDetails: StructuredQuestionDetails = {
const questionDetails: H.StructuredQuestionDetails = {
query: {
"source-table": PRODUCTS_ID,
fields: [
@@ -234,29 +210,29 @@ describe("scenarios > custom column > boolean functions", () => {
};
it("should be able to add a same-stage custom column", () => {
createQuestion(questionDetails, { visitQuestion: true });
openNotebook();
H.createQuestion(questionDetails, { visitQuestion: true });
H.openNotebook();
cy.log("add an identity column");
getNotebookStep("expression").icon("add").click();
enterCustomColumnDetails({
H.getNotebookStep("expression").icon("add").click();
H.enterCustomColumnDetails({
formula: `[${expressionName}]`,
name: "Identity column",
});
popover().button("Done").click();
H.popover().button("Done").click();
cy.log("add a simple expression");
getNotebookStep("expression").icon("add").click();
enterCustomColumnDetails({
H.getNotebookStep("expression").icon("add").click();
H.enterCustomColumnDetails({
formula: `[${expressionName}] != True`,
name: "Simple expression",
});
popover().button("Done").click();
H.popover().button("Done").click();
cy.log("assert query results");
visualize();
H.visualize();
cy.wait("@dataset");
assertTableData({
H.assertTableData({
columns: [
"Category",
expressionName,
@@ -268,50 +244,50 @@ describe("scenarios > custom column > boolean functions", () => {
});
it("should be able to add a same-stage filter", () => {
createQuestion(questionDetails, { visitQuestion: true });
assertQueryBuilderRowCount(200);
tableHeaderClick(expressionName);
popover().within(() => {
H.createQuestion(questionDetails, { visitQuestion: true });
H.assertQueryBuilderRowCount(200);
H.tableHeaderClick(expressionName);
H.popover().within(() => {
cy.findByText("Filter by this column").click();
cy.findByLabelText("True").click();
cy.findByText("Add filter").click();
});
assertQueryBuilderRowCount(51);
H.assertQueryBuilderRowCount(51);
});
it("should be able to add a same-stage aggregation", () => {
createQuestion(questionDetails, { visitQuestion: true });
openNotebook();
getNotebookStep("expression").button("Summarize").click();
popover().findByText("Minimum of ...").click();
popover().findByText(expressionName).click();
getNotebookStep("summarize")
H.createQuestion(questionDetails, { visitQuestion: true });
H.openNotebook();
H.getNotebookStep("expression").button("Summarize").click();
H.popover().findByText("Minimum of ...").click();
H.popover().findByText(expressionName).click();
H.getNotebookStep("summarize")
.findByTestId("aggregate-step")
.icon("add")
.click();
popover().findByText("Maximum of ...").click();
popover().findByText(expressionName).click();
visualize();
H.popover().findByText("Maximum of ...").click();
H.popover().findByText(expressionName).click();
H.visualize();
cy.wait("@dataset");
assertTableData({
H.assertTableData({
columns: [`Min of ${expressionName}`, `Max of ${expressionName}`],
firstRows: [["false", "true"]],
});
});
it("should be able to add a same-stage breakout", () => {
createQuestion(questionDetails, { visitQuestion: true });
openNotebook();
getNotebookStep("expression").button("Summarize").click();
popover().findByText("Count of rows").click();
getNotebookStep("summarize")
H.createQuestion(questionDetails, { visitQuestion: true });
H.openNotebook();
H.getNotebookStep("expression").button("Summarize").click();
H.popover().findByText("Count of rows").click();
H.getNotebookStep("summarize")
.findByTestId("breakout-step")
.findByText("Pick a column to group by")
.click();
popover().findByText(expressionName).click();
visualize();
H.popover().findByText(expressionName).click();
H.visualize();
cy.wait("@dataset");
assertTableData({
H.assertTableData({
columns: [expressionName, "Count"],
firstRows: [
["false", "149"],
@@ -321,13 +297,13 @@ describe("scenarios > custom column > boolean functions", () => {
});
it("should be able to add a same-stage sorting", () => {
createQuestion(questionDetails, { visitQuestion: true });
openNotebook();
getNotebookStep("expression").button("Sort").click();
popover().findByText(expressionName).click();
visualize();
H.createQuestion(questionDetails, { visitQuestion: true });
H.openNotebook();
H.getNotebookStep("expression").button("Sort").click();
H.popover().findByText(expressionName).click();
H.visualize();
cy.wait("@dataset");
assertTableData({
H.assertTableData({
columns: ["Category", expressionName],
firstRows: [["Doohickey", "false"]],
});
@@ -335,7 +311,7 @@ describe("scenarios > custom column > boolean functions", () => {
});
describe("previous stage", () => {
const questionDetails: StructuredQuestionDetails = {
const questionDetails: H.StructuredQuestionDetails = {
query: {
"source-table": PRODUCTS_ID,
expressions: {
@@ -353,17 +329,17 @@ describe("scenarios > custom column > boolean functions", () => {
};
it("should be able to add a post-aggregation custom column", () => {
createQuestion(questionDetails, { visitQuestion: true });
openNotebook();
getNotebookStep("summarize").button("Custom column").click();
enterCustomColumnDetails({
H.createQuestion(questionDetails, { visitQuestion: true });
H.openNotebook();
H.getNotebookStep("summarize").button("Custom column").click();
H.enterCustomColumnDetails({
formula: `not([${expressionName}])`,
name: "Simple expression",
});
popover().button("Done").click();
visualize();
H.popover().button("Done").click();
H.visualize();
cy.wait("@dataset");
assertTableData({
H.assertTableData({
columns: [expressionName, "Count", "Simple expression"],
firstRows: [
["false", "149", "true"],
@@ -373,46 +349,46 @@ describe("scenarios > custom column > boolean functions", () => {
});
it("should be able to add a post-aggregation filter", () => {
createQuestion(questionDetails, { visitQuestion: true });
assertQueryBuilderRowCount(2);
tableHeaderClick(expressionName);
popover().within(() => {
H.createQuestion(questionDetails, { visitQuestion: true });
H.assertQueryBuilderRowCount(2);
H.tableHeaderClick(expressionName);
H.popover().within(() => {
cy.findByText("Filter by this column").click();
cy.findByLabelText("True").click();
cy.findByText("Add filter").click();
});
assertQueryBuilderRowCount(1);
H.assertQueryBuilderRowCount(1);
});
it("should be able to add a post-aggregation aggregation", () => {
createQuestion(questionDetails, { visitQuestion: true });
openNotebook();
getNotebookStep("summarize").button("Summarize").click();
popover().findByText("Minimum of ...").click();
popover().findByText(expressionName).click();
visualize();
H.createQuestion(questionDetails, { visitQuestion: true });
H.openNotebook();
H.getNotebookStep("summarize").button("Summarize").click();
H.popover().findByText("Minimum of ...").click();
H.popover().findByText(expressionName).click();
H.visualize();
cy.wait("@dataset");
assertTableData({
H.assertTableData({
columns: [`Min of ${expressionName}`],
firstRows: [["false"]],
});
});
it("should be able to add a post-aggregation breakout and sorting", () => {
createQuestion(questionDetails, { visitQuestion: true });
H.createQuestion(questionDetails, { visitQuestion: true });
cy.log("add a breakout");
openNotebook();
getNotebookStep("summarize").button("Summarize").click();
popover().findByText("Count of rows").click();
getNotebookStep("summarize", { stage: 1 })
H.openNotebook();
H.getNotebookStep("summarize").button("Summarize").click();
H.popover().findByText("Count of rows").click();
H.getNotebookStep("summarize", { stage: 1 })
.findByTestId("breakout-step")
.findByText("Pick a column to group by")
.click();
popover().findByText(expressionName).click();
visualize();
H.popover().findByText(expressionName).click();
H.visualize();
cy.wait("@dataset");
assertTableData({
H.assertTableData({
columns: [expressionName, "Count"],
firstRows: [
["false", 1],
@@ -421,12 +397,12 @@ describe("scenarios > custom column > boolean functions", () => {
});
cy.log("add sorting");
openNotebook();
getNotebookStep("summarize", { stage: 1 }).button("Sort").click();
popover().findByText(expressionName).click();
getNotebookStep("sort", { stage: 1 }).icon("arrow_up").click();
visualize();
assertTableData({
H.openNotebook();
H.getNotebookStep("summarize", { stage: 1 }).button("Sort").click();
H.popover().findByText(expressionName).click();
H.getNotebookStep("sort", { stage: 1 }).icon("arrow_up").click();
H.visualize();
H.assertTableData({
columns: [expressionName, "Count"],
firstRows: [
["true", 1],
@@ -437,7 +413,7 @@ describe("scenarios > custom column > boolean functions", () => {
});
describe("source card", () => {
const questionDetails: StructuredQuestionDetails = {
const questionDetails: H.StructuredQuestionDetails = {
name: "Source",
query: {
"source-table": PRODUCTS_ID,
@@ -453,7 +429,7 @@ describe("scenarios > custom column > boolean functions", () => {
function getNestedQuestionDetails(
cardId: number,
): StructuredQuestionDetails {
): H.StructuredQuestionDetails {
return {
name: "Nested",
query: {
@@ -463,97 +439,97 @@ describe("scenarios > custom column > boolean functions", () => {
}
it("should be able to add a custom column for a boolean column", () => {
createQuestion(questionDetails).then(({ body: card }) =>
createQuestion(getNestedQuestionDetails(card.id), {
H.createQuestion(questionDetails).then(({ body: card }) =>
H.createQuestion(getNestedQuestionDetails(card.id), {
visitQuestion: true,
}),
);
cy.log("add a custom column");
openNotebook();
getNotebookStep("data").button("Custom column").click();
enterCustomColumnDetails({
H.openNotebook();
H.getNotebookStep("data").button("Custom column").click();
H.enterCustomColumnDetails({
formula: `[${expressionName}] != True`,
name: "Simple expression",
});
popover().button("Done").click();
visualize();
H.popover().button("Done").click();
H.visualize();
cy.wait("@dataset");
assertQueryBuilderRowCount(200);
H.assertQueryBuilderRowCount(200);
cy.log("use the new custom column in a filter");
tableHeaderClick("Simple expression");
popover().within(() => {
H.tableHeaderClick("Simple expression");
H.popover().within(() => {
cy.findByText("Filter by this column").click();
cy.findByLabelText("True").click();
cy.findByText("Add filter").click();
});
assertQueryBuilderRowCount(149);
H.assertQueryBuilderRowCount(149);
});
it("should be able to add a filter for a boolean column", () => {
createQuestion(questionDetails).then(({ body: card }) =>
createQuestion(getNestedQuestionDetails(card.id), {
H.createQuestion(questionDetails).then(({ body: card }) =>
H.createQuestion(getNestedQuestionDetails(card.id), {
visitQuestion: true,
}),
);
assertQueryBuilderRowCount(200);
tableHeaderClick(expressionName);
popover().within(() => {
H.assertQueryBuilderRowCount(200);
H.tableHeaderClick(expressionName);
H.popover().within(() => {
cy.findByText("Filter by this column").click();
cy.findByLabelText("True").click();
cy.findByText("Add filter").click();
});
assertQueryBuilderRowCount(51);
H.assertQueryBuilderRowCount(51);
});
it("should be able to add an aggregation for a boolean column", () => {
createQuestion(questionDetails).then(({ body: card }) =>
createQuestion(getNestedQuestionDetails(card.id), {
H.createQuestion(questionDetails).then(({ body: card }) =>
H.createQuestion(getNestedQuestionDetails(card.id), {
visitQuestion: true,
}),
);
openNotebook();
getNotebookStep("data").button("Summarize").click();
popover().findByText("Minimum of ...").click();
popover().findByText(expressionName).click();
visualize();
H.openNotebook();
H.getNotebookStep("data").button("Summarize").click();
H.popover().findByText("Minimum of ...").click();
H.popover().findByText(expressionName).click();
H.visualize();
cy.wait("@dataset");
assertTableData({
H.assertTableData({
columns: [`Min of ${expressionName}`],
firstRows: [["false"]],
});
});
it.skip("should be able to add a breakout and sorting for a boolean column (metabase#49305)", () => {
createQuestion(questionDetails).then(({ body: card }) =>
createQuestion(getNestedQuestionDetails(card.id), {
H.createQuestion(questionDetails).then(({ body: card }) =>
H.createQuestion(getNestedQuestionDetails(card.id), {
visitQuestion: true,
}),
);
cy.log("add a breakout");
openNotebook();
getNotebookStep("data").button("Summarize").click();
getNotebookStep("summarize")
H.openNotebook();
H.getNotebookStep("data").button("Summarize").click();
H.getNotebookStep("summarize")
.findByTestId("breakout-step")
.findByText("Pick a column to group by")
.click();
popover().findByText(expressionName).click();
visualize();
H.popover().findByText(expressionName).click();
H.visualize();
cy.wait("@dataset");
assertTableData({
H.assertTableData({
columns: [expressionName],
firstRows: [["false"], ["true"]],
});
cy.log("add sorting");
openNotebook();
getNotebookStep("summarize").button("Sort").click();
popover().findByText(expressionName).click();
getNotebookStep("sort").icon("arrow_up").click();
visualize();
assertTableData({
H.openNotebook();
H.getNotebookStep("summarize").button("Sort").click();
H.popover().findByText(expressionName).click();
H.getNotebookStep("sort").icon("arrow_up").click();
H.visualize();
H.assertTableData({
columns: [expressionName],
firstRows: [["true"], ["false"]],
});
@@ -562,7 +538,7 @@ describe("scenarios > custom column > boolean functions", () => {
});
describe("dashboards", () => {
const questionDetails: StructuredQuestionDetails = {
const questionDetails: H.StructuredQuestionDetails = {
name: "Q1",
query: {
"source-table": PEOPLE_ID,
@@ -588,7 +564,7 @@ describe("scenarios > custom column > boolean functions", () => {
sectionId: "string",
};
const dashboardDetails: DashboardDetails = {
const dashboardDetails: H.DashboardDetails = {
name: "D1",
parameters: [parameterDetails],
};
@@ -604,10 +580,10 @@ describe("scenarios > custom column > boolean functions", () => {
};
}
function createDashboardWithQuestion(opts?: DashboardDetails) {
return createDashboard({ ...dashboardDetails, ...opts }).then(
function createDashboardWithQuestion(opts?: H.DashboardDetails) {
return H.createDashboard({ ...dashboardDetails, ...opts }).then(
({ body: dashboard }) => {
return createQuestion(questionDetails).then(({ body: card }) => {
return H.createQuestion(questionDetails).then(({ body: card }) => {
return cy
.request("PUT", `/api/dashboard/${dashboard.id}`, {
dashcards: [
@@ -631,31 +607,31 @@ describe("scenarios > custom column > boolean functions", () => {
it("should be able setup an 'open question' click behavior", () => {
createDashboardWithQuestion().then(dashboard =>
visitDashboard(dashboard.id),
H.visitDashboard(dashboard.id),
);
cy.log("setup click behavior");
editDashboard();
showDashboardCardActions();
getDashboardCard().findByLabelText("Click behavior").click();
sidebar().within(() => {
H.editDashboard();
H.showDashboardCardActions();
H.getDashboardCard().findByLabelText("Click behavior").click();
H.sidebar().within(() => {
cy.findByText(expressionName).click();
cy.findByText("Go to a custom destination").click();
cy.findByText("Saved question").click();
});
entityPickerModal().within(() => {
entityPickerModalTab("Questions").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Questions").click();
cy.findByText("Q1").click();
});
sidebar()
H.sidebar()
.findByTestId("click-mappings")
.findByText(expressionName)
.click();
popover().findByText(expressionName).click();
saveDashboard();
H.popover().findByText(expressionName).click();
H.saveDashboard();
cy.log("verify click behavior");
getDashboardCard().findAllByText("false").first().click();
H.getDashboardCard().findAllByText("false").first().click();
cy.wait("@dataset");
cy.findByTestId("qb-filters-panel")
.findByText(`${expressionName} is false`)
@@ -664,36 +640,36 @@ describe("scenarios > custom column > boolean functions", () => {
it("should be able setup an 'open dashboard' click behavior for the same dashboard", () => {
createDashboardWithQuestion().then(dashboard =>
visitDashboard(dashboard.id),
H.visitDashboard(dashboard.id),
);
cy.log("setup click behavior");
editDashboard();
showDashboardCardActions();
getDashboardCard().findByLabelText("Click behavior").click();
sidebar().within(() => {
H.editDashboard();
H.showDashboardCardActions();
H.getDashboardCard().findByLabelText("Click behavior").click();
H.sidebar().within(() => {
cy.findByText(expressionName).click();
cy.findByText("Go to a custom destination").click();
cy.findByText("Dashboard").click();
});
entityPickerModal().within(() => {
entityPickerModalTab("Dashboards").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Dashboards").click();
cy.findByText("D1").click();
});
sidebar()
H.sidebar()
.findByTestId("click-mappings")
.findByText(parameterDetails.name)
.click();
popover().findByText(expressionName).click();
saveDashboard();
H.popover().findByText(expressionName).click();
H.saveDashboard();
cy.log("verify click behavior");
getDashboardCard().within(() => {
H.getDashboardCard().within(() => {
cy.findByText("Hudson Borer").should("be.visible");
cy.findByText("Sydney Rempel").should("not.exist");
cy.findAllByText("false").first().click();
});
getDashboardCard().within(() => {
H.getDashboardCard().within(() => {
cy.findByText("Hudson Borer").should("not.exist");
cy.findByText("Sydney Rempel").should("be.visible");
});
@@ -702,37 +678,37 @@ describe("scenarios > custom column > boolean functions", () => {
it("should be able setup an 'open dashboard' click behavior for another dashboard", () => {
createDashboardWithQuestion({ name: "D2" });
createDashboardWithQuestion({ name: "D1" }).then(dashboard =>
visitDashboard(dashboard.id),
H.visitDashboard(dashboard.id),
);
cy.log("setup click behavior");
editDashboard();
showDashboardCardActions();
getDashboardCard().findByLabelText("Click behavior").click();
sidebar().within(() => {
H.editDashboard();
H.showDashboardCardActions();
H.getDashboardCard().findByLabelText("Click behavior").click();
H.sidebar().within(() => {
cy.findByText(expressionName).click();
cy.findByText("Go to a custom destination").click();
cy.findByText("Dashboard").click();
});
entityPickerModal().within(() => {
entityPickerModalTab("Dashboards").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Dashboards").click();
cy.findByText("D2").click();
});
sidebar()
H.sidebar()
.findByTestId("click-mappings")
.findByText(parameterDetails.name)
.click();
popover().findByText(expressionName).click();
saveDashboard();
H.popover().findByText(expressionName).click();
H.saveDashboard();
cy.log("verify click behavior");
getDashboardCard().within(() => {
H.getDashboardCard().within(() => {
cy.findByText("Hudson Borer").should("be.visible");
cy.findByText("Sydney Rempel").should("not.exist");
cy.findAllByText("false").first().click();
});
cy.findByTestId("dashboard-name-heading").should("have.value", "D2");
getDashboardCard().within(() => {
H.getDashboardCard().within(() => {
cy.findByText("Hudson Borer").should("not.exist");
cy.findByText("Sydney Rempel").should("be.visible");
});

View File

@@ -1,34 +1,24 @@
import { H } from "e2e/support";
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import {
addCustomColumn,
describeWithSnowplow,
expectGoodSnowplowEvent,
expectNoBadSnowplowEvents,
expressionEditorWidget,
openOrdersTable,
popover,
resetSnowplow,
restore,
} from "e2e/support/helpers";
describe("scenarios > question > custom column > expression shortcuts > combine", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsNormalUser();
});
it("should be possible to select a combine columns shortcut", () => {
openOrdersTable({ mode: "notebook", limit: 5 });
addCustomColumn();
H.openOrdersTable({ mode: "notebook", limit: 5 });
H.addCustomColumn();
selectCombineColumns();
selectColumn(0, "Total");
expressionEditorWidget().findByText("Total").should("exist");
H.expressionEditorWidget().findByText("Total").should("exist");
selectColumn(1, "Product", "Rating");
expressionEditorWidget().within(() => {
H.expressionEditorWidget().within(() => {
cy.findByText("Product → Rating").should("exist");
cy.findByTestId("combine-example").should(
@@ -58,14 +48,14 @@ describe("scenarios > question > custom column > expression shortcuts > combine"
});
it("should be possible to cancel when using the combine column shortcut", () => {
openOrdersTable({ mode: "notebook" });
addCustomColumn();
H.openOrdersTable({ mode: "notebook" });
H.addCustomColumn();
selectCombineColumns();
selectColumn(0, "Total");
selectColumn(1, "Product", "Rating");
expressionEditorWidget().within(() => {
H.expressionEditorWidget().within(() => {
// Click the back button, in the header
cy.findByText("Select columns to combine").click();
});
@@ -75,8 +65,8 @@ describe("scenarios > question > custom column > expression shortcuts > combine"
});
it("should be possible to add and remove more than one column", () => {
openOrdersTable({ mode: "notebook" });
addCustomColumn();
H.openOrdersTable({ mode: "notebook" });
H.addCustomColumn();
selectCombineColumns();
selectColumn(0, "Total");
@@ -98,13 +88,13 @@ describe("scenarios > question > custom column > expression shortcuts > combine"
});
it("should pick the correct default separator based on the type of the first column", () => {
openOrdersTable({ mode: "notebook" });
addCustomColumn();
H.openOrdersTable({ mode: "notebook" });
H.addCustomColumn();
selectCombineColumns();
selectColumn(0, "User", "Email");
expressionEditorWidget().within(() => {
H.expressionEditorWidget().within(() => {
cy.findByText("Separated by (empty)").should("exist");
cy.findByText(/Separated by/).click();
@@ -113,18 +103,18 @@ describe("scenarios > question > custom column > expression shortcuts > combine"
});
it("should be possible to edit a previous stages' columns when there is an aggregation (metabase#43226)", () => {
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
cy.button("Summarize").click();
popover().findByText("Count of rows").click();
H.popover().findByText("Count of rows").click();
addCustomColumn();
H.addCustomColumn();
selectCombineColumns();
selectColumn(0, "User", "Email");
expressionEditorWidget().within(() => {
H.expressionEditorWidget().within(() => {
cy.findByText("Separated by (empty)").should("exist");
cy.findByText(/Separated by/).click();
@@ -133,30 +123,30 @@ describe("scenarios > question > custom column > expression shortcuts > combine"
});
});
describeWithSnowplow(
H.describeWithSnowplow(
"scenarios > question > custom column > combine shortcuts",
() => {
beforeEach(() => {
restore();
resetSnowplow();
H.restore();
H.resetSnowplow();
cy.signInAsNormalUser();
});
afterEach(() => {
expectNoBadSnowplowEvents();
H.expectNoBadSnowplowEvents();
});
it("should send an event for combine columns", () => {
openOrdersTable({ mode: "notebook" });
addCustomColumn();
H.openOrdersTable({ mode: "notebook" });
H.addCustomColumn();
selectCombineColumns();
selectColumn(0, "User", "Email");
selectColumn(1, "User", "Email");
expressionEditorWidget().button("Done").click();
H.expressionEditorWidget().button("Done").click();
expectGoodSnowplowEvent({
H.expectGoodSnowplowEvent({
event: "column_combine_via_shortcut",
custom_expressions_used: ["concat"],
database_id: SAMPLE_DB_ID,
@@ -173,11 +163,11 @@ function selectCombineColumns() {
}
function selectColumn(index: number, table: string, name?: string) {
expressionEditorWidget().within(() => {
H.expressionEditorWidget().within(() => {
cy.findAllByTestId("column-input").eq(index).click();
});
popover()
H.popover()
.last()
.within(() => {
if (name) {
@@ -191,7 +181,7 @@ function selectColumn(index: number, table: string, name?: string) {
}
function addColumn() {
expressionEditorWidget().within(() => {
H.expressionEditorWidget().within(() => {
cy.findByText("Add column").click();
});
}

View File

@@ -1,17 +1,6 @@
import { H } from "e2e/support";
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
addCustomColumn,
describeWithSnowplow,
expectGoodSnowplowEvent,
expectNoBadSnowplowEvents,
expressionEditorWidget,
openOrdersTable,
openTable,
popover,
resetSnowplow,
restore,
} from "e2e/support/helpers";
const { ORDERS_ID, ORDERS } = SAMPLE_DATABASE;
@@ -28,11 +17,11 @@ function selectCombineColumns() {
}
function selectColumn(index: number, table: string, name?: string) {
expressionEditorWidget().within(() => {
H.expressionEditorWidget().within(() => {
cy.findAllByTestId("column-input").eq(index).click();
});
popover()
H.popover()
.last()
.within(() => {
if (name) {
@@ -128,7 +117,7 @@ describe("scenarios > question > custom column > expression shortcuts > extract"
];
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
// Make the PRODUCT_ID column a URL column for these tests, to avoid having to create a new model
@@ -139,92 +128,92 @@ describe("scenarios > question > custom column > expression shortcuts > extract"
for (const extraction of EXTRACTIONS) {
it(`should be possible to use the ${extraction.name} extraction on ${extraction.column}`, () => {
openTable({ mode: "notebook", limit: 1, table: extraction.table });
addCustomColumn();
H.openTable({ mode: "notebook", limit: 1, table: extraction.table });
H.addCustomColumn();
selectExtractColumn();
cy.findAllByTestId("dimension-list-item")
.contains(extraction.column)
.click();
popover().findAllByRole("button").contains(extraction.name).click();
H.popover().findAllByRole("button").contains(extraction.name).click();
cy.findByTestId("expression-editor-textfield").should(
"contain",
`${extraction.fn}(`,
);
expressionEditorWidget()
H.expressionEditorWidget()
.findByTestId("expression-name")
.should("have.value", extraction.name);
});
}
it("should be possible to create the same extraction multiple times", () => {
openOrdersTable({ mode: "notebook", limit: 5 });
addCustomColumn();
H.openOrdersTable({ mode: "notebook", limit: 5 });
H.addCustomColumn();
selectExtractColumn();
cy.findAllByTestId("dimension-list-item").contains("Created At").click();
popover().findAllByRole("button").contains("Hour of day").click();
H.popover().findAllByRole("button").contains("Hour of day").click();
expressionEditorWidget()
H.expressionEditorWidget()
.findByTestId("expression-name")
.should("have.value", "Hour of day");
expressionEditorWidget().button("Done").click();
H.expressionEditorWidget().button("Done").click();
cy.findAllByTestId("notebook-cell-item").last().click();
selectExtractColumn();
cy.findAllByTestId("dimension-list-item").contains("Created At").click();
popover().findAllByRole("button").contains("Hour of day").click();
H.popover().findAllByRole("button").contains("Hour of day").click();
expressionEditorWidget()
H.expressionEditorWidget()
.findByTestId("expression-name")
.should("have.value", "Hour of day (1)");
});
it("should be possible to edit a previous stages' columns when an aggregation is present (metabase#43226)", () => {
openOrdersTable({ mode: "notebook", limit: 5 });
H.openOrdersTable({ mode: "notebook", limit: 5 });
cy.button("Summarize").click();
popover().findByText("Count of rows").click();
H.popover().findByText("Count of rows").click();
addCustomColumn();
H.addCustomColumn();
selectExtractColumn();
cy.findAllByTestId("dimension-list-item").contains("Created At").click();
popover().findAllByRole("button").contains("Hour of day").click();
H.popover().findAllByRole("button").contains("Hour of day").click();
expressionEditorWidget()
H.expressionEditorWidget()
.findByTestId("expression-name")
.should("have.value", "Hour of day");
});
});
describeWithSnowplow(
H.describeWithSnowplow(
"scenarios > question > custom column > expression shortcuts > extract",
() => {
beforeEach(() => {
restore();
resetSnowplow();
H.restore();
H.resetSnowplow();
cy.signInAsNormalUser();
});
afterEach(() => {
expectNoBadSnowplowEvents();
H.expectNoBadSnowplowEvents();
});
it("should track column extraction via shortcut", () => {
openTable({ mode: "notebook", limit: 1, table: ORDERS_ID });
addCustomColumn();
H.openTable({ mode: "notebook", limit: 1, table: ORDERS_ID });
H.addCustomColumn();
selectExtractColumn();
cy.findAllByTestId("dimension-list-item").contains("Created At").click();
popover().findAllByRole("button").contains("Hour of day").click();
H.popover().findAllByRole("button").contains("Hour of day").click();
expectGoodSnowplowEvent({
H.expectGoodSnowplowEvent({
event: "column_extract_via_shortcut",
custom_expressions_used: ["get-hour"],
database_id: SAMPLE_DB_ID,
@@ -236,28 +225,28 @@ describeWithSnowplow(
describe("scenarios > question > custom column > expression shortcuts > combine", () => {
function addColumn() {
expressionEditorWidget().within(() => {
H.expressionEditorWidget().within(() => {
cy.findByText("Add column").click();
});
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsNormalUser();
});
it("should be possible to select a combine columns shortcut", () => {
openOrdersTable({ mode: "notebook", limit: 5 });
addCustomColumn();
H.openOrdersTable({ mode: "notebook", limit: 5 });
H.addCustomColumn();
selectCombineColumns();
selectColumn(0, "Total");
expressionEditorWidget().findByText("Total").should("exist");
H.expressionEditorWidget().findByText("Total").should("exist");
selectColumn(1, "Product", "Rating");
expressionEditorWidget().within(() => {
H.expressionEditorWidget().within(() => {
cy.findByText("Product → Rating").should("exist");
cy.findByTestId("combine-example").should(
@@ -287,14 +276,14 @@ describe("scenarios > question > custom column > expression shortcuts > combine"
});
it("should be possible to cancel when using the combine column shortcut", () => {
openOrdersTable({ mode: "notebook" });
addCustomColumn();
H.openOrdersTable({ mode: "notebook" });
H.addCustomColumn();
selectCombineColumns();
selectColumn(0, "Total");
selectColumn(1, "Product", "Rating");
expressionEditorWidget().within(() => {
H.expressionEditorWidget().within(() => {
// Click the back button, in the header
cy.findByText("Select columns to combine").click();
});
@@ -304,8 +293,8 @@ describe("scenarios > question > custom column > expression shortcuts > combine"
});
it("should be possible to add and remove more than one column", () => {
openOrdersTable({ mode: "notebook" });
addCustomColumn();
H.openOrdersTable({ mode: "notebook" });
H.addCustomColumn();
selectCombineColumns();
selectColumn(0, "Total");
@@ -327,13 +316,13 @@ describe("scenarios > question > custom column > expression shortcuts > combine"
});
it("should pick the correct default separator based on the type of the first column", () => {
openOrdersTable({ mode: "notebook" });
addCustomColumn();
H.openOrdersTable({ mode: "notebook" });
H.addCustomColumn();
selectCombineColumns();
selectColumn(0, "User", "Email");
expressionEditorWidget().within(() => {
H.expressionEditorWidget().within(() => {
cy.findByText("Separated by (empty)").should("exist");
cy.findByText(/Separated by/).click();
@@ -342,18 +331,18 @@ describe("scenarios > question > custom column > expression shortcuts > combine"
});
it("should be possible to edit a previous stages' columns when there is an aggregation (metabase#43226)", () => {
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
cy.button("Summarize").click();
popover().findByText("Count of rows").click();
H.popover().findByText("Count of rows").click();
addCustomColumn();
H.addCustomColumn();
selectCombineColumns();
selectColumn(0, "User", "Email");
expressionEditorWidget().within(() => {
H.expressionEditorWidget().within(() => {
cy.findByText("Separated by (empty)").should("exist");
cy.findByText(/Separated by/).click();
@@ -362,30 +351,30 @@ describe("scenarios > question > custom column > expression shortcuts > combine"
});
});
describeWithSnowplow(
H.describeWithSnowplow(
"scenarios > question > custom column > combine shortcuts",
() => {
beforeEach(() => {
restore();
resetSnowplow();
H.restore();
H.resetSnowplow();
cy.signInAsNormalUser();
});
afterEach(() => {
expectNoBadSnowplowEvents();
H.expectNoBadSnowplowEvents();
});
it("should send an event for combine columns", () => {
openOrdersTable({ mode: "notebook" });
addCustomColumn();
H.openOrdersTable({ mode: "notebook" });
H.addCustomColumn();
selectCombineColumns();
selectColumn(0, "User", "Email");
selectColumn(1, "User", "Email");
expressionEditorWidget().button("Done").click();
H.expressionEditorWidget().button("Done").click();
expectGoodSnowplowEvent({
H.expectGoodSnowplowEvent({
event: "column_combine_via_shortcut",
custom_expressions_used: ["concat"],
database_id: SAMPLE_DB_ID,

View File

@@ -1,28 +1,22 @@
import {
enterCustomColumnDetails,
openProductsTable,
popover,
restore,
summarize,
} from "e2e/support/helpers";
import { H } from "e2e/support";
describe("scenarios > question > custom column > typing suggestion", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
openProductsTable({ mode: "notebook" });
H.openProductsTable({ mode: "notebook" });
});
it("should not suggest arithmetic operators", () => {
addCustomColumn();
enterCustomColumnDetails({ formula: "[Price] " });
H.enterCustomColumnDetails({ formula: "[Price] " });
cy.findByTestId("expression-suggestions-list").should("not.exist");
});
it("should correctly accept the chosen field suggestion", () => {
addCustomColumn();
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: "[Rating]{leftarrow}{leftarrow}{leftarrow}",
blur: false,
});
@@ -38,7 +32,7 @@ describe("scenarios > question > custom column > typing suggestion", () => {
it("should correctly accept the chosen function suggestion", () => {
addCustomColumn();
enterCustomColumnDetails({ formula: "LTRIM([Title])", blur: false });
H.enterCustomColumnDetails({ formula: "LTRIM([Title])", blur: false });
// Place the cursor between "is" and "empty"
cy.get("@formula").type("{leftarrow}".repeat(13));
@@ -52,7 +46,7 @@ describe("scenarios > question > custom column > typing suggestion", () => {
it("should correctly insert function suggestion with the opening parenthesis", () => {
addCustomColumn();
enterCustomColumnDetails({ formula: "BET{enter}" });
H.enterCustomColumnDetails({ formula: "BET{enter}" });
cy.findByTestId("expression-editor-textfield").should(
"contain",
@@ -62,7 +56,7 @@ describe("scenarios > question > custom column > typing suggestion", () => {
it("should show expression function helper if a proper function is typed", () => {
addCustomColumn();
enterCustomColumnDetails({ formula: "lower(", blur: false });
H.enterCustomColumnDetails({ formula: "lower(", blur: false });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains("lower(text)");
@@ -84,10 +78,10 @@ describe("scenarios > question > custom column > typing suggestion", () => {
});
it("should not show suggestions for an unfocused field (metabase#31643)", () => {
summarize({ mode: "notebook" });
popover().findByText("Custom Expression").click();
enterCustomColumnDetails({ formula: "Count{enter}" });
popover().findByLabelText("Name").focus();
H.summarize({ mode: "notebook" });
H.popover().findByText("Custom Expression").click();
H.enterCustomColumnDetails({ formula: "Count{enter}" });
H.popover().findByLabelText("Name").focus();
cy.findByTestId("expression-suggestions-list").should("not.exist");
});
});

View File

@@ -1,43 +1,6 @@
import { H } from "e2e/support";
import { SAMPLE_DB_ID, WRITABLE_DB_ID } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
addCustomColumn,
addOrUpdateDashboardCard,
cartesianChartCircle,
createNativeQuestion,
createQuestion,
createSegment,
editDashboard,
enterCustomColumnDetails,
entityPickerModal,
entityPickerModalTab,
filter,
filterWidget,
getDashboardCard,
getNotebookStep,
main,
modal,
multiAutocompleteInput,
openNotebook,
openOrdersTable,
openProductsTable,
popover,
queryBuilderMain,
resetTestTable,
restore,
saveDashboard,
selectDashboardFilter,
selectFilterOperator,
setFilter,
startNewQuestion,
summarize,
tableHeaderClick,
visitDashboard,
visitQuestion,
visitQuestionAdhoc,
visualize,
withDatabase,
} from "e2e/support/helpers";
const { PRODUCTS, PRODUCTS_ID, ORDERS, ORDERS_ID, REVIEWS, REVIEWS_ID } =
SAMPLE_DATABASE;
@@ -46,12 +9,12 @@ describe.skip("issue 12445", { tags: "@external" }, () => {
const CC_NAME = "Abbr";
beforeEach(() => {
restore("mysql-8");
H.restore("mysql-8");
cy.signInAsAdmin();
});
it("should correctly apply substring for a custom column (metabase#12445)", () => {
withDatabase(2, ({ PEOPLE, PEOPLE_ID }) => {
H.withDatabase(2, ({ PEOPLE, PEOPLE_ID }) => {
cy.log("Create a question with `Source` column and abbreviated CC");
cy.createQuestion(
{
@@ -85,39 +48,39 @@ describe("issue 13289", () => {
beforeEach(() => {
cy.intercept("POST", "/api/dataset").as("dataset");
restore();
H.restore();
cy.signInAsAdmin();
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Custom column").click();
// Add custom column that will be used later in summarize (group by)
enterCustomColumnDetails({ formula: "1 + 1", name: CC_NAME });
H.enterCustomColumnDetails({ formula: "1 + 1", name: CC_NAME });
cy.button("Done").click();
});
it("should allow 'zoom in' drill-through when grouped by custom column (metabase#13289) (metabase#13289)", () => {
summarize({ mode: "notebook" });
H.summarize({ mode: "notebook" });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Count of rows").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Pick a column to group by").click();
popover().findByText(CC_NAME).click();
H.popover().findByText(CC_NAME).click();
cy.icon("add").last().click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("Created At").click();
});
visualize();
H.visualize();
cy.findByTestId("query-visualization-root").within(() => {
cartesianChartCircle()
H.cartesianChartCircle()
.eq(5) // random circle in the graph (there is no specific reason for this index)
.click();
});
@@ -139,38 +102,38 @@ describe("issue 13751", { tags: "@external" }, () => {
const PG_DB_NAME = "QA Postgres12";
beforeEach(() => {
restore("postgres-12");
H.restore("postgres-12");
cy.signInAsAdmin();
startNewQuestion();
entityPickerModal().within(() => {
entityPickerModalTab("Tables").click();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText(PG_DB_NAME).should("be.visible").click();
cy.findByTextEnsureVisible("People").click();
});
});
it("should allow using strings in filter based on a custom column (metabase#13751)", () => {
addCustomColumn();
enterCustomColumnDetails({
H.addCustomColumn();
H.enterCustomColumnDetails({
formula: 'regexextract([State], "^C[A-Z]")',
name: CC_NAME,
});
cy.button("Done").click();
getNotebookStep("filter")
H.getNotebookStep("filter")
.findByText(/Add filter/)
.click();
popover().findByText(CC_NAME).click();
selectFilterOperator("Is");
popover().within(() => {
H.popover().findByText(CC_NAME).click();
H.selectFilterOperator("Is");
H.popover().within(() => {
cy.findByPlaceholderText("Enter some text").type("CO");
cy.button("Add filter").click();
});
visualize();
H.visualize();
queryBuilderMain().findByText("Arnold Adams").should("be.visible");
H.queryBuilderMain().findByText("Arnold Adams").should("be.visible");
});
});
@@ -185,10 +148,10 @@ describe.skip(
const ESCAPED_REGEX = "(?<=\\/\\/)[^\\/]*";
beforeEach(() => {
restore("postgres-12");
H.restore("postgres-12");
cy.signInAsAdmin();
startNewQuestion();
H.startNewQuestion();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(PG_DB_NAME).should("be.visible").click();
cy.findByTextEnsureVisible("People").click();
@@ -197,7 +160,7 @@ describe.skip(
it("should not remove regex escape characters (metabase#14517)", () => {
cy.log("Create custom column using `regexextract()`");
cy.findByLabelText("Custom Column").click();
popover().within(() => {
H.popover().within(() => {
cy.get("[contenteditable='true']")
.type(`regexextract([State], "${ESCAPED_REGEX}")`)
.blur();
@@ -225,23 +188,23 @@ describe("issue 14843", () => {
beforeEach(() => {
cy.intercept("POST", "/api/dataset").as("dataset");
restore();
H.restore();
cy.signInAsAdmin();
});
it("should correctly filter custom column by 'Not equal to' (metabase#14843)", () => {
cy.createQuestion(questionDetails, { visitQuestion: true });
openNotebook();
H.openNotebook();
filter({ mode: "notebook" });
popover().findByText(CC_NAME).click();
selectFilterOperator("Not equal to");
popover().within(() => {
multiAutocompleteInput().type("3");
H.filter({ mode: "notebook" });
H.popover().findByText(CC_NAME).click();
H.selectFilterOperator("Not equal to");
H.popover().within(() => {
H.multiAutocompleteInput().type("3");
cy.button("Add filter").click();
});
visualize();
H.visualize();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(`${CC_NAME} is not equal to 3`);
@@ -266,7 +229,7 @@ describe("issue 18069", () => {
};
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.createQuestion(questionDetails).then(({ body: { id: QUESTION_ID } }) => {
@@ -275,11 +238,11 @@ describe("issue 18069", () => {
});
it("should not allow choosing text fields for SUM (metabase#18069)", () => {
summarize({ mode: "notebook" });
H.summarize({ mode: "notebook" });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Sum of ...").click();
popover().within(() => {
H.popover().within(() => {
// regular fields
cy.findByText("Price");
cy.findByText("Rating");
@@ -294,7 +257,7 @@ describe("issue 18069", () => {
cy.findByText("CC_ScaledRating").click();
});
visualize();
H.visualize();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("1,041.45");
@@ -312,26 +275,26 @@ describe("issue 18747", () => {
},
};
function addNumberParameterToDashboard() {
editDashboard();
H.editDashboard();
setFilter("Number", "Equal to");
H.setFilter("Number", "Equal to");
}
function mapParameterToCustomColumn() {
cy.findByTestId("dashcard-container").contains("Select…").click();
popover().contains("Quantity_2").click({ force: true });
H.popover().contains("Quantity_2").click({ force: true });
}
function addValueToParameterFilter() {
filterWidget().click();
popover().within(() => {
H.filterWidget().click();
H.popover().within(() => {
cy.findByRole("searchbox").type("14");
cy.button("Add filter").click();
});
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.createQuestionAndDashboard({ questionDetails }).then(
@@ -348,7 +311,7 @@ describe("issue 18747", () => {
},
],
}).then(() => {
visitDashboard(dashboard_id);
H.visitDashboard(dashboard_id);
});
},
);
@@ -394,14 +357,14 @@ describe("issue 18814", () => {
};
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.createQuestion(questionDetails, { visitQuestion: true });
});
it("should be able to use a custom column in aggregation for a nested query (metabase#18814)", () => {
openNotebook();
H.openNotebook();
cy.icon("sum").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -409,9 +372,9 @@ describe("issue 18814", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Pick a column to group by").click();
popover().contains(ccName).click();
H.popover().contains(ccName).click();
visualize();
H.visualize();
cy.findByTestId("query-visualization-root").should("contain", "2022");
});
@@ -445,7 +408,7 @@ describe("issue 19744", () => {
cy.findByText("Save").click();
cy.findByLabelText("Name").type(name);
modal().button("Save").click();
H.modal().button("Save").click();
cy.findByText("Not now").click();
@@ -457,23 +420,23 @@ describe("issue 19744", () => {
function addQuestionToDashboardAndVisit() {
cy.createDashboard().then(({ body: { id: dashboard_id } }) => {
cy.get("@questionId").then(card_id => {
addOrUpdateDashboardCard({
H.addOrUpdateDashboardCard({
card_id,
dashboard_id,
card: { size_x: 21, size_y: 10 },
});
});
visitDashboard(dashboard_id);
H.visitDashboard(dashboard_id);
});
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
// For this specific repro, it's crucial to first visit the question in order to load the `results_metadata`...
visitQuestionAdhoc(questionDetails);
H.visitQuestionAdhoc(questionDetails);
// ...and then to save it using the UI
saveQuestion("19744");
@@ -481,12 +444,12 @@ describe("issue 19744", () => {
});
it("custom column after aggregation shouldn't limit or change the behavior of dashboard filters (metabase#19744)", () => {
editDashboard();
H.editDashboard();
setFilter("Date picker", "All Options");
H.setFilter("Date picker", "All Options");
cy.findByTestId("dashcard-container").contains("Select…").click();
popover().contains("Created At");
H.popover().contains("Created At");
});
});
@@ -529,12 +492,12 @@ describe("issue 19745", () => {
function updateQuestionAndSelectFilter(updateExpressions) {
cy.createQuestionAndDashboard({ questionDetails, dashboardDetails }).then(
({ body: { card_id, dashboard_id } }) => {
visitQuestion(card_id);
H.visitQuestion(card_id);
// this should modify the query and remove the second stage
openNotebook();
H.openNotebook();
updateExpressions();
visualize();
H.visualize();
cy.findByTestId("viz-settings-button").click();
cy.findByRole("button", { name: "Add or remove columns" }).click();
cy.findByLabelText("Count").should("not.be.checked").click();
@@ -542,17 +505,17 @@ describe("issue 19745", () => {
// as we select all columns in the first stage of the query,
// it should be possible to map a filter to a selected column
visitDashboard(dashboard_id);
editDashboard();
H.visitDashboard(dashboard_id);
H.editDashboard();
cy.findByText("Date filter").click();
selectDashboardFilter(getDashboardCard(), "Created At");
saveDashboard();
H.selectDashboardFilter(H.getDashboardCard(), "Created At");
H.saveDashboard();
},
);
}
function removeExpression(name) {
getNotebookStep("expression", { stage: 1 }).within(() => {
H.getNotebookStep("expression", { stage: 1 }).within(() => {
cy.findByText(name).within(() => {
cy.icon("close").click();
});
@@ -560,7 +523,7 @@ describe("issue 19745", () => {
}
function removeAllExpressions() {
getNotebookStep("expression", { stage: 1 }).within(() => {
H.getNotebookStep("expression", { stage: 1 }).within(() => {
cy.findByLabelText("Remove step").click({ force: true });
});
}
@@ -575,7 +538,7 @@ describe("issue 19745", () => {
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -615,7 +578,7 @@ describe("issue 20229", () => {
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.createQuestion(questionDetails, { visitQuestion: true });
@@ -625,14 +588,14 @@ describe("issue 20229", () => {
ccAssertion();
// Switch to the notebook view to deselect at least one column
openNotebook();
H.openNotebook();
cy.findAllByTestId("fields-picker").click();
popover().within(() => {
H.popover().within(() => {
unselectColumn("Tax");
});
visualize();
H.visualize();
ccAssertion();
});
@@ -657,10 +620,10 @@ describe("issue 21135", () => {
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.createQuestion(questionDetails, { visitQuestion: true });
openNotebook();
H.openNotebook();
});
it("should handle cc with the same name as the table column (metabase#21135)", () => {
@@ -684,22 +647,22 @@ describe("issue 21135", () => {
describe("issue 21513", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
it("should handle cc with the same name as an aggregation function (metabase#21513)", () => {
openProductsTable({ mode: "notebook" });
summarize({ mode: "notebook" });
popover().findByText("Count of rows").click();
H.openProductsTable({ mode: "notebook" });
H.summarize({ mode: "notebook" });
H.popover().findByText("Count of rows").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Pick a column to group by").click();
popover().findByText("Category").click();
H.popover().findByText("Category").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Custom column").click();
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: "[Count] * 2",
name: "Double Count",
});
@@ -727,13 +690,13 @@ describe("issue 23862", () => {
};
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
it("should group by a custom column and work in a nested question (metabase#23862)", () => {
cy.createQuestion(questionDetails).then(({ body: { id } }) => {
visitQuestionAdhoc(
H.visitQuestionAdhoc(
{
dataset_query: {
type: "query",
@@ -775,20 +738,20 @@ describe("issue 24922", () => {
};
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
createSegment(segmentDetails);
H.createSegment(segmentDetails);
});
it("should allow segments in case custom expressions (metabase#24922)", () => {
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Custom column").click();
enterCustomColumnDetails(customColumnDetails);
H.enterCustomColumnDetails(customColumnDetails);
cy.button("Done").click();
visualize();
H.visualize();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("CustomColumn").should("be.visible");
});
@@ -819,7 +782,7 @@ describe.skip("issue 25189", () => {
beforeEach(() => {
cy.intercept("POST", "/api/dataset").as("dataset");
restore();
H.restore();
cy.signInAsAdmin();
cy.createQuestion(questionDetails).then(
@@ -842,8 +805,8 @@ describe.skip("issue 25189", () => {
.and("contain", ccTable);
// 2. We shouldn't see duplication in the bulk filter modal
filter();
modal().within(() => {
H.filter();
H.modal().within(() => {
// Implicit assertion - will fail if more than one element is found
cy.findByText(ccFunction);
cy.findByText(ccTable);
@@ -857,7 +820,7 @@ describe.skip("issue 25189", () => {
cy.findByText("No results!");
// 3. We shouldn't see duplication in the breakout fields
summarize();
H.summarize();
cy.findByTestId("sidebar-content").within(() => {
// Another implicit assertion
cy.findByText(ccFunction);
@@ -871,18 +834,18 @@ describe.skip("issue 25189", () => {
const tableName = "colors27745";
beforeEach(() => {
restore(`${dialect}-writable`);
H.restore(`${dialect}-writable`);
cy.signInAsAdmin();
resetTestTable({ type: dialect, table: tableName });
H.resetTestTable({ type: dialect, table: tableName });
cy.request("POST", `/api/database/${WRITABLE_DB_ID}/sync_schema`);
cy.intercept("GET", "/api/search*").as("search");
});
it("should display all summarize options if the only numeric field is a custom column (metabase#27745)", () => {
startNewQuestion();
H.startNewQuestion();
entityPickerModal().within(() => {
H.entityPickerModal().within(() => {
cy.findByPlaceholderText("Search this collection or everywhere…").type(
"colors",
);
@@ -893,16 +856,16 @@ describe.skip("issue 25189", () => {
.click();
});
cy.findByLabelText("Custom column").click();
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: "case([ID] > 1, 25, 5)",
name: "Numeric",
});
cy.button("Done").click();
visualize();
H.visualize();
tableHeaderClick("Numeric");
popover().findByText(/^Sum$/).click();
H.tableHeaderClick("Numeric");
H.popover().findByText(/^Sum$/).click();
cy.wait("@dataset");
cy.findByTestId("scalar-value").invoke("text").should("eq", "55");
@@ -933,7 +896,7 @@ describe("issue 32032", () => {
};
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.createQuestion({ query: QUERY }, { visitQuestion: true });
cy.intercept("POST", "/api/dataset").as("dataset");
@@ -944,9 +907,9 @@ describe("issue 32032", () => {
.findAllByText("xavier")
.eq(1)
.click();
popover().findByText("Is xavier").click();
H.popover().findByText("Is xavier").click();
cy.wait("@dataset");
main()
H.main()
.findByText(/There was a problem/i)
.should("not.exist");
cy.findByTestId("TableInteractive-root")
@@ -957,12 +920,12 @@ describe("issue 32032", () => {
describe("issue 42949", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
it("should correctly show available shortcuts for date and number columns (metabase#42949)", () => {
createNativeQuestion(
H.createNativeQuestion(
{
native: {
query: `
@@ -978,22 +941,22 @@ describe("issue 42949", () => {
cy.findByTestId("qb-header").findByText("Explore results").click();
cy.log("Verify header drills - CREATED_AT");
tableHeaderClick("CREATED_AT");
popover().findByText("Extract day, month…").should("be.visible");
popover().findByText("Combine columns").should("not.exist");
H.tableHeaderClick("CREATED_AT");
H.popover().findByText("Extract day, month…").should("be.visible");
H.popover().findByText("Combine columns").should("not.exist");
cy.realPress("Escape");
cy.log("Verify header drills - V");
tableHeaderClick("V");
popover().findByText("Extract part of column").should("not.exist");
popover().findByText("Combine columns").should("not.exist");
H.tableHeaderClick("V");
H.popover().findByText("Extract part of column").should("not.exist");
H.popover().findByText("Combine columns").should("not.exist");
cy.realPress("Escape");
cy.log("Verify plus button - extract column");
cy.button("Add column").click();
popover().findByText("Extract part of column").click();
popover().findByText("CREATED_AT").click();
popover().within(() => {
H.popover().findByText("Extract part of column").click();
H.popover().findByText("CREATED_AT").click();
H.popover().within(() => {
cy.findByText("Day of month").should("be.visible");
cy.findByText("Day of week").should("be.visible");
cy.findByText("Month of year").should("be.visible");
@@ -1004,16 +967,16 @@ describe("issue 42949", () => {
cy.log("Verify plus button - combine columns");
cy.button("Add column").click();
popover().findByText("Combine columns").click();
popover().findAllByTestId("column-input").eq(0).click();
popover()
H.popover().findByText("Combine columns").click();
H.popover().findAllByTestId("column-input").eq(0).click();
H.popover()
.last()
.within(() => {
cy.findByText("CREATED_AT").should("be.visible");
cy.findByText("V").should("be.visible");
cy.findByText("Year").should("be.visible").click();
});
popover().button("Done").click();
H.popover().button("Done").click();
cy.findAllByTestId("header-cell")
.eq(3)
@@ -1028,7 +991,7 @@ describe("issue 42949", () => {
});
it("should correctly show available shortcuts for a number column (metabase#42949)", () => {
createNativeQuestion(
H.createNativeQuestion(
{
native: {
query: "select 1 as n",
@@ -1041,21 +1004,21 @@ describe("issue 42949", () => {
cy.findByLabelText("Switch to data").click();
cy.log("Verify header drills");
tableHeaderClick("N");
popover().findByText("Extract part of column").should("not.exist");
popover().findByText("Combine columns").should("not.exist");
H.tableHeaderClick("N");
H.popover().findByText("Extract part of column").should("not.exist");
H.popover().findByText("Combine columns").should("not.exist");
cy.realPress("Escape");
cy.log("Verify plus button");
cy.button("Add column").click();
popover().findByText("Extract part of column").should("not.exist");
popover().findByText("Combine columns").click();
popover().findAllByTestId("column-input").eq(0).click();
popover().last().findByText("N").should("be.visible");
H.popover().findByText("Extract part of column").should("not.exist");
H.popover().findByText("Combine columns").click();
H.popover().findAllByTestId("column-input").eq(0).click();
H.popover().last().findByText("N").should("be.visible");
});
it("should correctly show available shortcuts for a string column (metabase#42949)", () => {
createNativeQuestion(
H.createNativeQuestion(
{
native: {
query: "select 'abc'",
@@ -1068,30 +1031,30 @@ describe("issue 42949", () => {
cy.findByLabelText("Switch to data").click();
cy.log("Verify header drills");
tableHeaderClick("'abc'");
popover().findByText("Extract part of column").should("not.exist");
popover().findByText("Combine columns").should("be.visible");
H.tableHeaderClick("'abc'");
H.popover().findByText("Extract part of column").should("not.exist");
H.popover().findByText("Combine columns").should("be.visible");
cy.realPress("Escape");
cy.log("Verify plus button");
cy.button("Add column").click();
popover().findByText("Extract part of column").should("not.exist");
popover().findByText("Combine columns").click();
popover().findAllByTestId("column-input").eq(0).click();
popover().last().findByText("'abc'").should("be.visible");
H.popover().findByText("Extract part of column").should("not.exist");
H.popover().findByText("Combine columns").click();
H.popover().findAllByTestId("column-input").eq(0).click();
H.popover().last().findByText("'abc'").should("be.visible");
});
});
describe("issue 49342", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsNormalUser();
});
it("should be possible to leave the expression input with the Tab key (metabase#49342)", () => {
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
cy.findByLabelText("Custom column").click();
enterCustomColumnDetails({ formula: "[Tot{Enter}", blur: false });
H.enterCustomColumnDetails({ formula: "[Tot{Enter}", blur: false });
cy.get(".ace_text-input").first().focus().realPress("Tab");
cy.findByTestId("expression-name").should("be.focused");
@@ -1107,31 +1070,33 @@ describe("issue 49342", () => {
describe("issue 49882", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsNormalUser();
cy.intercept("POST", "/api/dataset/query_metadata").as("queryMetadata");
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
cy.wait("@queryMetadata");
cy.findByLabelText("Custom column").click();
});
it("should not eat up subsequent characters when applying a suggestion (metabase#49882-1)", () => {
const moveCursorTo2ndCaseArgument = "{leftarrow}".repeat(6);
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: `case([Total] > 200, , "X")${moveCursorTo2ndCaseArgument}[tot{enter}`,
});
cy.get(".ace_text-input")
.first()
.should("have.value", 'case([Total] > 200, [Total] , "X")\n\n');
popover().findByText("Expecting a closing parenthesis").should("not.exist");
H.popover()
.findByText("Expecting a closing parenthesis")
.should("not.exist");
});
it("does not clear expression input when expression is invalid (metabase#49882-2)", () => {
const selectTax = `{leftarrow}${"{shift+leftarrow}".repeat(5)}`;
const moveCursorBefore2ndCase = "{leftarrow}".repeat(41);
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: `case([Tax] > 1, case([Total] > 200, [Total], "Nothing"), [Tax])${selectTax}`,
});
cy.get(".ace_text-input").first().focus().realPress(["Control", "X"]);
@@ -1150,11 +1115,11 @@ describe("issue 49882", () => {
'case([Tax] > 1, [Tax] case([Total] > 200, [Total], "Nothing"), )\n\n',
);
popover().findByText("Invalid expression").should("be.visible");
H.popover().findByText("Invalid expression").should("be.visible");
});
it("should allow moving cursor between wrapped lines with arrow up and arrow down keys (metabase#49882-3)", () => {
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula:
'case([Tax] > 1, case([Total] > 200, [Total], "Nothing"), [Tax]){leftarrow}{leftarrow}{uparrow}x{downarrow}y',
});
@@ -1170,7 +1135,7 @@ describe("issue 49882", () => {
it("should update currently selected suggestion when suggestions list is updated (metabase#49882-4)", () => {
const selectProductVendor =
"{downarrow}{downarrow}{downarrow}{downarrow}{downarrow}{enter}";
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: `[Product${selectProductVendor}{leftarrow}{leftarrow}`,
blur: false,
});
@@ -1189,28 +1154,28 @@ describe("issue 49304", () => {
};
beforeEach(() => {
restore();
H.restore();
cy.signInAsNormalUser();
});
it("should be possible to switch between filter widgets and the expression editor for multi-argument operators (metabase#49304)", () => {
createQuestion(questionDetails, { visitQuestion: true });
openNotebook();
H.createQuestion(questionDetails, { visitQuestion: true });
H.openNotebook();
cy.log(
"add a filter using a filter widget and check that it is rendered in the expression editor",
);
getNotebookStep("data").button("Filter").click();
popover().findByText("Category").click();
selectFilterOperator("Contains");
popover().within(() => {
H.getNotebookStep("data").button("Filter").click();
H.popover().findByText("Category").click();
H.selectFilterOperator("Contains");
H.popover().within(() => {
cy.findByPlaceholderText("Enter some text").type("gadget,widget");
cy.button("Add filter").click();
});
getNotebookStep("filter")
H.getNotebookStep("filter")
.findByText("Category contains 2 selections")
.click();
popover().within(() => {
H.popover().within(() => {
cy.button("Back").click();
cy.findByText("Custom Expression").click();
cy.get(".ace_content").should(
@@ -1222,17 +1187,17 @@ describe("issue 49304", () => {
cy.log(
"modify the expression in the expression editor and make sure it is rendered correctly in the filter widget",
);
popover().within(() => {
enterCustomColumnDetails({
H.popover().within(() => {
H.enterCustomColumnDetails({
formula:
'contains([Category], "gadget", "widget", "gizmo", "case-insensitive")',
});
cy.button("Done").click();
});
getNotebookStep("filter")
H.getNotebookStep("filter")
.findByText("Category contains 3 selections")
.click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("gadget").should("be.visible");
cy.findByText("widget").should("be.visible");
cy.findByText("gizmo").should("be.visible");
@@ -1242,14 +1207,14 @@ describe("issue 49304", () => {
cy.log(
"change options in the filter widget and make sure they get reflected in the expression editor",
);
popover().within(() => {
H.popover().within(() => {
cy.findByLabelText("Case sensitive").click();
cy.button("Update filter").click();
});
getNotebookStep("filter")
H.getNotebookStep("filter")
.findByText("Category contains 3 selections")
.click();
popover().within(() => {
H.popover().within(() => {
cy.button("Back").click();
cy.findByText("Custom Expression").click();
cy.get(".ace_content").should(
@@ -1261,16 +1226,16 @@ describe("issue 49304", () => {
cy.log(
"remove options from the expression in the expression editor and make sure it is rendered correctly in the filter widget",
);
popover().within(() => {
enterCustomColumnDetails({
H.popover().within(() => {
H.enterCustomColumnDetails({
formula: 'contains([Category], "gadget", "widget", "gizmo")',
});
cy.button("Done").click();
});
getNotebookStep("filter")
H.getNotebookStep("filter")
.findByText("Category contains 3 selections")
.click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("gadget").should("be.visible");
cy.findByText("widget").should("be.visible");
cy.findByText("gizmo").should("be.visible");

View File

@@ -1,28 +1,6 @@
import { H } from "e2e/support";
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
addCustomColumn,
cartesianChartCircle,
checkExpressionEditorHelperPopoverPosition,
createQuestion,
enterCustomColumnDetails,
entityPickerModal,
entityPickerModalTab,
filter,
getNotebookStep,
openOrdersTable,
openPeopleTable,
openProductsTable,
openTable,
popover,
queryBuilderMain,
restore,
startNewQuestion,
summarize,
tableHeaderClick,
visitQuestionAdhoc,
visualize,
} from "e2e/support/helpers";
const { ORDERS, ORDERS_ID, PRODUCTS, PRODUCTS_ID, PEOPLE_ID } = SAMPLE_DATABASE;
@@ -30,7 +8,7 @@ describe("scenarios > question > custom column", () => {
beforeEach(() => {
cy.intercept("POST", "/api/dataset").as("dataset");
restore();
H.restore();
cy.signInAsNormalUser();
});
@@ -51,24 +29,24 @@ describe("scenarios > question > custom column", () => {
},
{ visitQuestion: true },
);
cartesianChartCircle().eq(5).click();
popover()
H.cartesianChartCircle().eq(5).click();
H.popover()
.findByText(/Automatic Insights/i)
.click();
popover().findByText(/X-ray/i);
popover()
H.popover().findByText(/X-ray/i);
H.popover()
.findByText(/Compare to the rest/i)
.click();
});
it("can create a custom column (metabase#13241)", () => {
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
cy.findByLabelText("Custom column").click();
enterCustomColumnDetails({ formula: "1 + 1", name: "Math" });
H.enterCustomColumnDetails({ formula: "1 + 1", name: "Math" });
cy.button("Done").click();
visualize();
H.visualize();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("There was a problem with your question").should("not.exist");
@@ -77,15 +55,15 @@ describe("scenarios > question > custom column", () => {
it("should not show default period in date column name (metabase#36631)", () => {
const name = "Base question";
createQuestion({ name, query: { "source-table": ORDERS_ID } });
H.createQuestion({ name, query: { "source-table": ORDERS_ID } });
startNewQuestion();
entityPickerModal().within(() => {
entityPickerModalTab("Saved questions").click();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Saved questions").click();
cy.findByText(name).click();
});
cy.button("Custom column").click();
enterCustomColumnDetails({ formula: "[cre", blur: false });
H.enterCustomColumnDetails({ formula: "[cre", blur: false });
cy.findAllByTestId("expression-suggestions-list-item")
.should("have.length", 1)
@@ -94,23 +72,23 @@ describe("scenarios > question > custom column", () => {
});
it("should not show binning for a numeric custom column", () => {
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
cy.findByLabelText("Custom column").click();
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: "[Product.Price] / 2",
name: "Half Price",
});
cy.button("Done").click();
cy.button("Summarize").click();
popover().findByText("Count of rows").click();
H.popover().findByText("Count of rows").click();
getNotebookStep("summarize")
H.getNotebookStep("summarize")
.findByText("Pick a column to group by")
.click();
popover()
H.popover()
.findByRole("option", { name: "Half Price" })
.within(() => {
cy.findByLabelText("Binning strategy").should("not.exist");
@@ -118,26 +96,28 @@ describe("scenarios > question > custom column", () => {
})
.click();
getNotebookStep("summarize").findByText("Half Price").should("be.visible");
H.getNotebookStep("summarize")
.findByText("Half Price")
.should("be.visible");
});
it("should show temporal units for a date/time custom column", () => {
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
cy.findByLabelText("Custom column").click();
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: "[Product.Created At]",
name: "Product Date",
});
cy.button("Done").click();
cy.button("Summarize").click();
popover().findByText("Count of rows").click();
H.popover().findByText("Count of rows").click();
getNotebookStep("summarize")
H.getNotebookStep("summarize")
.findByText("Pick a column to group by")
.click();
popover()
H.popover()
.findByRole("option", { name: "Product Date" })
.within(() => {
cy.findByLabelText("Binning strategy").should("not.exist");
@@ -145,28 +125,28 @@ describe("scenarios > question > custom column", () => {
})
.click();
getNotebookStep("summarize")
H.getNotebookStep("summarize")
.findByText("Product Date: Month")
.should("be.visible");
});
it("should not show binning options for a coordinate custom column", () => {
openPeopleTable({ mode: "notebook" });
H.openPeopleTable({ mode: "notebook" });
cy.findByLabelText("Custom column").click();
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: "[Latitude]",
name: "UserLAT",
});
cy.button("Done").click();
cy.button("Summarize").click();
popover().findByText("Count of rows").click();
H.popover().findByText("Count of rows").click();
getNotebookStep("summarize")
H.getNotebookStep("summarize")
.findByText("Pick a column to group by")
.click();
popover()
H.popover()
.findByRole("option", { name: "UserLAT" })
.within(() => {
cy.findByLabelText("Binning strategy").should("not.exist");
@@ -174,26 +154,26 @@ describe("scenarios > question > custom column", () => {
})
.click();
getNotebookStep("summarize").findByText("UserLAT").should("be.visible");
H.getNotebookStep("summarize").findByText("UserLAT").should("be.visible");
});
// flaky test (#19454)
it.skip("should show info popovers when hovering over custom column dimensions in the summarize sidebar", () => {
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
cy.findByLabelText("Custom column").click();
enterCustomColumnDetails({ formula: "1 + 1", name: "Math" });
H.enterCustomColumnDetails({ formula: "1 + 1", name: "Math" });
cy.button("Done").click();
visualize();
H.visualize();
summarize();
H.summarize();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Group by").parent().findByText("Math").trigger("mouseenter");
popover().contains("Math");
popover().contains("No description");
H.popover().contains("Math");
H.popover().contains("No description");
});
it("can create a custom column with an existing column name", () => {
@@ -209,24 +189,24 @@ describe("scenarios > question > custom column", () => {
];
customFormulas.forEach(({ formula, name }) => {
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
cy.findByLabelText("Custom column").click();
enterCustomColumnDetails({ formula, name });
H.enterCustomColumnDetails({ formula, name });
cy.button("Done").click();
visualize();
H.visualize();
cy.findByTestId("query-visualization-root").contains(name);
});
});
it("should create custom column with fields from aggregated data (metabase#12762)", () => {
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
summarize({ mode: "notebook" });
H.summarize({ mode: "notebook" });
popover().within(() => {
H.popover().within(() => {
cy.findByText("Sum of ...").click();
cy.findByText("Subtotal").click();
});
@@ -237,7 +217,7 @@ describe("scenarios > question > custom column", () => {
.last() // This is brittle.
.click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("Sum of ...").click();
cy.findByText("Total").click();
});
@@ -252,13 +232,13 @@ describe("scenarios > question > custom column", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Custom column").click();
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: "[Sum of Subtotal] + [Sum of Total]",
name: columnName,
});
cy.button("Done").click();
visualize();
H.visualize();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("There was a problem with your question").should("not.exist");
@@ -268,23 +248,23 @@ describe("scenarios > question > custom column", () => {
});
it("should not return same results for columns with the same name (metabase#12649)", () => {
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
// join with Products
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Join data").click();
entityPickerModal().within(() => {
entityPickerModalTab("Tables").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText("Products").click();
});
// add custom column
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Custom column").click();
enterCustomColumnDetails({ formula: "1 + 1", name: "x" });
H.enterCustomColumnDetails({ formula: "1 + 1", name: "x" });
cy.button("Done").click();
visualize();
H.visualize();
cy.log(
"**Fails in 0.35.0, 0.35.1, 0.35.2, 0.35.4 and the latest master (2026-10-21)**",
@@ -365,7 +345,7 @@ describe("scenarios > question > custom column", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains(`Sum of ${CC_NAME}`);
cartesianChartCircle().should("have.length.of.at.least", 8);
H.cartesianChartCircle().should("have.length.of.at.least", 8);
});
it("should create custom column after aggregation with 'cum-sum/count' (metabase#13634)", () => {
@@ -496,7 +476,7 @@ describe("scenarios > question > custom column", () => {
cy.log("Reported failing on 0.38.1-SNAPSHOT (6d77f099)");
cy.findByTestId("step-expression-0-0").should("not.exist");
visualize(response => {
H.visualize(response => {
expect(response.body.error).to.not.exist;
});
@@ -507,7 +487,7 @@ describe("scenarios > question > custom column", () => {
it("should handle using `case()` when referencing the same column names (metabase#14854)", () => {
const CC_NAME = "CE with case";
visitQuestionAdhoc(
H.visitQuestionAdhoc(
{
dataset_query: {
type: "query",
@@ -555,15 +535,15 @@ describe("scenarios > question > custom column", () => {
}).then(({ body: { id: QUESTION_ID } }) => {
cy.visit(`/question/${QUESTION_ID}/notebook`);
});
summarize({ mode: "notebook" });
popover().within(() => {
H.summarize({ mode: "notebook" });
H.popover().within(() => {
cy.findByText("Sum of ...").click();
cy.findByText("MyCC [2027]").click();
});
cy.findAllByTestId("notebook-cell-item")
.contains("Sum of MyCC [2027]")
.click();
popover().within(() => {
H.popover().within(() => {
cy.icon("chevronleft").click();
cy.findByText("Custom Expression").click();
});
@@ -571,16 +551,16 @@ describe("scenarios > question > custom column", () => {
});
it.skip("should work with `isNull` function (metabase#15922)", () => {
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Custom column").click();
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: "isnull([Discount])",
name: "No discount",
});
cy.button("Done").click();
visualize(response => {
H.visualize(response => {
expect(response.body.error).to.not.exist;
});
@@ -591,7 +571,7 @@ describe("scenarios > question > custom column", () => {
});
it("should be able to add a date range filter to a custom column", () => {
visitQuestionAdhoc({
H.visitQuestionAdhoc({
display: "table",
dataset_query: {
database: SAMPLE_DB_ID,
@@ -603,9 +583,9 @@ describe("scenarios > question > custom column", () => {
},
});
tableHeaderClick("CustomDate");
H.tableHeaderClick("CustomDate");
popover().within(() => {
H.popover().within(() => {
cy.findByText("Filter by this column").click();
cy.findByText("Specific dates…").click();
cy.findByLabelText("Start date").clear().type("12/10/2024");
@@ -619,17 +599,17 @@ describe("scenarios > question > custom column", () => {
});
it("should work with relative date filter applied to a custom column (metabase#16273)", () => {
openOrdersTable({ mode: "notebook" });
addCustomColumn();
H.openOrdersTable({ mode: "notebook" });
H.addCustomColumn();
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: "case([Discount] > 0, [Created At], [Product → Created At])",
name: "MiscDate",
});
popover().button("Done").click();
H.popover().button("Done").click();
filter({ mode: "notebook" });
popover().within(() => {
H.filter({ mode: "notebook" });
H.popover().within(() => {
cy.findByText("MiscDate").click();
cy.findByText("Relative dates…").click();
cy.findByText("Previous").click();
@@ -637,26 +617,26 @@ describe("scenarios > question > custom column", () => {
});
cy.findByRole("listbox").findByText("years").click();
popover().within(() => {
H.popover().within(() => {
cy.findByText("Include this year").click();
cy.button("Add filter").click();
});
visualize(({ body }) => {
H.visualize(({ body }) => {
expect(body.error).to.not.exist;
});
queryBuilderMain().findByText("MiscDate").should("be.visible");
H.queryBuilderMain().findByText("MiscDate").should("be.visible");
cy.findByTestId("qb-filters-panel")
.findByText("MiscDate is in the previous 30 years")
.should("be.visible");
});
it("should allow switching focus with Tab", () => {
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
cy.findByLabelText("Custom column").click();
enterCustomColumnDetails({ formula: "1 + 2" });
H.enterCustomColumnDetails({ formula: "1 + 2" });
// next focus: the textbox for the name
cy.realPress("Tab");
@@ -671,10 +651,10 @@ describe("scenarios > question > custom column", () => {
});
it("should allow tabbing away from, then back to editor, while formatting expression and placing caret after reformatted expression", () => {
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
cy.findByLabelText("Custom column").click();
enterCustomColumnDetails({ formula: "1+1" });
H.enterCustomColumnDetails({ formula: "1+1" });
cy.realPress("Tab");
cy.realPress(["Shift", "Tab"]);
@@ -689,10 +669,10 @@ describe("scenarios > question > custom column", () => {
});
it("should allow choosing a suggestion with Tab", () => {
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
cy.findByLabelText("Custom column").click();
enterCustomColumnDetails({ formula: "[C", blur: false });
H.enterCustomColumnDetails({ formula: "[C", blur: false });
// Suggestion popover shows up and this select the first one ([Created At])
cy.realPress("Tab");
@@ -718,13 +698,13 @@ describe("scenarios > question > custom column", () => {
// TODO: fixme!
it.skip("should render custom expression helper near the custom expression field", () => {
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
cy.findByLabelText("Custom column").click();
popover().within(() => {
enterCustomColumnDetails({ formula: "floor" });
H.popover().within(() => {
H.enterCustomColumnDetails({ formula: "floor" });
checkExpressionEditorHelperPopoverPosition();
H.checkExpressionEditorHelperPopoverPosition();
});
});
});
@@ -733,24 +713,24 @@ describe("scenarios > question > custom column > data type", () => {
function addCustomColumns(columns) {
cy.wrap(columns).each((column, index) => {
if (index) {
getNotebookStep("expression").icon("add").click();
H.getNotebookStep("expression").icon("add").click();
} else {
cy.findByLabelText("Custom column").click();
}
enterCustomColumnDetails(column);
H.enterCustomColumnDetails(column);
cy.button("Done").click({ force: true });
});
}
function openCustomColumnInTable(table) {
openTable({ table, mode: "notebook" });
H.openTable({ table, mode: "notebook" });
cy.findByText("Custom column").click();
}
beforeEach(() => {
restore();
restore("postgres-12");
H.restore();
H.restore("postgres-12");
cy.signInAsAdmin();
});
@@ -758,16 +738,16 @@ describe("scenarios > question > custom column > data type", () => {
it("should understand string functions (metabase#13217)", () => {
openCustomColumnInTable(PRODUCTS_ID);
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: "concat([Category], [Title])",
name: "CategoryTitle",
});
cy.button("Done").click();
filter({ mode: "notebook" });
H.filter({ mode: "notebook" });
popover().within(() => {
H.popover().within(() => {
cy.findByText("CategoryTitle").click();
cy.findByPlaceholderText("Enter a number").should("not.exist");
cy.findByPlaceholderText("Enter some text").should("be.visible");
@@ -775,9 +755,9 @@ describe("scenarios > question > custom column > data type", () => {
});
it("should understand date functions", () => {
startNewQuestion();
entityPickerModal().within(() => {
entityPickerModalTab("Tables").click();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText("QA Postgres12").click();
cy.findByText("Orders").click();
});
@@ -810,17 +790,17 @@ describe("scenarios > question > custom column > data type", () => {
},
]);
visualize();
H.visualize();
});
it("should relay the type of a date field", () => {
openCustomColumnInTable(PEOPLE_ID);
enterCustomColumnDetails({ formula: "[Birth Date]", name: "DoB" });
H.enterCustomColumnDetails({ formula: "[Birth Date]", name: "DoB" });
cy.button("Done").click();
filter({ mode: "notebook" });
popover().within(() => {
H.filter({ mode: "notebook" });
H.popover().within(() => {
cy.findByText("DoB").click();
cy.findByPlaceholderText("Enter a number").should("not.exist");
cy.findByText("Relative dates…").click();
@@ -832,14 +812,14 @@ describe("scenarios > question > custom column > data type", () => {
it("should handle CASE (metabase#13122)", () => {
openCustomColumnInTable(ORDERS_ID);
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: "case([Discount] > 0, [Created At], [Product → Created At])",
name: "MiscDate",
});
cy.button("Done").click();
filter({ mode: "notebook" });
popover().within(() => {
H.filter({ mode: "notebook" });
H.popover().within(() => {
cy.findByText("MiscDate").click();
cy.findByPlaceholderText("Enter a number").should("not.exist");
@@ -852,14 +832,14 @@ describe("scenarios > question > custom column > data type", () => {
it("should handle COALESCE", () => {
openCustomColumnInTable(ORDERS_ID);
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: "COALESCE([Product → Created At], [Created At])",
name: "MiscDate",
});
cy.button("Done").click();
filter({ mode: "notebook" });
popover().within(() => {
H.filter({ mode: "notebook" });
H.popover().within(() => {
cy.findByText("MiscDate").click();
cy.findByPlaceholderText("Enter a number").should("not.exist");
cy.findByText("Relative dates…").click();
@@ -871,16 +851,16 @@ describe("scenarios > question > custom column > data type", () => {
describe("scenarios > question > custom column > error feedback", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
openProductsTable({ mode: "notebook" });
H.openProductsTable({ mode: "notebook" });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Custom column").click();
});
it("should catch non-existent field reference", () => {
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: "abcdef",
name: "Non-existent",
});
@@ -890,7 +870,7 @@ describe("scenarios > question > custom column > error feedback", () => {
});
it("should fail on expression validation errors", () => {
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: "SUBSTRING('foo', 0, 1)",
name: "BadSubstring",
});
@@ -903,17 +883,17 @@ describe("scenarios > question > custom column > error feedback", () => {
// ExpressionEditorTextfield jsx component
describe("scenarios > question > custom column > expression editor", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
// This is the default screen size but we need it explicitly set for this test because of the resize later on
cy.viewport(1280, 800);
openOrdersTable({ mode: "notebook" });
H.openOrdersTable({ mode: "notebook" });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Custom column").click();
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: "1+1", // Formula was intentionally written without spaces (important for this repro)!
name: "Math",
});
@@ -960,34 +940,34 @@ describe("scenarios > question > custom column > expression editor", () => {
describe("scenarios > question > custom column > help text", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
openProductsTable({ mode: "notebook" });
H.openProductsTable({ mode: "notebook" });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Custom column").click();
});
it("should appear while inside a function", () => {
enterCustomColumnDetails({ formula: "Lower(", blur: false });
H.enterCustomColumnDetails({ formula: "Lower(", blur: false });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains("lower(text)");
});
it("should appear after a field reference", () => {
enterCustomColumnDetails({ formula: "Lower([Category]", blur: false });
H.enterCustomColumnDetails({ formula: "Lower([Category]", blur: false });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains("lower(text)");
});
it("should not appear while outside a function", () => {
enterCustomColumnDetails({ formula: "Lower([Category])", blur: false });
H.enterCustomColumnDetails({ formula: "Lower([Category])", blur: false });
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("lower(text)").should("not.exist");
});
it("should not appear when formula field is not in focus (metabase#15891)", () => {
enterCustomColumnDetails({
H.enterCustomColumnDetails({
formula: "rou{enter}1.5){leftArrow}",
blur: false,
});
@@ -1013,7 +993,7 @@ describe("scenarios > question > custom column > help text", () => {
});
it("should not disappear when clicked on (metabase#17548)", () => {
enterCustomColumnDetails({ formula: "rou{enter}", blur: false });
H.enterCustomColumnDetails({ formula: "rou{enter}", blur: false });
// Shouldn't hide on click
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
import { H } from "e2e/support";
import {
ORDERS_BY_YEAR_QUESTION_ID,
ORDERS_COUNT_QUESTION_ID,
} from "e2e/support/cypress_sample_instance_data";
import { updateDashboardCards, visitDashboard } from "e2e/support/helpers";
const cards = [
{
@@ -30,7 +30,7 @@ describe("dashboard card fetching", () => {
});
it("should pass same dashboard_load_id to every query to enable metadata cache sharing", () => {
createDashboardWithCards({ cards }).then(visitDashboard);
createDashboardWithCards({ cards }).then(H.visitDashboard);
cy.wait(["@dashcardQuery", "@dashcardQuery"]).then(interceptions => {
const query1 = interceptions[0].request.body;
@@ -50,7 +50,7 @@ function createDashboardWithCards({
return cy
.createDashboard({ name: dashboardName })
.then(({ body: { id } }) => {
updateDashboardCards({
H.updateDashboardCards({
dashboard_id: id,
cards,
});

View File

@@ -1,34 +1,7 @@
import { H } from "e2e/support";
import { SAMPLE_DB_ID, WRITABLE_DB_ID } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import { ORDERS_QUESTION_ID } from "e2e/support/cypress_sample_instance_data";
import {
addOrUpdateDashboardCard,
assertDescendantNotOverflowsContainer,
assertIsEllipsified,
assertIsNotEllipsified,
createNativeQuestionAndDashboard,
createQuestion,
createQuestionAndDashboard,
cypressWaitAll,
echartsContainer,
editDashboard,
focusNativeEditor,
getDashboardCard,
modal,
openNavigationSidebar,
pieSlices,
popover,
queryBuilderHeader,
resetTestTable,
restore,
resyncDatabase,
saveDashboard,
setActionsEnabledForDB,
showDashboardCardActions,
sidebar,
updateSetting,
visitDashboard,
} from "e2e/support/helpers";
import { createMockParameter } from "metabase-types/api/mocks";
const { ORDERS, ORDERS_ID, REVIEWS, PRODUCTS, PRODUCTS_ID, REVIEWS_ID } =
@@ -36,7 +9,7 @@ const { ORDERS, ORDERS_ID, REVIEWS, PRODUCTS, PRODUCTS_ID, REVIEWS_ID } =
describe("issue 18067", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -46,10 +19,10 @@ describe("issue 18067", () => {
() => {
const dialect = "mysql";
const TEST_TABLE = "many_data_types";
resetTestTable({ type: dialect, table: TEST_TABLE });
restore(`${dialect}-writable`);
H.resetTestTable({ type: dialect, table: TEST_TABLE });
H.restore(`${dialect}-writable`);
cy.signInAsAdmin();
resyncDatabase({
H.resyncDatabase({
dbId: WRITABLE_DB_ID,
tableName: TEST_TABLE,
tableAlias: "testTable",
@@ -68,17 +41,17 @@ describe("issue 18067", () => {
dashboardDetails,
questionDetails,
}).then(({ body: { dashboard_id } }) => {
visitDashboard(dashboard_id);
H.visitDashboard(dashboard_id);
});
});
editDashboard();
H.editDashboard();
cy.log('Select "click behavior" option');
showDashboardCardActions();
H.showDashboardCardActions();
cy.findByTestId("dashboardcard-actions-panel").icon("click").click();
sidebar().within(() => {
H.sidebar().within(() => {
cy.findByText("Boolean").scrollIntoView().click();
cy.contains("Click behavior for Boolean").should("be.visible");
});
@@ -88,7 +61,7 @@ describe("issue 18067", () => {
describe("issue 15993", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -103,7 +76,7 @@ describe("issue 15993", () => {
({ body: { id: nativeId } }) => {
cy.createDashboard().then(({ body: { id: dashboardId } }) => {
// Add native question to the dashboard
addOrUpdateDashboardCard({
H.addOrUpdateDashboardCard({
dashboard_id: dashboardId,
card_id: nativeId,
card: {
@@ -111,7 +84,7 @@ describe("issue 15993", () => {
visualization_settings: getVisualizationSettings(question1Id),
},
});
visitDashboard(dashboardId);
H.visitDashboard(dashboardId);
});
},
);
@@ -156,7 +129,7 @@ describe("issue 15993", () => {
describe("issue 16334", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.intercept("POST", "/api/dataset").as("dataset");
cy.intercept("POST", "/api/dashboard/*/dashcard/*/card/*/query").as(
@@ -172,7 +145,7 @@ describe("issue 16334", () => {
},
};
createQuestion({
H.createQuestion({
name: "16334",
query: {
"source-table": PRODUCTS_ID,
@@ -181,9 +154,9 @@ describe("issue 16334", () => {
},
display: "pie",
}).then(({ body: { id: question1Id } }) => {
createQuestionAndDashboard({ questionDetails }).then(
H.createQuestionAndDashboard({ questionDetails }).then(
({ body: { id, card_id, dashboard_id } }) => {
addOrUpdateDashboardCard({
H.addOrUpdateDashboardCard({
dashboard_id,
card_id,
card: {
@@ -192,7 +165,7 @@ describe("issue 16334", () => {
},
});
visitDashboard(dashboard_id);
H.visitDashboard(dashboard_id);
cy.wait("@dashcardQuery");
},
);
@@ -211,7 +184,7 @@ describe("issue 16334", () => {
cy.findByTestId("app-bar").should("contain.text", "Started from 16334");
// Make sure the original visualization didn't change
pieSlices().should("have.length", 2);
H.pieSlices().should("have.length", 2);
const getVisualizationSettings = targetId => ({
column_settings: {
@@ -294,7 +267,7 @@ describe("issue 17160", () => {
cy.wrap(dashboardId).as("sourceDashboardId");
// Add the question to the dashboard
addOrUpdateDashboardCard({
H.addOrUpdateDashboardCard({
dashboard_id: dashboardId,
card_id: questionId,
}).then(({ body: { id: dashCardId } }) => {
@@ -456,7 +429,7 @@ describe("issue 17160", () => {
function visitSourceDashboard() {
cy.get("@sourceDashboardId").then(id => {
visitDashboard(id);
H.visitDashboard(id);
});
}
@@ -471,7 +444,7 @@ describe("issue 17160", () => {
beforeEach(() => {
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -551,12 +524,12 @@ describe("issue 18454", () => {
};
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.createQuestionAndDashboard({ questionDetails }).then(
({ body: { id, card_id, dashboard_id } }) => {
visitDashboard(dashboard_id);
H.visitDashboard(dashboard_id);
},
);
});
@@ -640,7 +613,7 @@ describe("adding an additional series to a dashcard (metabase#20637)", () => {
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -659,7 +632,7 @@ describe("adding an additional series to a dashcard (metabase#20637)", () => {
cy.wait("@additionalSeriesCardQuery");
cy.findByTestId("add-series-modal").button("Done").click();
saveDashboard();
H.saveDashboard();
// refresh the page and make sure the dashcard query endpoint was used
cy.reload();
@@ -685,7 +658,7 @@ describe("issue 22265", () => {
};
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.intercept("GET", "/api/card/*/series?limit=*").as("seriesQuery");
@@ -712,11 +685,11 @@ describe("issue 22265", () => {
});
cy.wrap(dashboard_id).as("dashboardId");
visitDashboard(dashboard_id);
H.visitDashboard(dashboard_id);
},
);
editDashboard();
H.editDashboard();
cy.findByTestId("add-series-button").click({ force: true });
cy.wait("@seriesQuery");
@@ -744,8 +717,8 @@ describe("issue 22265", () => {
cy.request("PUT", `/api/card/${invalidQuestionId}`, questionDetailUpdate);
});
visitDashboard("@dashboardId");
editDashboard();
H.visitDashboard("@dashboardId");
H.editDashboard();
cy.findByTestId("add-series-button").click({ force: true });
cy.wait("@seriesQuery");
@@ -774,7 +747,7 @@ describe("issue 23137", () => {
};
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
});
@@ -785,7 +758,7 @@ describe("issue 23137", () => {
cy.createQuestionAndDashboard({
questionDetails: GAUGE_QUESTION_DETAILS,
}).then(({ body: { id, card_id, dashboard_id } }) => {
addOrUpdateDashboardCard({
H.addOrUpdateDashboardCard({
card_id,
dashboard_id,
card: {
@@ -801,12 +774,12 @@ describe("issue 23137", () => {
},
});
visitDashboard(dashboard_id);
H.visitDashboard(dashboard_id);
});
cy.findByTestId("gauge-arc-1").click();
cy.wait("@cardQuery");
queryBuilderHeader().findByDisplayValue("Orders").should("be.visible");
H.queryBuilderHeader().findByDisplayValue("Orders").should("be.visible");
});
it("should navigate to a target from a progress card (metabase#23137)", () => {
@@ -815,7 +788,7 @@ describe("issue 23137", () => {
cy.createQuestionAndDashboard({
questionDetails: PROGRESS_QUESTION_DETAILS,
}).then(({ body: { id, card_id, dashboard_id } }) => {
addOrUpdateDashboardCard({
H.addOrUpdateDashboardCard({
card_id,
dashboard_id,
card: {
@@ -831,12 +804,12 @@ describe("issue 23137", () => {
},
});
visitDashboard(dashboard_id);
H.visitDashboard(dashboard_id);
});
cy.findByTestId("progress-bar").click();
cy.wait("@cardQuery");
queryBuilderHeader().findByDisplayValue("Orders").should("be.visible");
H.queryBuilderHeader().findByDisplayValue("Orders").should("be.visible");
});
});
@@ -883,7 +856,7 @@ describe("issues 27020 and 27105: static-viz fails to render for certain date fo
}
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -891,7 +864,7 @@ describe("issues 27020 and 27105: static-viz fails to render for certain date fo
// This is currently the default setting, anyway.
// But we want to explicitly set it in case something changes in the future,
// because it is a crucial step for this reproduction.
updateSetting("custom-formatting", {
H.updateSetting("custom-formatting", {
"type/Temporal": {
date_style: "MMMM D, YYYY",
},
@@ -961,7 +934,7 @@ describe("issue 29304", () => {
describe("display: scalar", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.intercept("api/dashboard/*/dashcard/*/card/*/query").as(
"getDashcardQuery",
@@ -1101,7 +1074,7 @@ describe("issue 31628", () => {
const setupDashboardWithQuestionInCards = (question, cards) => {
cy.createDashboard().then(({ body: dashboard }) => {
cypressWaitAll(
H.cypressWaitAll(
cards.map(card => {
return cy.createQuestionAndAddToDashboard(
question,
@@ -1111,7 +1084,7 @@ describe("issue 31628", () => {
}),
);
visitDashboard(dashboard.id);
H.visitDashboard(dashboard.id);
});
};
@@ -1121,7 +1094,7 @@ describe("issue 31628", () => {
const descendants = dashcard.querySelectorAll(descendantsSelector);
descendants.forEach(descendant => {
assertDescendantNotOverflowsContainer(
H.assertDescendantNotOverflowsContainer(
descendant,
dashcard,
`dashcard[${dashcardIndex}] [data-testid="${descendant.dataset.testid}"]`,
@@ -1144,14 +1117,14 @@ describe("issue 31628", () => {
describe(`${width}x${height} - ${sidebar} - ${name}`, () => {
beforeEach(() => {
restore();
H.restore();
cy.viewport(width, height);
cy.signInAsAdmin();
setupDashboardWithQuestionInCards(SCALAR_QUESTION, cards);
if (openSidebar) {
cy.wait(100);
openNavigationSidebar();
H.openNavigationSidebar();
}
});
@@ -1164,7 +1137,7 @@ describe("issue 31628", () => {
describe("1x2 card", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setupDashboardWithQuestionInCards(SCALAR_QUESTION, [
{ size_x: 1, size_y: 2, row: 0, col: 0 },
@@ -1177,7 +1150,7 @@ describe("issue 31628", () => {
*/
const scalarContainer = cy.findByTestId("scalar-container");
scalarContainer.then($element => assertIsEllipsified($element[0]));
scalarContainer.then($element => H.assertIsEllipsified($element[0]));
scalarContainer.realHover();
cy.findByRole("tooltip").findByText("18,760").should("exist");
@@ -1200,7 +1173,7 @@ describe("issue 31628", () => {
describe("2x2 card", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setupDashboardWithQuestionInCards(SCALAR_QUESTION, [
{ size_x: 2, size_y: 2, row: 0, col: 0 },
@@ -1213,7 +1186,7 @@ describe("issue 31628", () => {
*/
const scalarContainer = cy.findByTestId("scalar-container");
scalarContainer.then($element => assertIsNotEllipsified($element[0]));
scalarContainer.then($element => H.assertIsNotEllipsified($element[0]));
scalarContainer.realHover();
cy.findByRole("tooltip").should("not.exist");
@@ -1228,7 +1201,7 @@ describe("issue 31628", () => {
*/
const scalarTitle = cy.findByTestId("scalar-title");
scalarTitle.then($element => assertIsEllipsified($element[0]));
scalarTitle.then($element => H.assertIsEllipsified($element[0]));
scalarTitle.realHover();
cy.findByRole("tooltip")
@@ -1248,7 +1221,7 @@ describe("issue 31628", () => {
describe("5x3 card", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setupDashboardWithQuestionInCards(SCALAR_QUESTION, [
{ size_x: 6, size_y: 3, row: 0, col: 0 },
@@ -1261,7 +1234,7 @@ describe("issue 31628", () => {
*/
const scalarContainer = cy.findByTestId("scalar-container");
scalarContainer.then($element => assertIsNotEllipsified($element[0]));
scalarContainer.then($element => H.assertIsNotEllipsified($element[0]));
scalarContainer.realHover();
cy.findByRole("tooltip").should("not.exist");
@@ -1276,7 +1249,7 @@ describe("issue 31628", () => {
*/
const scalarTitle = cy.findByTestId("scalar-title");
scalarTitle.then($element => assertIsNotEllipsified($element[0]));
scalarTitle.then($element => H.assertIsNotEllipsified($element[0]));
scalarTitle.realHover();
cy.findByRole("tooltip").should("not.exist");
@@ -1286,7 +1259,7 @@ describe("issue 31628", () => {
*/
cy.findByTestId("scalar-description").realHover();
popover().findByText(SCALAR_QUESTION.description).should("exist");
H.popover().findByText(SCALAR_QUESTION.description).should("exist");
});
});
});
@@ -1304,13 +1277,13 @@ describe("issue 31628", () => {
describe(`${width}x${height} - ${sidebar} - ${name}`, () => {
beforeEach(() => {
restore();
H.restore();
cy.viewport(width, height);
cy.signInAsAdmin();
setupDashboardWithQuestionInCards(SMART_SCALAR_QUESTION, cards);
if (openSidebar) {
openNavigationSidebar();
H.openNavigationSidebar();
}
});
@@ -1323,7 +1296,7 @@ describe("issue 31628", () => {
describe("2x2 card", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setupDashboardWithQuestionInCards(SMART_SCALAR_QUESTION, [
{ size_x: 2, size_y: 2, row: 0, col: 0 },
@@ -1336,7 +1309,7 @@ describe("issue 31628", () => {
*/
const scalarContainer = cy.findByTestId("scalar-container");
scalarContainer.then($element => assertIsNotEllipsified($element[0]));
scalarContainer.then($element => H.assertIsNotEllipsified($element[0]));
scalarContainer.realHover();
cy.findByRole("tooltip").should("not.exist");
@@ -1351,7 +1324,7 @@ describe("issue 31628", () => {
*/
const scalarTitle = cy.findByTestId("legend-caption-title");
scalarTitle.then($element => assertIsEllipsified($element[0]));
scalarTitle.then($element => H.assertIsEllipsified($element[0]));
scalarTitle.realHover();
cy.findByRole("tooltip")
@@ -1376,7 +1349,7 @@ describe("issue 31628", () => {
previousValue.within(() => {
cy.contains("34.7%").should("exist");
cy.contains("• vs. previous month: 527").should("not.exist");
previousValue.then($element => assertIsNotEllipsified($element[0]));
previousValue.then($element => H.assertIsNotEllipsified($element[0]));
});
});
@@ -1389,7 +1362,7 @@ describe("issue 31628", () => {
cy.contains("34.7%").should("exist");
cy.contains("34.72%").should("not.exist");
cy.contains("• vs. previous month: 527").should("not.exist");
previousValue.then($element => assertIsNotEllipsified($element[0]));
previousValue.then($element => H.assertIsNotEllipsified($element[0]));
});
});
@@ -1402,7 +1375,7 @@ describe("issue 31628", () => {
cy.contains("35%").should("exist");
cy.contains("34.72%").should("not.exist");
cy.contains("• vs. previous month: 527").should("not.exist");
previousValue.then($element => assertIsNotEllipsified($element[0]));
previousValue.then($element => H.assertIsNotEllipsified($element[0]));
});
});
@@ -1413,13 +1386,13 @@ describe("issue 31628", () => {
previousValue
.findByText("35%")
.then($element => assertIsEllipsified($element[0]));
.then($element => H.assertIsEllipsified($element[0]));
});
});
describe("7x3 card", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setupDashboardWithQuestionInCards(SMART_SCALAR_QUESTION, [
{ size_x: 7, size_y: 3, row: 0, col: 0 },
@@ -1432,7 +1405,7 @@ describe("issue 31628", () => {
*/
let scalarContainer = cy.findByTestId("scalar-container");
scalarContainer.then($element => assertIsNotEllipsified($element[0]));
scalarContainer.then($element => H.assertIsNotEllipsified($element[0]));
scalarContainer.realHover();
cy.findByRole("tooltip").should("not.exist");
@@ -1447,7 +1420,7 @@ describe("issue 31628", () => {
*/
scalarContainer = cy.findByTestId("legend-caption-title");
scalarContainer.then($element => assertIsEllipsified($element[0]));
scalarContainer.then($element => H.assertIsEllipsified($element[0]));
scalarContainer.realHover();
cy.findByRole("tooltip")
@@ -1471,7 +1444,7 @@ describe("issue 31628", () => {
previousValue.within(() => {
cy.contains("34.72%").should("exist");
cy.contains("• vs. previous month: 527").should("exist");
previousValue.then($element => assertIsNotEllipsified($element[0]));
previousValue.then($element => H.assertIsNotEllipsified($element[0]));
});
/**
@@ -1485,7 +1458,7 @@ describe("issue 31628", () => {
describe("7x4 card", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setupDashboardWithQuestionInCards(SMART_SCALAR_QUESTION, [
{ size_x: 7, size_y: 4, row: 0, col: 0 },
@@ -1498,7 +1471,7 @@ describe("issue 31628", () => {
*/
let scalarContainer = cy.findByTestId("scalar-container");
scalarContainer.then($element => assertIsNotEllipsified($element[0]));
scalarContainer.then($element => H.assertIsNotEllipsified($element[0]));
scalarContainer.realHover();
cy.findByRole("tooltip").should("not.exist");
@@ -1513,7 +1486,7 @@ describe("issue 31628", () => {
*/
scalarContainer = cy.findByTestId("legend-caption-title");
scalarContainer.then($element => assertIsEllipsified($element[0]));
scalarContainer.then($element => H.assertIsEllipsified($element[0]));
scalarContainer.realHover();
cy.findByRole("tooltip")
@@ -1537,7 +1510,7 @@ describe("issue 31628", () => {
previousValue.within(() => {
cy.contains("34.72%").should("exist");
cy.contains("• vs. previous month: 527").should("exist");
previousValue.then($element => assertIsNotEllipsified($element[0]));
previousValue.then($element => H.assertIsNotEllipsified($element[0]));
});
/**
@@ -1584,7 +1557,7 @@ describe("issue 32231", () => {
const defaultError = "Which fields do you want to use for the X and Y axes?";
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cy.intercept("GET", "/api/card/*/series?limit=*").as("seriesQuery");
@@ -1607,30 +1580,30 @@ describe("issue 32231", () => {
],
});
visitDashboard(dashboard_id);
H.visitDashboard(dashboard_id);
},
);
editDashboard();
H.editDashboard();
cy.findByTestId("add-series-button").click({ force: true });
cy.wait("@seriesQuery");
cy.findByTestId("add-series-modal").within(() => {
echartsContainer().should("exist");
H.echartsContainer().should("exist");
cy.findByText(issue32231Error).should("not.exist");
cy.findByText(multipleSeriesError).should("not.exist");
cy.findByText(defaultError).should("not.exist");
cy.findByLabelText(incompleteQuestion.name).click();
echartsContainer().should("not.exist");
H.echartsContainer().should("not.exist");
cy.findByText(issue32231Error).should("not.exist");
cy.findByText(multipleSeriesError).should("exist");
cy.findByText(defaultError).should("not.exist");
cy.findByLabelText(incompleteQuestion.name).click();
echartsContainer().should("exist");
H.echartsContainer().should("exist");
cy.findByText(issue32231Error).should("not.exist");
cy.findByText(multipleSeriesError).should("not.exist");
cy.findByText(defaultError).should("not.exist");
@@ -1654,7 +1627,7 @@ describe("issue 32231", () => {
],
});
visitDashboard(dashboard_id);
H.visitDashboard(dashboard_id);
});
cy.findByTestId("dashcard").findByText(defaultError).should("exist");
@@ -1704,13 +1677,13 @@ describe("issue 43219", () => {
const getQuestionAlias = index => `question-${index}`;
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
cypressWaitAll(
H.cypressWaitAll(
Array.from({ length: cardsCount }, (_value, index) => {
const name = `Series ${index + 1}`;
return createQuestion({ ...questionDetails, name }).then(
return H.createQuestion({ ...questionDetails, name }).then(
({ body: question }) => {
cy.wrap(question).as(getQuestionAlias(index));
},
@@ -1740,18 +1713,18 @@ describe("issue 43219", () => {
},
],
}).then(({ dashboard }) => {
visitDashboard(dashboard.id);
H.visitDashboard(dashboard.id);
});
});
});
it("is possible to map parameters to dashcards with lots of series (metabase#43219)", () => {
editDashboard();
H.editDashboard();
cy.findByTestId("edit-dashboard-parameters-widget-container")
.findByText("Text")
.click();
getDashboardCard(0).within(() => {
H.getDashboardCard(0).within(() => {
cy.findByText("Series 10").should("exist").and("not.be.visible");
cy.findByTestId("visualization-root").scrollTo("bottom");
@@ -1764,9 +1737,9 @@ describe("issue 43219", () => {
describe("issue 48878", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
setActionsEnabledForDB(SAMPLE_DB_ID);
H.setActionsEnabledForDB(SAMPLE_DB_ID);
cy.signInAsNormalUser();
cy.intercept("POST", "/api/dataset").as("dataset");
@@ -1796,7 +1769,7 @@ describe("issue 48878", () => {
it("does not crash the action button viz (metabase#48878)", () => {
cy.reload();
cy.wait("@fetchCard");
getDashboardCard(0).findByText("Click Me").should("be.visible");
H.getDashboardCard(0).findByText("Click Me").should("be.visible");
});
function setup() {
@@ -1814,7 +1787,7 @@ describe("issue 48878", () => {
cy.log("create model");
cy.button("New").click();
popover().findByText("Model").click();
H.popover().findByText("Model").click();
createModel({
name: "SQL Model",
query: "select * from orders limit 5",
@@ -1823,19 +1796,19 @@ describe("issue 48878", () => {
cy.log("create model action");
cy.findByTestId("qb-header-info-button").click();
modal().findByText("See more about this model").click();
H.modal().findByText("See more about this model").click();
cy.findByRole("tab", { name: "Actions" }).click();
cy.findByTestId("model-actions-header").findByText("New action").click();
modal().within(() => {
focusNativeEditor().type("UPDATE orders SET plan = {{ plan ", {
H.modal().within(() => {
H.focusNativeEditor().type("UPDATE orders SET plan = {{ plan ", {
parseSpecialCharSequences: false,
});
cy.button("Save").click();
});
modal()
H.modal()
.last()
.within(() => {
cy.findByPlaceholderText("My new fantastic action").type("Test action");
@@ -1848,9 +1821,9 @@ describe("issue 48878", () => {
cy.log("create dashoard");
cy.button("New").click();
popover().findByText("Dashboard").click();
H.popover().findByText("Dashboard").click();
modal().within(() => {
H.modal().within(() => {
cy.findByPlaceholderText("What is the name of your dashboard?").type(
"Dash",
);
@@ -1860,7 +1833,7 @@ describe("issue 48878", () => {
cy.button("Add action").click();
cy.button("Pick an action").click();
modal().within(() => {
H.modal().within(() => {
cy.findByText("SQL Model").click();
cy.findByText("Test action").click();
cy.button("Done").click();
@@ -1875,14 +1848,14 @@ describe("issue 48878", () => {
.findByText("Use a native query")
.click();
focusNativeEditor().type(query);
H.focusNativeEditor().type(query);
cy.findByTestId("native-query-editor-sidebar")
.findByTestId("run-button")
.click();
cy.wait("@dataset");
cy.button("Save").click();
modal().within(() => {
H.modal().within(() => {
cy.findByPlaceholderText("What is the name of your model?").type(name);
cy.button("Save").click();
cy.wait("@saveQuestion");
@@ -1902,10 +1875,10 @@ SELECT 'group_2', 'sub_group_2', 52, 'group_2__sub_group_2';
`;
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
createNativeQuestionAndDashboard({
H.createNativeQuestionAndDashboard({
questionDetails: {
name: "46318",
native: { query },
@@ -1918,23 +1891,23 @@ SELECT 'group_2', 'sub_group_2', 52, 'group_2__sub_group_2';
},
},
}).then(response => {
visitDashboard(response.body.dashboard_id);
H.visitDashboard(response.body.dashboard_id);
});
editDashboard();
getDashboardCard().realHover().icon("click").click();
H.editDashboard();
H.getDashboardCard().realHover().icon("click").click();
cy.get("aside").within(() => {
cy.findByText("Go to a custom destination").click();
cy.findByText("URL").click();
});
modal().within(() => {
H.modal().within(() => {
cy.findByPlaceholderText("e.g. http://acme.com/id/{{user_id}}").type(
"http://localhost:4000/?q={{group_name}}",
{ parseSpecialCharSequences: false },
);
cy.button("Done").click();
});
saveDashboard();
H.saveDashboard();
});
it("passes values from unused columns of row visualization to click behavior (metabase#46318)", () => {

View File

@@ -1,18 +1,8 @@
import _ from "underscore";
import { H } from "e2e/support";
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
createDashboard,
createQuestion,
editDashboard,
getDashboardCard,
popover,
resizeDashboardCard,
restore,
saveDashboard,
visitDashboard,
} from "e2e/support/helpers";
import { GRID_WIDTH } from "metabase/lib/dashboard_grid";
const VISUALIZATION_SIZES = {
@@ -203,16 +193,16 @@ describe(
{ requestTimeout: 15000 },
() => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
it("should display all visualization cards with their default sizes", () => {
TEST_QUESTIONS.forEach(question => {
createQuestion(question);
H.createQuestion(question);
});
createDashboard().then(({ body: { id: dashId } }) => {
visitDashboard(dashId);
H.createDashboard().then(({ body: { id: dashId } }) => {
H.visitDashboard(dashId);
cy.findByTestId("dashboard-header").within(() => {
cy.findByLabelText("Edit dashboard").click();
@@ -242,7 +232,7 @@ describe(
cy.wait("@cardQuery");
});
saveDashboard();
H.saveDashboard();
cy.request("GET", `/api/dashboard/${dashId}`).then(({ body }) => {
body.dashcards.forEach(({ card, size_x, size_y }) => {
@@ -257,11 +247,11 @@ describe(
it("should not allow cards to be resized smaller than min height", () => {
const cardIds = [];
TEST_QUESTIONS.forEach(question => {
createQuestion(question).then(({ body: { id } }) => {
H.createQuestion(question).then(({ body: { id } }) => {
cardIds.push(id);
});
});
createDashboard().then(({ body: { id: dashId } }) => {
H.createDashboard().then(({ body: { id: dashId } }) => {
cy.request("PUT", `/api/dashboard/${dashId}`, {
dashcards: cardIds.map((cardId, index) => ({
id: index,
@@ -272,19 +262,19 @@ describe(
size_y: 10,
})),
});
visitDashboard(dashId);
editDashboard();
H.visitDashboard(dashId);
H.editDashboard();
cy.request("GET", `/api/dashboard/${dashId}`).then(({ body }) => {
body.dashcards.forEach(({ card }, index) => {
resizeDashboardCard({
card: getDashboardCard(index),
H.resizeDashboardCard({
card: H.getDashboardCard(index),
x: -getDefaultSize(card.display).width * 200,
y: -getDefaultSize(card.display).height * 200,
});
});
saveDashboard();
H.saveDashboard();
cy.request("GET", `/api/dashboard/${dashId}`).then(({ body }) => {
body.dashcards.forEach(({ card, size_x, size_y }) => {
@@ -300,8 +290,8 @@ describe(
);
describe("issue 31701", () => {
const entityCard = () => getDashboardCard(0);
const customCard = () => getDashboardCard(1);
const entityCard = () => H.getDashboardCard(0);
const customCard = () => H.getDashboardCard(1);
const editEntityLinkContainer = () =>
cy.findByTestId("entity-edit-display-link");
@@ -314,37 +304,37 @@ describe("issue 31701", () => {
cy.findByTestId("custom-view-text-link");
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
createQuestion({
H.createQuestion({
name: TEST_QUESTION_NAME,
query: {
"source-table": ORDERS_ID,
},
});
createDashboard({
H.createDashboard({
name: TEST_DASHBOARD_NAME,
}).then(({ body: { id: dashId } }) => {
visitDashboard(dashId);
H.visitDashboard(dashId);
});
editDashboard();
H.editDashboard();
cy.log("Add first link card (connected to an entity");
cy.findByLabelText("Add a link or iframe").click();
popover().findByText("Link").click();
getDashboardCard(0).as("entityCard").click().type(TEST_QUESTION_NAME);
popover()
H.popover().findByText("Link").click();
H.getDashboardCard(0).as("entityCard").click().type(TEST_QUESTION_NAME);
H.popover()
.findAllByTestId("search-result-item-name")
.first()
.trigger("click");
cy.log("Add second link card (text only)");
cy.findByLabelText("Add a link or iframe").click();
popover().findByText("Link").click();
getDashboardCard(1)
H.popover().findByText("Link").click();
H.getDashboardCard(1)
.as("customCard")
.click()
.type(TEST_QUESTION_NAME)
@@ -361,7 +351,7 @@ describe("issue 31701", () => {
assertLinkCardOverflow(editCustomLinkContainer(), customCard());
});
saveDashboard();
H.saveDashboard();
cy.log("when viewing a saved dashboard");
viewports.forEach(([width, height]) => {

View File

@@ -1,21 +1,8 @@
import {
createNewTab,
editDashboard,
getDashboardCard,
getDashboardCards,
getTextCardDetails,
goToTab,
moveDashCardToTab,
removeDashboardCard,
restore,
undo,
updateDashboardCards,
visitDashboard,
} from "e2e/support/helpers";
import { H } from "e2e/support";
describe("scenarios > dashboard cards > undo", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
@@ -24,35 +11,35 @@ describe("scenarios > dashboard cards > undo", () => {
{ scrollBehavior: false },
() => {
const checkOrder = () => {
getDashboardCard(0).findByText("Text card 1");
getDashboardCard(1).findByText("Text card 2");
getDashboardCard(2).findByText("Text card 3");
getDashboardCard(3).findByText("Text card 4");
H.getDashboardCard(0).findByText("Text card 1");
H.getDashboardCard(1).findByText("Text card 2");
H.getDashboardCard(2).findByText("Text card 3");
H.getDashboardCard(3).findByText("Text card 4");
};
const cards = [
getTextCardDetails({
H.getTextCardDetails({
text: "Text card 1",
size_x: 4,
size_y: 1,
row: 0,
col: 1,
}),
getTextCardDetails({
H.getTextCardDetails({
text: "Text card 2",
size_x: 4,
size_y: 1,
row: 1,
col: 0,
}),
getTextCardDetails({
H.getTextCardDetails({
text: "Text card 3",
size_x: 4,
size_y: 1,
row: 2,
col: 3,
}),
getTextCardDetails({
H.getTextCardDetails({
text: "Text card 4",
size_x: 4,
size_y: 1,
@@ -62,36 +49,36 @@ describe("scenarios > dashboard cards > undo", () => {
];
cy.createDashboard().then(({ body: { id: dashboard_id } }) => {
updateDashboardCards({ dashboard_id, cards });
H.updateDashboardCards({ dashboard_id, cards });
visitDashboard(dashboard_id);
H.visitDashboard(dashboard_id);
});
checkOrder();
editDashboard();
H.editDashboard();
for (let i = 0; i < cards.length; i++) {
removeDashboardCard(i);
getDashboardCards().should("have.length", cards.length - 1);
H.removeDashboardCard(i);
H.getDashboardCards().should("have.length", cards.length - 1);
undo();
getDashboardCards().should("have.length", cards.length);
H.undo();
H.getDashboardCards().should("have.length", cards.length);
checkOrder();
// Seems to be needed to allow the UI to catch up before hovering the next element.
// TODO: improve this.
cy.wait(200);
}
createNewTab();
goToTab("Tab 1");
H.createNewTab();
H.goToTab("Tab 1");
for (let i = 0; i < cards.length; i++) {
moveDashCardToTab({ dashcardIndex: i, tabName: "Tab 2" });
getDashboardCards().should("have.length", cards.length - 1);
H.moveDashCardToTab({ dashcardIndex: i, tabName: "Tab 2" });
H.getDashboardCards().should("have.length", cards.length - 1);
undo();
getDashboardCards().should("have.length", cards.length);
H.undo();
H.getDashboardCards().should("have.length", cards.length);
checkOrder();
cy.wait(200);
}

View File

@@ -1,3 +1,4 @@
import { H } from "e2e/support";
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
@@ -5,29 +6,6 @@ import {
ORDERS_DASHBOARD_ID,
ORDERS_QUESTION_ID,
} from "e2e/support/cypress_sample_instance_data";
import {
addOrUpdateDashboardCard,
assertQueryBuilderRowCount,
assertTooltipRow,
chartPathWithFillColor,
echartsContainer,
echartsTooltip,
editDashboard,
entityPickerModal,
filterWidget,
getDashboardCard,
main,
modal,
popover,
queryBuilderHeader,
queryBuilderMain,
restore,
saveDashboard,
showDashboardCardActions,
sidebar,
tooltipHeader,
visitDashboard,
} from "e2e/support/helpers";
const {
ORDERS,
@@ -41,20 +19,20 @@ const {
describe("scenarios > dashboard > dashboard drill", () => {
beforeEach(() => {
restore();
H.restore();
cy.signInAsAdmin();
});
it("should handle URL click through on a table", () => {
createDashboardWithQuestion({}, dashboardId => {
visitDashboard(dashboardId);
H.visitDashboard(dashboardId);
cy.findByTestId("dashboard-header").icon("pencil").click();
showDashboardCardActions();
H.showDashboardCardActions();
cy.findByTestId("dashboardcard-actions-panel").icon("click").click();
// configure a URL click through on the "MY_NUMBER" column
sidebar()
H.sidebar()
.findByText("On-click behavior for each column")
.parent()
.parent()
@@ -63,7 +41,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
cy.findByText("URL").click();
// set the url and text template
modal().within(() => {
H.modal().within(() => {
cy.get("input").first().type("/foo/{{my_number}}/{{my_param}}", {
parseSpecialCharSequences: false,
});
@@ -81,7 +59,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
setParamValue("My Param", "param-value");
// click value and confirm url updates
getDashboardCard().findByText("column value: 111").click();
H.getDashboardCard().findByText("column value: 111").click();
cy.location("pathname").should("eq", "/foo/111/param-value");
});
});
@@ -128,7 +106,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
visualization_settings: clickBehavior,
});
visitDashboard(dashboard_id);
H.visitDashboard(dashboard_id);
},
);
@@ -164,7 +142,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
questionId => {
createDashboard(
{ questionId, visualization_settings: dashCardSettings },
dashboardIdA => visitDashboard(dashboardIdA),
dashboardIdA => H.visitDashboard(dashboardIdA),
);
},
);
@@ -174,28 +152,30 @@ describe("scenarios > dashboard > dashboard drill", () => {
});
it("should handle question click through on a table", () => {
createDashboardWithQuestion({}, dashboardId => visitDashboard(dashboardId));
createDashboardWithQuestion({}, dashboardId =>
H.visitDashboard(dashboardId),
);
cy.findByLabelText("Edit dashboard").click();
showDashboardCardActions();
H.showDashboardCardActions();
cy.findByLabelText("Click behavior").click();
sidebar().within(() => {
H.sidebar().within(() => {
// Configuring on-click behavior for MY_NUMBER column
cy.findByText("MY_NUMBER").click();
cy.findByText("Go to a custom destination").click();
cy.findByText("Saved question").click();
});
modal().findByText("Orders").click();
H.modal().findByText("Orders").click();
sidebar().findByText("User ID").click();
popover().findByText("MY_NUMBER").click();
H.sidebar().findByText("User ID").click();
H.popover().findByText("MY_NUMBER").click();
sidebar().findByText("Product → Category").click();
popover().findByText("My Param").click();
H.sidebar().findByText("Product → Category").click();
H.popover().findByText("My Param").click();
sidebar()
H.sidebar()
.findByLabelText(/Customize link text/)
.type("num: {{my_number}}", {
parseSpecialCharSequences: false,
@@ -204,18 +184,18 @@ describe("scenarios > dashboard > dashboard drill", () => {
cy.findByTestId("edit-bar").button("Save").click();
// wait to leave editing mode and set a param value
main().findByText("You're editing this dashboard.").should("not.exist");
H.main().findByText("You're editing this dashboard.").should("not.exist");
setParamValue("My Param", "Widget");
// click on table value
cy.findByTestId("dashcard").findByText("num: 111").click();
queryBuilderHeader().findByText("Orders").should("be.visible");
H.queryBuilderHeader().findByText("Orders").should("be.visible");
cy.findByTestId("qb-filters-panel").within(() => {
cy.findByText("User ID is 111").should("be.visible");
cy.findByText("Product → Category is Widget").should("be.visible");
});
assertQueryBuilderRowCount(5);
H.assertQueryBuilderRowCount(5);
});
it("should handle dashboard click through on a table", () => {
@@ -226,14 +206,14 @@ describe("scenarios > dashboard > dashboard drill", () => {
createDashboardWithQuestion(
{ dashboardName: "end dash" },
dashboardIdB => {
visitDashboard(dashboardIdA);
H.visitDashboard(dashboardIdA);
},
);
},
);
});
cy.icon("pencil").click();
showDashboardCardActions();
H.showDashboardCardActions();
cy.findByTestId("dashboardcard-actions-panel").within(() => {
cy.icon("click").click();
});
@@ -252,7 +232,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
.parent()
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
.within(() => cy.findByText("Dashboard").click());
entityPickerModal().within(() => {
H.entityPickerModal().within(() => {
cy.findByRole("tab", { name: /Dashboards/ }).click();
cy.findByText("end dash").click();
});
@@ -262,7 +242,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
.within(() => cy.findByText("My Param").click());
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
popover().within(() => cy.findByText("MY_STRING").click());
H.popover().within(() => cy.findByText("MY_STRING").click());
// set the text template
cy.findByPlaceholderText("E.x. Details for {{Column Name}}").type(
@@ -288,21 +268,21 @@ describe("scenarios > dashboard > dashboard drill", () => {
it("should open the same dashboard when a custom URL click behavior points to the same dashboard (metabase#22702)", () => {
createDashboardWithQuestion({}, dashboardId => {
visitDashboard(dashboardId);
editDashboard();
showDashboardCardActions();
H.visitDashboard(dashboardId);
H.editDashboard();
H.showDashboardCardActions();
cy.findByTestId("dashboardcard-actions-panel")
.icon("click")
.should("be.visible")
.click();
sidebar().within(() => {
H.sidebar().within(() => {
cy.findByText("MY_NUMBER").click();
cy.findByText("Go to a custom destination").click();
cy.findByText("URL").click();
});
modal().within(() => {
H.modal().within(() => {
cy.get("input")
.first()
.type(`/dashboard/${dashboardId}?my_param=Aaron Hand`, { delay: 0 });
@@ -310,7 +290,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
cy.button("Done").click();
});
saveDashboard();
H.saveDashboard();
cy.findByTestId("dashcard").findByText("Click behavior").click();
cy.get("fieldset").should("contain", "Aaron Hand");
@@ -322,9 +302,11 @@ describe("scenarios > dashboard > dashboard drill", () => {
// This was flaking. Example: https://dashboard.cypress.io/projects/a394u1/runs/2109/test-results/91a15b66-4b80-40bf-b569-de28abe21f42
it.skip("should handle cross-filter on a table", () => {
createDashboardWithQuestion({}, dashboardId => visitDashboard(dashboardId));
createDashboardWithQuestion({}, dashboardId =>
H.visitDashboard(dashboardId),
);
cy.icon("pencil").click();
showDashboardCardActions();
H.showDashboardCardActions();
cy.findByTestId("dashboardcard-actions-panel").within(() => {
cy.icon("click").click();
});
@@ -344,7 +326,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
.within(() => cy.findByText("My Param").click());
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
popover().within(() => cy.findByText("MY_STRING").click());
H.popover().within(() => cy.findByText("MY_STRING").click());
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Save").click();
@@ -419,7 +401,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
it("when clicking on the field value (metabase#13062-1)", () => {
cy.findByTestId("dashcard").findByText("xavier").click();
popover().findByText("Is xavier").click();
H.popover().findByText("Is xavier").click();
cy.findByTestId("qb-filters-panel").within(() => {
cy.findByText("Reviewer is xavier").should("be.visible");
@@ -427,11 +409,11 @@ describe("scenarios > dashboard > dashboard drill", () => {
});
// xavier's review
queryBuilderMain()
H.queryBuilderMain()
.contains("Reprehenderit non error")
.should("be.visible");
assertQueryBuilderRowCount(1);
H.assertQueryBuilderRowCount(1);
});
it("when clicking on the card title (metabase#13062-2)", () => {
@@ -441,11 +423,11 @@ describe("scenarios > dashboard > dashboard drill", () => {
.should("be.visible");
// Sample review body
queryBuilderMain()
H.queryBuilderMain()
.contains("Ad perspiciatis quis et consectetur.")
.should("be.visible");
assertQueryBuilderRowCount(907);
H.assertQueryBuilderRowCount(907);
});
});
@@ -495,7 +477,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
],
});
visitDashboard(ORDERS_DASHBOARD_ID);
H.visitDashboard(ORDERS_DASHBOARD_ID);
cy.findAllByTestId("column-header").contains("ID").click().click();
cy.get(".test-Table-ID").contains(PK_VALUE).first().click();
@@ -557,7 +539,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
});
cy.intercept("POST", "/api/dataset").as("dataset");
visitDashboard(ORDERS_DASHBOARD_ID);
H.visitDashboard(ORDERS_DASHBOARD_ID);
// Product ID in the first row (query fails for User ID as well)
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("105").click();
@@ -598,7 +580,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
});
cy.log("Add question to the dashboard");
addOrUpdateDashboardCard({
H.addOrUpdateDashboardCard({
card_id: QUESTION_ID,
dashboard_id: DASHBOARD_ID,
card: {
@@ -633,23 +615,23 @@ describe("scenarios > dashboard > dashboard drill", () => {
},
});
visitDashboard(DASHBOARD_ID);
H.visitDashboard(DASHBOARD_ID);
cy.intercept(
"POST",
`/api/dashboard/${DASHBOARD_ID}/dashcard/*/card/${QUESTION_ID}/query`,
).as("cardQuery");
chartPathWithFillColor("#509EE3")
H.chartPathWithFillColor("#509EE3")
.eq(14) // August 2023 (Total of 12 reviews, 9 unique days)
.click();
cy.wait("@cardQuery");
cy.url().should("include", "2023-08");
chartPathWithFillColor("#509EE3").should("have.length", 1);
H.chartPathWithFillColor("#509EE3").should("have.length", 1);
// Since hover doesn't work in Cypress we can't assert on the popover that's shown when one hovers the bar
// But when this issue gets fixed, Y-axis should definitely show "12" (total count of reviews)
echartsContainer().get("text").contains("12");
H.echartsContainer().get("text").contains("12");
});
});
});
@@ -676,16 +658,16 @@ describe("scenarios > dashboard > dashboard drill", () => {
cy.createQuestionAndDashboard({ questionDetails, dashboardDetails }).then(
({ body: { dashboard_id } }) => {
visitDashboard(dashboard_id);
H.visitDashboard(dashboard_id);
// click the first bar on the card's graph and do a zoom drill-through
chartPathWithFillColor("#509EE3").eq(0).click();
H.chartPathWithFillColor("#509EE3").eq(0).click();
cy.findByText("See this month by week").click();
cy.wait("@dataset");
// check that the display is still a bar chart by checking that a .bar element exists
chartPathWithFillColor("#509EE3").should("exist");
H.chartPathWithFillColor("#509EE3").should("exist");
},
);
});
@@ -716,7 +698,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
createQuestion({ visualization_settings: questionSettings }, questionId => {
createDashboard(
{ questionId, visualization_settings: dashCardSettings },
dashboardIdA => visitDashboard(dashboardIdA),
dashboardIdA => H.visitDashboard(dashboardIdA),
);
});
@@ -735,7 +717,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
}).then(({ body: { id: QUESTION_ID } }) => {
cy.createDashboard().then(({ body: { id: DASHBOARD_ID } }) => {
// Add previously added question to the dashboard
addOrUpdateDashboardCard({
H.addOrUpdateDashboardCard({
card_id: QUESTION_ID,
dashboard_id: DASHBOARD_ID,
card: {
@@ -755,12 +737,12 @@ describe("scenarios > dashboard > dashboard drill", () => {
},
});
visitDashboard(DASHBOARD_ID);
H.visitDashboard(DASHBOARD_ID);
cy.icon("pencil").click();
// Edit "Visualization options"
showDashboardCardActions();
H.showDashboardCardActions();
cy.icon("palette").click();
modal().within(() => {
H.modal().within(() => {
cy.findByText("Reset to defaults").click();
cy.button("Done").click();
});
@@ -807,7 +789,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
],
});
// Add question to the dashboard
addOrUpdateDashboardCard({
H.addOrUpdateDashboardCard({
card_id: QUESTION_ID,
dashboard_id: DASHBOARD_ID,
card: {
@@ -872,7 +854,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
}).then(({ body: { id: QUESTION2_ID } }) => {
cy.createDashboard().then(({ body: { id: DASHBOARD_ID } }) => {
// Add the first question to the dashboard
addOrUpdateDashboardCard({
H.addOrUpdateDashboardCard({
card_id: QUESTION1_ID,
dashboard_id: DASHBOARD_ID,
card: {
@@ -884,19 +866,19 @@ describe("scenarios > dashboard > dashboard drill", () => {
},
});
visitDashboard(DASHBOARD_ID);
H.visitDashboard(DASHBOARD_ID);
const assertTooltipValues = () =>
echartsTooltip().within(() => {
tooltipHeader().should("have.text", 1);
assertTooltipRow("15612_1", { color: "#88BF4D", value: "5" });
assertTooltipRow("15612_2", { color: "#98D9D9", value: "10" });
H.echartsTooltip().within(() => {
H.tooltipHeader().should("have.text", 1);
H.assertTooltipRow("15612_1", { color: "#88BF4D", value: "5" });
H.assertTooltipRow("15612_2", { color: "#98D9D9", value: "10" });
});
chartPathWithFillColor("#88BF4D").first().trigger("mousemove");
H.chartPathWithFillColor("#88BF4D").first().trigger("mousemove");
assertTooltipValues();
chartPathWithFillColor("#98D9D9").first().trigger("mousemove");
H.chartPathWithFillColor("#98D9D9").first().trigger("mousemove");
assertTooltipValues();
});
});
@@ -965,7 +947,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
],
});
visitDashboard(ORDERS_DASHBOARD_ID);
H.visitDashboard(ORDERS_DASHBOARD_ID);
});
it("should correctly drill-through on Orders filter (metabase#11503-1)", () => {
@@ -973,13 +955,13 @@ describe("scenarios > dashboard > dashboard drill", () => {
drillThroughCardTitle("Orders");
queryBuilderMain().within(() => {
H.queryBuilderMain().within(() => {
cy.findByText("37.65").should("be.visible");
cy.findByText("110.93").should("be.visible");
cy.findByText("52.72").should("not.exist");
});
assertQueryBuilderRowCount(2);
H.assertQueryBuilderRowCount(2);
postDrillAssertion("ID is 2 selections");
});
@@ -988,18 +970,18 @@ describe("scenarios > dashboard > dashboard drill", () => {
setFilterValue(productsIdFilter.name);
drillThroughCardTitle("Orders");
queryBuilderMain().within(() => {
H.queryBuilderMain().within(() => {
cy.findByText("37.65").should("not.exist");
cy.findAllByText("105.12").should("have.length", 17);
});
assertQueryBuilderRowCount(191);
H.assertQueryBuilderRowCount(191);
postDrillAssertion("Product → ID is 2 selections");
});
function setFilterValue(filterName) {
filterWidget().contains(filterName).click();
H.filterWidget().contains(filterName).click();
cy.findByPlaceholderText("Enter an ID").type("1,2,");
cy.button("Add filter").click();
cy.findByText("2 selections");
@@ -1007,7 +989,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
function postDrillAssertion(filterName) {
cy.findByTestId("qb-filters-panel").findByText(filterName).click();
popover().within(() => {
H.popover().within(() => {
cy.findAllByRole("combobox")
.last()
.should("contain", "1")
@@ -1062,7 +1044,7 @@ function createDashboard(
],
});
addOrUpdateDashboardCard({
H.addOrUpdateDashboardCard({
card_id: questionId,
dashboard_id: dashboardId,
card: {
@@ -1087,7 +1069,7 @@ function setParamValue(paramName, text) {
// wait to leave editing mode and set a param value
cy.findByText("You're editing this dashboard.").should("not.exist");
cy.findByText(paramName).click();
popover().within(() => {
H.popover().within(() => {
cy.findByPlaceholderText("Search by Name").type(text);
cy.findByText("Add filter").click();
});

View File

@@ -1,26 +1,8 @@
import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data";
import {
createNewTab,
dashboardGrid,
describeWithSnowplow,
editDashboard,
enableTracking,
entityPickerModal,
expectGoodSnowplowEvent,
expectNoBadSnowplowEvents,
findDashCardAction,
getDashboardCard,
getDashboardCards,
goToTab,
menu,
modal,
resetSnowplow,
restore,
saveDashboard,
selectDashboardFilter,
sidebar,
visitDashboard,
} from "e2e/support/helpers";
ORDERS_DASHBOARD_ID,
READ_ONLY_PERSONAL_COLLECTION_ID,
} from "e2e/support/cypress_sample_instance_data";
import * as H from "e2e/support/helpers";
import { createMockParameter } from "metabase-types/api/mocks";
const CATEGORY_FILTER = createMockParameter({
@@ -29,32 +11,32 @@ const CATEGORY_FILTER = createMockParameter({
type: "string/=",
});
describeWithSnowplow("scenarios > dashboard cards > sections", () => {
H.describeWithSnowplow("scenarios > dashboard cards > sections", () => {
beforeEach(() => {
resetSnowplow();
restore();
H.resetSnowplow();
H.restore();
cy.signInAsAdmin();
enableTracking();
H.enableTracking();
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
cy.request("PUT", `/api/dashboard/${ORDERS_DASHBOARD_ID}`, {
parameters: [CATEGORY_FILTER],
});
visitDashboard(ORDERS_DASHBOARD_ID);
H.visitDashboard(ORDERS_DASHBOARD_ID);
});
afterEach(() => {
expectNoBadSnowplowEvents();
H.expectNoBadSnowplowEvents();
});
it("should add sections and select a question for an empty card", () => {
editDashboard();
H.editDashboard();
getDashboardCards().should("have.length", 1);
H.getDashboardCards().should("have.length", 1);
addSection("KPIs w/ large chart below");
getDashboardCards().should("have.length", 7);
expectGoodSnowplowEvent({
H.getDashboardCards().should("have.length", 7);
H.expectGoodSnowplowEvent({
event: "dashboard_section_added",
section_layout: "kpi_chart_below",
});
@@ -62,25 +44,25 @@ describeWithSnowplow("scenarios > dashboard cards > sections", () => {
cy.findByPlaceholderText("Heading").type("This is a heading");
selectQuestion("Orders, Count");
createNewTab();
getDashboardCards().should("have.length", 0);
H.createNewTab();
H.getDashboardCards().should("have.length", 0);
addSection("KPI grid");
getDashboardCards().should("have.length", 5);
expectGoodSnowplowEvent({
H.getDashboardCards().should("have.length", 5);
H.expectGoodSnowplowEvent({
event: "dashboard_section_added",
section_layout: "kpi_grid",
});
selectQuestion("Orders, Count, Grouped by Created At (year)");
mapDashCardToFilter(getDashboardCard(1), "Category");
overwriteDashCardTitle(getDashboardCard(1), "Line chart");
mapDashCardToFilter(H.getDashboardCard(1), "Category");
overwriteDashCardTitle(H.getDashboardCard(1), "Line chart");
goToTab("Tab 1");
saveDashboard();
H.goToTab("Tab 1");
H.saveDashboard();
dashboardGrid().within(() => {
getDashboardCards().should("have.length", 7);
H.dashboardGrid().within(() => {
H.getDashboardCards().should("have.length", 7);
cy.findAllByText("Select question").should("have.length", 0);
cy.findByText("This is a heading").should("exist");
@@ -93,10 +75,10 @@ describeWithSnowplow("scenarios > dashboard cards > sections", () => {
);
});
goToTab("Tab 2");
H.goToTab("Tab 2");
dashboardGrid().within(() => {
getDashboardCards().should("have.length", 5);
H.dashboardGrid().within(() => {
H.getDashboardCards().should("have.length", 5);
cy.findAllByText("Select question").should("have.length", 0);
cy.findByText("Line chart").should("exist");
@@ -110,35 +92,63 @@ describeWithSnowplow("scenarios > dashboard cards > sections", () => {
});
// Ensure parameter mapping is persisted
editDashboard();
H.editDashboard();
filterPanel().findByText("Category").click();
getDashboardCard(1).findByText("Product.Category").should("exist");
H.getDashboardCard(1).findByText("Product.Category").should("exist");
});
});
describe("scenarios > dashboard cards > sections > read only collections", () => {
beforeEach(() => {
H.restore();
cy.signIn("readonly");
});
it("Should allow you to select entites in collections you have read access to (metabase#50602)", () => {
H.createDashboard({ collection_id: READ_ONLY_PERSONAL_COLLECTION_ID }).then(
({ body }) => {
H.visitDashboard(body.id);
},
);
H.editDashboard();
addSection("KPIs w/ large chart below");
H.dashboardGrid()
.findAllByText("Select question")
.first()
.click({ force: true });
H.entityPickerModal()
.findByRole("tab", { name: /Questions/ })
.click();
H.entityPickerModalItem(0, "Our analytics").click();
H.entityPickerModalItem(1, "Orders, Count").click();
H.dashboardGrid().findByText("Orders, Count").should("exist");
});
});
function addSection(name) {
cy.findByLabelText("Add section").click();
menu().findByLabelText(name).click();
H.menu().findByLabelText(name).click();
}
function selectQuestion(question) {
dashboardGrid()
H.dashboardGrid()
.findAllByText("Select question")
.first()
.click({ force: true });
entityPickerModal()
H.entityPickerModal()
.findByRole("tab", { name: /Questions/ })
.click();
entityPickerModal().findByText(question).click();
H.entityPickerModal().findByText(question).click();
cy.wait("@cardQuery");
dashboardGrid().findByText(question).should("exist");
H.dashboardGrid().findByText(question).should("exist");
}
function overwriteDashCardTitle(dashcardElement, textTitle) {
findDashCardAction(dashcardElement, "Show visualization options").click({
H.findDashCardAction(dashcardElement, "Show visualization options").click({
force: true,
});
modal().within(() => {
H.modal().within(() => {
cy.findByLabelText("Title").type(`{selectall}{del}${textTitle}`).blur();
cy.button("Done").click();
});
@@ -150,6 +160,6 @@ function filterPanel() {
function mapDashCardToFilter(dashcardElement, filterName) {
filterPanel().findByText(filterName).click();
selectDashboardFilter(dashcardElement, filterName);
sidebar().button("Done").click();
H.selectDashboardFilter(dashcardElement, filterName);
H.sidebar().button("Done").click();
}

View File

@@ -1,24 +1,10 @@
import { H } from "e2e/support";
import { USER_GROUPS } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
FIRST_COLLECTION_ID,
ORDERS_COUNT_QUESTION_ID,
} from "e2e/support/cypress_sample_instance_data";
import {
describeWithSnowplow,
enableTracking,
entityPickerModal,
expectGoodSnowplowEvent,
expectNoBadSnowplowEvents,
findDashCardAction,
modal,
popover,
resetSnowplow,
restore,
saveDashboard,
undoToastList,
visitDashboard,
} from "e2e/support/helpers";
import {
createMockDashboardCard,
createMockHeadingDashboardCard,
@@ -114,12 +100,12 @@ function getDashboardCards(mappedQuestionId) {
];
}
describeWithSnowplow("scenarios > dashboard cards > replace question", () => {
H.describeWithSnowplow("scenarios > dashboard cards > replace question", () => {
beforeEach(() => {
resetSnowplow();
restore();
H.resetSnowplow();
H.restore();
cy.signInAsAdmin();
enableTracking();
H.enableTracking();
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
@@ -141,19 +127,19 @@ describeWithSnowplow("scenarios > dashboard cards > replace question", () => {
});
afterEach(() => {
expectNoBadSnowplowEvents();
H.expectNoBadSnowplowEvents();
});
it("should replace a dashboard card question (metabase#36984)", () => {
visitDashboardAndEdit();
findDashCardAction(findHeadingDashcard(), "Replace").should("not.exist");
H.findDashCardAction(findHeadingDashcard(), "Replace").should("not.exist");
// Ensure can replace with a question
replaceQuestion(findTargetDashcard(), {
nextQuestionName: "Orders",
});
expectGoodSnowplowEvent({ event: "dashboard_card_replaced" });
H.expectGoodSnowplowEvent({ event: "dashboard_card_replaced" });
findTargetDashcard().within(() => {
assertDashCardTitle("Orders");
cy.findByText("Product ID").should("exist");
@@ -171,7 +157,7 @@ describeWithSnowplow("scenarios > dashboard cards > replace question", () => {
});
// Ensure changes are persisted
saveDashboard();
H.saveDashboard();
findTargetDashcard().within(() => {
assertDashCardTitle("Orders Model");
cy.findByText("Product ID").should("exist");
@@ -193,7 +179,7 @@ describeWithSnowplow("scenarios > dashboard cards > replace question", () => {
});
// There're two toasts: "Undo replace" and "Auto-connect"
undoToastList().eq(0).button("Undo").click();
H.undoToastList().eq(0).button("Undo").click();
// Ensure we kept viz settings and parameter mapping changes from before
findTargetDashcard().within(() => {
@@ -208,7 +194,7 @@ describeWithSnowplow("scenarios > dashboard cards > replace question", () => {
});
// Ensure changes are persisted
saveDashboard();
H.saveDashboard();
findTargetDashcard().within(() => {
assertDashCardTitle("Custom name");
cy.findByText("18,760").should("exist");
@@ -238,7 +224,7 @@ describeWithSnowplow("scenarios > dashboard cards > replace question", () => {
});
// Ensure changes are persisted
saveDashboard();
H.saveDashboard();
findTargetDashcard().within(() => {
assertDashCardTitle("Next question");
cy.findByText("Ean").should("exist");
@@ -248,7 +234,7 @@ describeWithSnowplow("scenarios > dashboard cards > replace question", () => {
});
function visitDashboardAndEdit() {
visitDashboard("@dashboardId");
H.visitDashboard("@dashboardId");
cy.findByLabelText("Edit dashboard").click();
}
@@ -265,7 +251,7 @@ function replaceQuestion(
{ nextQuestionName, collectionName, tab },
) {
dashcardElement.realHover().findByLabelText("Replace").click();
entityPickerModal().within(() => {
H.entityPickerModal().within(() => {
if (tab) {
cy.findByRole("tablist").findByText(tab).click();
}
@@ -282,8 +268,8 @@ function assertDashCardTitle(title) {
}
function overwriteDashCardTitle(dashcardElement, textTitle) {
findDashCardAction(dashcardElement, "Show visualization options").click();
modal().within(() => {
H.findDashCardAction(dashcardElement, "Show visualization options").click();
H.modal().within(() => {
cy.findByLabelText("Title").type(`{selectall}{del}${textTitle}`).blur();
cy.button("Done").click();
});
@@ -295,7 +281,7 @@ function connectDashboardFilter(dashcardElement, { filterName, columnName }) {
);
filterPanel.findByText(filterName).click();
dashcardElement.button(/Select/).click();
popover().findByText(columnName).click();
H.popover().findByText(columnName).click();
filterPanel.findByText(filterName).click();
}

Some files were not shown because too many files have changed in this diff Show More