Compare commits

..
Author SHA1 Message Date
Beto Dealmeida 6eeb107b27 Small fixes 2026-04-23 14:05:55 -04:00
Beto Dealmeida e9a1513c39 Sort metrics/columns 2026-04-23 14:05:54 -04:00
Beto Dealmeida cb979b01e2 Disable adhoc 2026-04-23 14:05:54 -04:00
Beto Dealmeida 34f062c0a4 Small fix 2026-04-23 14:05:54 -04:00
Beto Dealmeida 727c61aa71 Disable adhoc SQL 2026-04-23 14:05:54 -04:00
Beto Dealmeida cb35bdf643 Dynamic columns/metrics 2026-04-23 14:05:54 -04:00
Beto Dealmeida b0a3661611 Explorable protocol 2026-04-23 14:05:54 -04:00
Beto Dealmeida 76dec98f33 feat: dynamic columns 2026-04-23 14:05:54 -04:00
Beto Dealmeida bde6bb1962 Fix edit dataset 2026-04-23 14:05:54 -04:00
Beto Dealmeida dc1fafce95 Small fixes 2026-04-23 14:05:54 -04:00
Beto Dealmeida 4b6cb09ed1 Fix lint 2026-04-23 14:05:54 -04:00
Beto Dealmeida 21b9ce3562 Fix filters 2026-04-23 14:05:54 -04:00
Beto Dealmeida b96dd6cf99 Working on filters 2026-04-23 14:05:54 -04:00
Beto Dealmeida 03f2530b9d Working on filters 2026-04-23 14:05:54 -04:00
Beto Dealmeida 8b9a0eae33 Fix header name 2026-04-23 14:05:54 -04:00
Beto Dealmeida 7c9ce279a9 Fix data connection name 2026-04-23 14:05:54 -04:00
Beto Dealmeida cac8656988 Small fixes 2026-04-23 14:05:54 -04:00
Beto Dealmeida 99893f75c4 Fix lint 2026-04-23 14:05:54 -04:00
Beto Dealmeida 72ad2acb86 Revert some aggressive renames 2026-04-23 14:05:54 -04:00
Beto Dealmeida 71f04979e9 More fixes 2026-04-23 14:05:54 -04:00
Beto Dealmeida b7a7d83ea5 chore: rename database/database when using semantic layers 2026-04-23 14:05:54 -04:00
Beto Dealmeida 3f0d302b56 Small fixes 2026-04-23 14:05:54 -04:00
Beto Dealmeida c7f4c1b818 Bulk delete 2026-04-23 14:05:53 -04:00
Beto Dealmeida 51c6708caa Address comments 2026-04-23 14:05:53 -04:00
Beto Dealmeida 8a9764a4b5 Improve design 2026-04-23 14:05:53 -04:00
Beto Dealmeida e7cb3c5a1e Fix lint/tests 2026-04-23 14:05:53 -04:00
Beto Dealmeida a86424bca3 Fix imports 2026-04-23 14:05:53 -04:00
Beto Dealmeida 6ca5464d27 feat: CRUD for adding/deleting semantic views 2026-04-23 14:05:53 -04:00
Beto Dealmeida 0f08f016d2 Address semantic layer review nits
Improve semantic layer schema refresh error handling and connections endpoint behavior to reduce noisy failures while keeping this feature branch focused. Also restore frontend typing consistency and add debounce coverage for dynamic schema refresh.
2026-04-23 14:00:59 -04:00
Beto Dealmeida 65fb2ff834 Fix rebase 2026-04-23 13:33:47 -04:00
Beto Dealmeida d659089c59 feat: UI for semantic layers 2026-04-23 13:33:47 -04:00
Beto Dealmeida 5e046a857c Update permissions 2026-04-23 13:33:47 -04:00
Beto Dealmeida 36554237aa Address comments 2026-04-23 13:33:47 -04:00
Beto Dealmeida 6f93e1cbb1 feat: API for semantic layers 2026-04-23 13:33:47 -04:00
Beto Dealmeida 913259299e Address comments 2026-04-23 13:26:40 -04:00
Beto Dealmeida 2351e0ead7 Move logic to commands 2026-04-16 18:16:23 -04:00
Beto Dealmeida 8c6f211003 Address comments 2026-04-16 18:16:23 -04:00
Beto Dealmeida 0e3d78817f Fix imports 2026-04-16 18:16:23 -04:00
Beto Dealmeida f0c8304e24 feat: UI for semantic views 2026-04-16 18:16:23 -04:00
Beto Dealmeida 80233aed46 Fix DAO 2026-04-16 18:16:23 -04:00
Beto Dealmeida 6f350428df Check uniqueness 2026-04-16 18:16:23 -04:00
Beto Dealmeida 548ccfde44 feat: API for semantic views 2026-04-16 18:16:23 -04:00
Beto Dealmeida 596008203c Fix Datasource type 2026-04-16 18:16:23 -04:00
Beto Dealmeida ff46c86df3 feat: Explore integration 2026-04-16 18:16:23 -04:00
Beto Dealmeida 4e30638024 Address more comments 2026-04-16 18:14:39 -04:00
Beto Dealmeida efa9159cc8 Address comments 2026-04-16 11:22:44 -04:00
Beto Dealmeida 14668f37bd Improvements 2026-03-10 15:32:07 -04:00
Beto Dealmeida 27a2466855 feat: models and DAOs 2026-03-10 14:15:07 -04:00
Beto Dealmeida e35c6946ec Fix lint/tests 2026-03-10 13:48:21 -04:00
Beto Dealmeida 12c5bfa0a5 Improve types 2026-03-10 12:56:09 -04:00
Beto Dealmeida 0303a234a3 Fix tests 2026-03-10 12:56:09 -04:00
Beto Dealmeida 09e9927652 Simplify 2026-03-10 12:56:09 -04:00
Beto Dealmeida 3f9ea361bb docs: add semantic layers to contribution types 2026-03-10 12:56:09 -04:00
Beto Dealmeida f1047140ee feat: add @semantic_layer decorator for extension discovery 2026-03-10 12:56:09 -04:00
Beto Dealmeida 15e3ab4493 Address comments 2026-03-10 12:56:09 -04:00
Beto Dealmeida 755aa2e32f Address TODOs 2026-03-10 12:56:09 -04:00
Beto Dealmeida 17d1ed7353 chore: remove AdhocFilter 2026-03-10 12:56:09 -04:00
Beto Dealmeida 9c1bcb70d0 feat: semantic layer extension 2026-03-10 12:56:09 -04:00
Amin GhadersohiandGitHub 6d7cfac8b2 fix(mcp): wrap LoggingMiddleware.on_message event_logger in try/except (#38560) 2026-03-10 17:48:08 +01:00
31754a39c9 fix(i18n): correct variable name for translated SQL Lab query message (#38494)
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-03-10 21:50:14 +07:00
Michael S. MolinaandGitHub bde48e563e fix: SQL Lab tab content padding (#38561) 2026-03-10 11:44:31 -03:00
629 changed files with 22216 additions and 20147 deletions
+20 -12
View File
@@ -63,7 +63,7 @@ jobs:
name: docker-image
path: docker-image.tar.gz
sharded-unit-tests:
sharded-jest-tests:
needs: frontend-build
if: needs.frontend-build.outputs.should-run == 'true'
strategy:
@@ -84,20 +84,19 @@ jobs:
run: |
mkdir -p ${{ github.workspace }}/superset-frontend/coverage
docker run \
-v ${{ github.workspace }}/superset-frontend/.vitest-reports:/app/superset-frontend/.vitest-reports \
-v ${{ github.workspace }}/superset-frontend/coverage:/app/superset-frontend/coverage \
--rm $TAG \
bash -c \
"npm run test -- --coverage.enabled --reporter=blob --shard=${{ matrix.shard }}/8"
"npm run test -- --coverage --shard=${{ matrix.shard }}/8 --coverageReporters=json"
- name: Upload Coverage Artifact
uses: actions/upload-artifact@v7
with:
name: blob-report-${{ matrix.shard }}
path: .vitest-reports/*
include-hidden-files: true
name: coverage-artifacts-${{ matrix.shard }}
path: superset-frontend/coverage
report-coverage:
needs: [sharded-unit-tests]
needs: [sharded-jest-tests]
if: needs.frontend-build.outputs.should-run == 'true'
runs-on: ubuntu-24.04
permissions:
@@ -113,12 +112,19 @@ jobs:
- name: Download Coverage Artifacts
uses: actions/download-artifact@v8
with:
pattern: blob-report-*
path: .vitest-reports
merge-multiple: true
pattern: coverage-artifacts-*
path: coverage/
- name: Merge reports
run: npx vitest --merge-reports
- name: Reorganize test result reports
run: |
find coverage/
for i in {1..8}; do
mv coverage/coverage-artifacts-${i}/coverage-final.json coverage/coverage-shard-${i}.json
done
shell: bash
- name: Merge Code Coverage
run: npx nyc merge coverage/ merged-output/coverage-summary.json
- name: Upload Code Coverage
uses: codecov/codecov-action@v5
@@ -126,6 +132,8 @@ jobs:
flags: javascript
use_oidc: true
verbose: true
disable_search: true
files: merged-output/coverage-summary.json
slug: apache/superset
lint-frontend:
@@ -52,6 +52,7 @@ jobs:
SUPERSET_SECRET_KEY: not-a-secret
run: |
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
pytest --durations-min=0.5 --cov=superset/semantic_layers/ ./tests/unit_tests/semantic_layers/ --cache-clear --cov-fail-under=100
- name: Upload code coverage
uses: codecov/codecov-action@v5
with:
+8
View File
@@ -24,6 +24,14 @@ assists people when migrating to a new version.
## Next
### Combined datasource list endpoint
Added a new combined datasource list endpoint at `GET /api/v1/datasource/` to serve datasets and semantic views in one response.
- The endpoint is available to users with at least one of `can_read` on `Dataset` or `SemanticView`.
- Semantic views are included only when the `SEMANTIC_LAYERS` feature flag is enabled.
- The endpoint enforces strict `order_column` validation and returns `400` for invalid sort columns.
### ClickHouse minimum driver version bump
The minimum required version of `clickhouse-connect` has been raised to `>=0.13.0`. If you are using the ClickHouse connector, please upgrade your `clickhouse-connect` package. The `_mutate_label` workaround that appended hash suffixes to column aliases has also been removed, as it is no longer needed with modern versions of the driver.
+7 -1
View File
@@ -105,7 +105,13 @@ class CeleryConfig:
CELERY_CONFIG = CeleryConfig
FEATURE_FLAGS = {"ALERT_REPORTS": True, "DATASET_FOLDERS": True}
FEATURE_FLAGS = {
"ALERT_REPORTS": True,
"DATASET_FOLDERS": True,
"ENABLE_EXTENSIONS": True,
"SEMANTIC_LAYERS": True,
}
EXTENSIONS_PATH = "/app/docker/extensions"
ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501
# The base URL for the email report hyperlinks.
@@ -224,3 +224,52 @@ async def analysis_guide(ctx: Context) -> str:
```
See [MCP Integration](./mcp) for implementation details.
### Semantic Layers
Extensions can register custom semantic layer implementations that allow Superset to connect to external data modeling frameworks. Each semantic layer defines how to authenticate, discover semantic views (tables/metrics/dimensions), and execute queries against the external system.
```python
from superset_core.semantic_layers.decorators import semantic_layer
from superset_core.semantic_layers.layer import SemanticLayer
from my_extension.config import MyConfig
from my_extension.view import MySemanticView
@semantic_layer(
id="my_platform",
name="My Data Platform",
description="Connect to My Data Platform's semantic layer",
)
class MySemanticLayer(SemanticLayer[MyConfig, MySemanticView]):
configuration_class = MyConfig
@classmethod
def from_configuration(cls, configuration: dict) -> "MySemanticLayer":
config = MyConfig.model_validate(configuration)
return cls(config)
@classmethod
def get_configuration_schema(cls, configuration=None) -> dict:
return MyConfig.model_json_schema()
@classmethod
def get_runtime_schema(cls, configuration=None, runtime_data=None) -> dict:
return {"type": "object", "properties": {}}
def get_semantic_views(self, runtime_configuration: dict) -> set[MySemanticView]:
# Return available views from the external platform
...
def get_semantic_view(self, name: str, additional_configuration: dict) -> MySemanticView:
# Return a specific view by name
...
```
**Note**: The `@semantic_layer` decorator automatically detects context and applies appropriate ID prefixing:
- **Extension context**: ID prefixed as `extensions.{publisher}.{name}.{id}`
- **Host context**: Original ID used as-is
The decorator registers the class in the semantic layers registry, making it available in the UI for users to create connections. The `configuration_class` should be a Pydantic model that defines the fields needed to connect (credentials, project, database, etc.). Superset uses the model's JSON schema to render the configuration form dynamically.
+6
View File
@@ -75,6 +75,12 @@
"lifecycle": "development",
"description": "Expand nested types in Presto into extra columns/arrays. Experimental, doesn't work with all nested types."
},
{
"name": "SEMANTIC_LAYERS",
"default": false,
"lifecycle": "development",
"description": "Enable semantic layers and show semantic views alongside datasets"
},
{
"name": "TABLE_V2_TIME_COMPARISON_ENABLED",
"default": false,
+1
View File
@@ -285,6 +285,7 @@ module = [
"superset.tags.filters",
"superset.commands.security.update",
"superset.commands.security.create",
"superset.semantic_layers.api",
]
warn_unused_ignores = false
+2
View File
@@ -43,6 +43,8 @@ classifiers = [
]
dependencies = [
"flask-appbuilder>=5.0.2,<6",
"isodate>=0.7.0",
"pyarrow>=16.0.0",
"pydantic>=2.8.0",
"sqlalchemy>=1.4.0,<2.0",
"sqlalchemy-utils>=0.38.0, <0.43", # expanding lowerbound to work with pydoris
@@ -0,0 +1,73 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
from typing import Any
from pydantic import BaseModel
def build_configuration_schema(
config_class: type[BaseModel],
configuration: BaseModel | None = None,
) -> dict[str, Any]:
"""
Build a JSON schema from a Pydantic configuration class.
Handles generic boilerplate that any semantic layer with dynamic fields needs:
- Reorders properties to match model field order (Pydantic sorts alphabetically)
- When ``configuration`` is None, sets ``enum: []`` on all ``x-dynamic`` properties
so the frontend renders them as empty dropdowns
Semantic layer implementations call this instead of
``model_json_schema()`` directly,
then only need to add their own dynamic population logic.
"""
schema = config_class.model_json_schema()
# Pydantic sorts properties alphabetically; restore model field order
field_order = [
field.alias or name for name, field in config_class.model_fields.items()
]
schema["properties"] = {
key: schema["properties"][key]
for key in field_order
if key in schema["properties"]
}
if configuration is None:
for prop_schema in schema["properties"].values():
if prop_schema.get("x-dynamic"):
prop_schema["enum"] = []
return schema
def check_dependencies(
prop_schema: dict[str, Any],
configuration: BaseModel,
) -> bool:
"""
Check whether a dynamic property's dependencies are satisfied.
Reads the ``x-dependsOn`` list from the property schema and returns ``True``
when every referenced attribute on ``configuration`` is truthy.
"""
dependencies = prop_schema.get("x-dependsOn", [])
return all(getattr(configuration, dep, None) for dep in dependencies)
@@ -0,0 +1,169 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Semantic layer DAO interfaces for superset-core.
Provides abstract DAO classes for semantic layers and views that define the
interface contract. Host implementations replace these with concrete classes
backed by SQLAlchemy during initialization.
Usage:
from superset_core.semantic_layers.daos import (
AbstractSemanticLayerDAO,
AbstractSemanticViewDAO,
)
"""
from __future__ import annotations
from abc import abstractmethod
from typing import Any, ClassVar
from superset_core.common.daos import BaseDAO
from superset_core.semantic_layers.models import SemanticLayerModel, SemanticViewModel
class AbstractSemanticLayerDAO(BaseDAO[SemanticLayerModel]):
"""
Abstract DAO interface for SemanticLayer.
Host implementations will replace this class during initialization
with a concrete DAO providing actual database access.
"""
model_cls: ClassVar[type[Any] | None] = None
base_filter = None
id_column_name = "uuid"
uuid_column_name = "uuid"
@classmethod
@abstractmethod
def validate_uniqueness(cls, name: str) -> bool:
"""
Validate that a semantic layer name is unique.
:param name: Semantic layer name to validate
:return: True if the name is unique, False otherwise
"""
...
@classmethod
@abstractmethod
def validate_update_uniqueness(cls, layer_uuid: str, name: str) -> bool:
"""
Validate that a semantic layer name is unique for an update operation,
excluding the layer being updated.
:param layer_uuid: UUID of the semantic layer being updated
:param name: New name to validate
:return: True if the name is unique, False otherwise
"""
...
@classmethod
@abstractmethod
def find_by_name(cls, name: str) -> SemanticLayerModel | None:
"""
Find a semantic layer by name.
:param name: Semantic layer name
:return: SemanticLayerModel instance or None
"""
...
@classmethod
@abstractmethod
def get_semantic_views(cls, layer_uuid: str) -> list[SemanticViewModel]:
"""
Get all semantic views associated with a semantic layer.
:param layer_uuid: UUID of the semantic layer
:return: List of SemanticViewModel instances
"""
...
class AbstractSemanticViewDAO(BaseDAO[SemanticViewModel]):
"""
Abstract DAO interface for SemanticView.
Host implementations will replace this class during initialization
with a concrete DAO providing actual database access.
"""
model_cls: ClassVar[type[Any] | None] = None
base_filter = None
id_column_name = "id"
uuid_column_name = "uuid"
@classmethod
@abstractmethod
def validate_uniqueness(
cls,
name: str,
layer_uuid: str,
configuration: dict[str, Any],
) -> bool:
"""
Validate that a semantic view is unique within a semantic layer.
Uniqueness is determined by the combination of name, layer UUID, and
configuration.
:param name: View name
:param layer_uuid: UUID of the parent semantic layer
:param configuration: Configuration dict to compare
:return: True if unique, False otherwise
"""
...
@classmethod
@abstractmethod
def validate_update_uniqueness(
cls,
view_uuid: str,
name: str,
layer_uuid: str,
configuration: dict[str, Any],
) -> bool:
"""
Validate that a semantic view is unique within a semantic layer for an
update operation, excluding the view being updated.
:param view_uuid: UUID of the view being updated
:param name: New name to validate
:param layer_uuid: UUID of the parent semantic layer
:param configuration: Configuration dict to compare
:return: True if unique, False otherwise
"""
...
@classmethod
@abstractmethod
def find_by_name(cls, name: str, layer_uuid: str) -> SemanticViewModel | None:
"""
Find a semantic view by name within a semantic layer.
:param name: View name
:param layer_uuid: UUID of the parent semantic layer
:return: SemanticViewModel instance or None
"""
...
__all__ = ["AbstractSemanticLayerDAO", "AbstractSemanticViewDAO"]
@@ -0,0 +1,102 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Semantic layer registration decorator for Superset.
This module provides a decorator interface to register semantic layer
implementations with the host application, enabling automatic discovery
by the extensions framework.
Usage:
from superset_core.semantic_layers.decorators import semantic_layer
@semantic_layer(
id="snowflake",
name="Snowflake Cortex",
description="Snowflake semantic layer via Cortex Analyst",
)
class SnowflakeSemanticLayer(SemanticLayer[SnowflakeConfig, SnowflakeView]):
...
# Or with minimal arguments:
@semantic_layer(id="dbt", name="dbt Semantic Layer")
class DbtSemanticLayer(SemanticLayer[DbtConfig, DbtView]):
...
"""
from __future__ import annotations
from typing import Callable, TypeVar
# Type variable for decorated semantic layer classes
T = TypeVar("T")
def semantic_layer(
id: str,
name: str,
description: str | None = None,
) -> Callable[[T], T]:
"""
Decorator to register a semantic layer implementation.
Automatically detects extension context and applies appropriate
namespacing to prevent ID conflicts between host and extension
semantic layers.
Host implementations will replace this function during initialization
with a concrete implementation providing actual functionality.
Args:
id: Unique semantic layer type identifier (e.g., "snowflake",
"dbt"). Used as the key in the semantic layers registry and
stored in the ``type`` column of the ``SemanticLayer`` model.
name: Human-readable display name (e.g., "Snowflake Cortex").
Shown in the UI when listing available semantic layer types.
description: Optional description for documentation and UI
tooltips.
Returns:
Decorated semantic layer class registered with the host
application.
Raises:
NotImplementedError: If called before host implementation is
initialized.
Example:
from superset_core.semantic_layers.decorators import semantic_layer
from superset_core.semantic_layers.layer import SemanticLayer
@semantic_layer(
id="snowflake",
name="Snowflake Cortex",
description="Connect to Snowflake Cortex Analyst",
)
class SnowflakeSemanticLayer(
SemanticLayer[SnowflakeConfig, SnowflakeView]
):
...
"""
raise NotImplementedError(
"Semantic layer decorator not initialized. "
"This decorator should be replaced during Superset startup."
)
__all__ = ["semantic_layer"]
@@ -0,0 +1,129 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Generic, TypeVar
from pydantic import BaseModel
from superset_core.semantic_layers.view import SemanticView
ConfigT = TypeVar("ConfigT", bound=BaseModel)
SemanticViewT = TypeVar("SemanticViewT", bound="SemanticView")
class SemanticLayer(ABC, Generic[ConfigT, SemanticViewT]):
"""
Abstract base class for semantic layers.
"""
configuration_class: type[BaseModel]
@classmethod
@abstractmethod
def from_configuration(
cls,
configuration: dict[str, Any],
) -> SemanticLayer[ConfigT, SemanticViewT]:
"""
Create a semantic layer from its configuration.
"""
raise NotImplementedError(
"Semantic layers must implement the from_configuration method"
)
@classmethod
@abstractmethod
def get_configuration_schema(
cls,
configuration: ConfigT | None = None,
) -> dict[str, Any]:
"""
Get the JSON schema for the configuration needed to add the semantic layer.
A partial configuration `configuration` can be sent to improve the schema,
allowing for progressive validation and better UX. For example, a semantic
layer might require:
- auth information
- a database
If the user provides the auth information, a client can send the partial
configuration to this method, and the resulting JSON schema would include
the list of databases the user has access to, allowing a dropdown to be
populated.
The Snowflake semantic layer has an example implementation of this method, where
database and schema names are populated based on the provided connection info.
"""
raise NotImplementedError(
"Semantic layers must implement the get_configuration_schema method"
)
@classmethod
@abstractmethod
def get_runtime_schema(
cls,
configuration: ConfigT,
runtime_data: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""
Get the JSON schema for the runtime parameters needed to load semantic views.
This returns the schema needed to connect to a semantic view given the
configuration for the semantic layer. For example, a semantic layer might
be configured by:
- auth information
- an optional database
If the user does not provide a database when creating the semantic layer, the
runtime schema would require the database name to be provided before loading any
semantic views. This allows users to create semantic layers that connect to a
specific database (or project, account, etc.), or that allow users to select it
at query time.
The Snowflake semantic layer has an example implementation of this method, where
database and schema names are required if they were not provided in the initial
configuration.
"""
raise NotImplementedError(
"Semantic layers must implement the get_runtime_schema method"
)
@abstractmethod
def get_semantic_views(
self,
runtime_configuration: dict[str, Any],
) -> set[SemanticViewT]:
"""
Get the semantic views available in the semantic layer.
The runtime configuration can provide information like a given project or
schema, used to restrict the semantic views returned.
"""
@abstractmethod
def get_semantic_view(
self,
name: str,
additional_configuration: dict[str, Any],
) -> SemanticViewT:
"""
Get a specific semantic view by its name and additional configuration.
"""
@@ -0,0 +1,85 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Semantic layer model interfaces for superset-core.
Provides abstract model classes for semantic layers and views that will be
replaced by the host implementation's concrete SQLAlchemy models during
initialization.
Usage:
from superset_core.semantic_layers.models import (
SemanticLayerModel,
SemanticViewModel,
)
"""
from __future__ import annotations
from datetime import datetime
from uuid import UUID
from superset_core.common.models import CoreModel
class SemanticLayerModel(CoreModel):
"""
Abstract interface for the SemanticLayer database model.
Host implementations will replace this class during initialization
with a concrete SQLAlchemy model providing actual persistence.
"""
__abstract__ = True
# Type hints for expected column attributes
uuid: UUID
name: str
description: str | None
type: str
configuration: str
configuration_version: int
cache_timeout: int | None
created_on: datetime | None
changed_on: datetime | None
class SemanticViewModel(CoreModel):
"""
Abstract interface for the SemanticView database model.
Host implementations will replace this class during initialization
with a concrete SQLAlchemy model providing actual persistence.
"""
__abstract__ = True
# Type hints for expected column attributes
id: int
uuid: UUID
name: str
description: str | None
configuration: str
configuration_version: int
cache_timeout: int | None
semantic_layer_uuid: UUID
created_on: datetime | None
changed_on: datetime | None
__all__ = ["SemanticLayerModel", "SemanticViewModel"]
@@ -0,0 +1,209 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
import enum
from dataclasses import dataclass
from datetime import date, datetime, time, timedelta
import isodate
import pyarrow as pa
@dataclass(frozen=True)
class Grain:
"""
Represents a time grain (e.g., day, month, year).
Attributes:
name: Human-readable name of the grain (e.g., "Second")
representation: ISO 8601 duration (e.g., "PT1S", "P1D", "P1M")
"""
name: str
representation: str
def __post_init__(self) -> None:
isodate.parse_duration(self.representation)
def __eq__(self, other: object) -> bool:
if isinstance(other, Grain):
return self.representation == other.representation
return NotImplemented
def __hash__(self) -> int:
return hash(self.representation)
class Grains:
"""Pre-defined common grains and factory for custom ones."""
SECOND = Grain("Second", "PT1S")
MINUTE = Grain("Minute", "PT1M")
HOUR = Grain("Hour", "PT1H")
DAY = Grain("Day", "P1D")
WEEK = Grain("Week", "P1W")
MONTH = Grain("Month", "P1M")
QUARTER = Grain("Quarter", "P3M")
YEAR = Grain("Year", "P1Y")
_REGISTRY: dict[str, Grain] = {
"PT1S": SECOND,
"PT1M": MINUTE,
"PT1H": HOUR,
"P1D": DAY,
"P1W": WEEK,
"P1M": MONTH,
"P3M": QUARTER,
"P1Y": YEAR,
}
@classmethod
def get(cls, representation: str, name: str | None = None) -> Grain:
"""Return a pre-defined grain or create a custom one."""
if grain := cls._REGISTRY.get(representation):
return grain
return Grain(name or representation, representation)
@dataclass(frozen=True)
class Dimension:
id: str
name: str
type: pa.DataType
definition: str | None = None
description: str | None = None
grain: Grain | None = None
@dataclass(frozen=True)
class Metric:
id: str
name: str
type: pa.DataType
definition: str
description: str | None = None
@dataclass(frozen=True)
class AdhocExpression:
id: str
definition: str
class Operator(str, enum.Enum):
EQUALS = "="
NOT_EQUALS = "!="
GREATER_THAN = ">"
LESS_THAN = "<"
GREATER_THAN_OR_EQUAL = ">="
LESS_THAN_OR_EQUAL = "<="
IN = "IN"
NOT_IN = "NOT IN"
LIKE = "LIKE"
NOT_LIKE = "NOT LIKE"
IS_NULL = "IS NULL"
IS_NOT_NULL = "IS NOT NULL"
ADHOC = "ADHOC"
FilterValues = str | int | float | bool | datetime | date | time | timedelta | None
class PredicateType(enum.Enum):
WHERE = "WHERE"
HAVING = "HAVING"
@dataclass(frozen=True, order=True)
class Filter:
type: PredicateType
column: Dimension | Metric | None
operator: Operator
value: FilterValues | frozenset[FilterValues]
class OrderDirection(enum.Enum):
ASC = "ASC"
DESC = "DESC"
OrderTuple = tuple[Metric | Dimension | AdhocExpression, OrderDirection]
@dataclass(frozen=True)
class GroupLimit:
"""
Limit query to top/bottom N combinations of specified dimensions.
The `filters` parameter allows specifying separate filter constraints for the
group limit subquery. This is useful when you want to determine the top N groups
using different criteria (e.g., a different time range) than the main query.
For example, you might want to find the top 10 products by sales over the last
30 days, but then show daily sales for those products over the last 7 days.
"""
dimensions: list[Dimension]
top: int
metric: Metric | None
direction: OrderDirection = OrderDirection.DESC
group_others: bool = False
filters: set[Filter] | None = None
@dataclass(frozen=True)
class SemanticRequest:
"""
Represents a request made to obtain semantic results.
This could be a SQL query, an HTTP request, etc.
"""
type: str
definition: str
@dataclass(frozen=True)
class SemanticResult:
"""
Represents the results of a semantic query.
This includes any requests (SQL queries, HTTP requests) that were performed in order
to obtain the results, in order to help troubleshooting.
"""
requests: list[SemanticRequest]
results: pa.Table
@dataclass(frozen=True)
class SemanticQuery:
"""
Represents a semantic query.
"""
metrics: list[Metric]
dimensions: list[Dimension]
filters: set[Filter] | None = None
order: list[OrderTuple] | None = None
limit: int | None = None
offset: int | None = None
group_limit: GroupLimit | None = None
@@ -0,0 +1,113 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
import enum
from abc import ABC, abstractmethod
from superset_core.semantic_layers.types import (
Dimension,
Filter,
Metric,
SemanticQuery,
SemanticResult,
)
# TODO (betodealmeida): move to the extension JSON
class SemanticViewFeature(enum.Enum):
"""
Custom features supported by semantic layers.
"""
ADHOC_EXPRESSIONS_IN_ORDERBY = "ADHOC_EXPRESSIONS_IN_ORDERBY"
GROUP_LIMIT = "GROUP_LIMIT"
GROUP_OTHERS = "GROUP_OTHERS"
class SemanticView(ABC):
"""
Abstract base class for semantic views.
"""
features: frozenset[SemanticViewFeature]
# Implementations must expose a display name for the view.
# Declared here as a type annotation (not abstract) so that existing
# implementations are not required to add a formal @abstractmethod.
name: str
@abstractmethod
def uid(self) -> str:
"""
Returns a unique identifier for the semantic view.
"""
@abstractmethod
def get_dimensions(self) -> set[Dimension]:
"""
Get the dimensions defined in the semantic view.
"""
@abstractmethod
def get_metrics(self) -> set[Metric]:
"""
Get the metrics defined in the semantic view.
"""
@abstractmethod
def get_values(
self,
dimension: Dimension,
filters: set[Filter] | None = None,
) -> SemanticResult:
"""
Return distinct values for a dimension.
"""
@abstractmethod
def get_table(self, query: SemanticQuery) -> SemanticResult:
"""
Execute a semantic query and return the results.
"""
@abstractmethod
def get_row_count(self, query: SemanticQuery) -> SemanticResult:
"""
Execute a query and return the number of rows the result would have.
"""
@abstractmethod
def get_compatible_metrics(
self,
selected_metrics: set[Metric],
selected_dimensions: set[Dimension],
) -> set[Metric]:
"""
Return metrics compatible with the selected dimensions.
"""
@abstractmethod
def get_compatible_dimensions(
self,
selected_metrics: set[Metric],
selected_dimensions: set[Dimension],
) -> set[Dimension]:
"""
Return dimensions compatible with the selected metrics.
"""
+1 -1
View File
@@ -48,7 +48,7 @@ module.exports = {
// but not test __mocks__ directories (e.g., packages/superset-ui-core/test/__mocks/)
'<rootDir>/packages/[^/]+/__mocks__',
],
setupFilesAfterEnv: ['<rootDir>/spec/helpers/setup.jest.ts'],
setupFilesAfterEnv: ['<rootDir>/spec/helpers/setup.ts'],
snapshotSerializers: ['@emotion/jest/serializer'],
testEnvironmentOptions: {
globalsCleanup: true,
+1 -2
View File
@@ -19,8 +19,7 @@
"__webpack_public_path__": "writable",
"__webpack_init_sharing__": "readonly",
"__webpack_share_scopes__": "readonly",
"jest": "readonly",
"vi": "readonly"
"jest": "readonly"
},
"settings": {
"react": {
+393 -1715
View File
File diff suppressed because it is too large Load Diff
+12 -8
View File
@@ -81,9 +81,9 @@
"storybook": "cross-env NODE_ENV=development BABEL_ENV=development storybook dev -p 6006",
"test-storybook": "test-storybook",
"test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"npx http-server storybook-static --port 6006 --silent\" \"npx wait-on tcp:127.0.0.1:6006 && npm run test-storybook -- --maxWorkers=2\"",
"tdd": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" vitest",
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" vitest --run --silent",
"test-loud": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" vitest --run",
"tdd": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --watch",
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80% --silent",
"test-loud": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80%",
"type": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" tsc --noEmit",
"update-maps": "cd plugins/legacy-plugin-chart-country-map/scripts && jupyter nbconvert --to notebook --execute --inplace --allow-errors --ExecutePreprocessor.timeout=1200 'Country Map GeoJSON Generator.ipynb'",
"validate-release": "../RELEASING/validate_this_release.sh"
@@ -117,7 +117,14 @@
"@luma.gl/gltf": "~9.2.5",
"@luma.gl/shadertools": "~9.2.5",
"@luma.gl/webgl": "~9.2.5",
"@fontsource/fira-code": "^5.2.7",
"@fontsource/inter": "^5.2.8",
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
"@jsonforms/core": "^3.7.0",
"@jsonforms/react": "^3.7.0",
"@jsonforms/vanilla-renderers": "^3.7.0",
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/antd": "^5.24.13",
"@rjsf/core": "^5.24.13",
"@rjsf/utils": "^5.24.3",
"@rjsf/validator-ajv8": "^5.24.13",
@@ -280,6 +287,7 @@
"@testing-library/user-event": "^12.8.3",
"@types/content-disposition": "^0.5.9",
"@types/dom-to-image": "^2.6.7",
"@types/jest": "^30.0.0",
"@types/js-levenshtein": "^1.1.3",
"@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15",
@@ -299,8 +307,6 @@
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitejs/plugin-react": "^5.1.4",
"@vitest/coverage-v8": "^4.0.18",
"babel-jest": "^30.0.2",
"babel-loader": "^10.0.0",
"babel-plugin-dynamic-import-node": "^2.3.3",
@@ -338,6 +344,7 @@
"imports-loader": "^5.0.0",
"jest": "^30.3.0",
"jest-environment-jsdom": "^29.7.0",
"jest-html-reporter": "^4.3.0",
"jest-websocket-mock": "^2.5.0",
"js-yaml-loader": "^1.2.2",
"jsdom": "^28.1.0",
@@ -366,9 +373,6 @@
"tsx": "^4.21.0",
"typescript": "5.4.5",
"unzipper": "^0.12.3",
"vite-plugin-svgr": "^4.5.0",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.0.18",
"vm-browserify": "^1.1.2",
"wait-on": "^9.0.4",
"webpack": "^5.105.4",
@@ -36,6 +36,7 @@
"devDependencies": {
"cross-env": "^10.1.0",
"fs-extra": "^11.3.3",
"jest": "^30.3.0",
"yeoman-test": "^11.3.1"
},
"engines": {
@@ -47,7 +47,7 @@ test('generator-superset:plugin-chart:creates files', async () => {
result.assertFile([
'.gitignore',
'babel.config.js',
'vi.config.js',
'jest.config.js',
'package.json',
'README.md',
'src/plugin/buildQuery.ts',
@@ -21,13 +21,13 @@ import { Theme } from './Theme';
import { AnyThemeConfig, ThemeAlgorithm } from './types';
// Mock emotion's cache to avoid actual DOM operations
vi.mock('@emotion/cache', () => ({
jest.mock('@emotion/cache', () => ({
__esModule: true,
default: vi.fn().mockReturnValue({}),
default: jest.fn().mockReturnValue({}),
}));
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
test('Theme.json serializes the theme configuration to a JSON string', () => {
@@ -31,16 +31,16 @@ import { Theme } from '../Theme';
import { ThemeAlgorithm } from '../types';
// Mock emotion's cache to avoid actual DOM operations
vi.mock('@emotion/cache', () => ({
jest.mock('@emotion/cache', () => ({
__esModule: true,
default: vi.fn().mockReturnValue({}),
default: jest.fn().mockReturnValue({}),
}));
let lightTheme: Theme;
let darkTheme: Theme;
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
// Create actual theme instances for testing
lightTheme = Theme.fromConfig({
@@ -17,33 +17,33 @@
* under the License.
*/
beforeEach(() => {
vi.resetModules();
vi.resetAllMocks();
jest.resetModules();
jest.resetAllMocks();
});
test('should pipe to `console` methods', () => {
const { logging } = require('@apache-superset/core/utils');
vi.spyOn(logging, 'debug').mockImplementation(() => {});
vi.spyOn(logging, 'log').mockImplementation(() => {});
vi.spyOn(logging, 'info').mockImplementation(() => {});
jest.spyOn(logging, 'debug').mockImplementation();
jest.spyOn(logging, 'log').mockImplementation();
jest.spyOn(logging, 'info').mockImplementation();
expect(() => {
logging.debug();
logging.log();
logging.info();
}).not.toThrow();
vi.spyOn(logging, 'warn').mockImplementation(() => {
jest.spyOn(logging, 'warn').mockImplementation(() => {
throw new Error('warn');
});
expect(() => logging.warn()).toThrow('warn');
vi.spyOn(logging, 'error').mockImplementation(() => {
jest.spyOn(logging, 'error').mockImplementation(() => {
throw new Error('error');
});
expect(() => logging.error()).toThrow('error');
vi.spyOn(logging, 'trace').mockImplementation(() => {
jest.spyOn(logging, 'trace').mockImplementation(() => {
throw new Error('Trace:');
});
expect(() => logging.trace()).toThrow('Trace:');
@@ -21,10 +21,10 @@ import { render } from '@superset-ui/core/spec';
import { GenericDataType } from '@apache-superset/core/common';
import { ColumnOption, ColumnOptionProps } from '../../src';
vi.mock('@superset-ui/chart-controls/components/SQLPopover', () => ({
jest.mock('@superset-ui/chart-controls/components/SQLPopover', () => ({
SQLPopover: () => <div data-test="mock-sql-popover" />,
}));
vi.mock(
jest.mock(
'@superset-ui/chart-controls/components/ColumnTypeLabel/ColumnTypeLabel',
() => ({
ColumnTypeLabel: ({ type }: { type: string }) => (
@@ -33,7 +33,7 @@ vi.mock(
}),
);
vi.mock('@superset-ui/core/components/InfoTooltip', () => ({
jest.mock('@superset-ui/core/components/InfoTooltip', () => ({
InfoTooltip: () => <div data-test="mock-tooltip" />,
}));
@@ -20,7 +20,7 @@ import '@testing-library/jest-dom';
import { fireEvent, render } from '@superset-ui/core/spec';
import { InfoTooltip, InfoTooltipProps } from '@superset-ui/core/components';
vi.mock('@superset-ui/core/components/Tooltip', () => ({
jest.mock('@superset-ui/core/components/Tooltip', () => ({
Tooltip: ({ children }: { children: React.ReactNode }) => (
<div data-test="mock-tooltip">{children}</div>
),
@@ -40,7 +40,7 @@ test('renders a tooltip', () => {
});
test('responds to keydown events', () => {
const clickHandler = vi.fn();
const clickHandler = jest.fn();
const { getByRole } = setup({
label: 'test',
tooltip: 'this is a test',
@@ -23,24 +23,24 @@ import {
MetricOptionProps,
} from '../../src/components/MetricOption';
vi.mock('@superset-ui/core/components/InfoTooltip', () => ({
jest.mock('@superset-ui/core/components/InfoTooltip', () => ({
InfoTooltip: () => <div data-test="mock-tooltip" />,
}));
vi.mock(
jest.mock(
'@superset-ui/chart-controls/components/ColumnTypeLabel/ColumnTypeLabel',
() => ({
ColumnTypeLabel: () => <div data-test="mock-column-type-label" />,
}),
);
vi.mock(
jest.mock(
'@superset-ui/core/components/Tooltip',
() =>
({ children }: { children: React.ReactNode }) => (
<div data-test="mock-tooltip">{children}</div>
),
);
vi.mock('@superset-ui/chart-controls/components/SQLPopover', () => ({
jest.mock('@superset-ui/chart-controls/components/SQLPopover', () => ({
SQLPopover: () => <div data-test="mock-sql-popover" />,
}));
@@ -31,7 +31,7 @@ const defaultProps: RadioButtonControlProps = {
['option2', 'Option 2'],
['option3', 'Option 3'],
],
onChange: vi.fn(),
onChange: jest.fn(),
};
const setup = (props: Partial<RadioButtonControlProps> = {}) =>
@@ -89,7 +89,7 @@ test('respects initial value prop', () => {
});
test('calls onChange when radio button is clicked', () => {
const onChange = vi.fn();
const onChange = jest.fn();
setup({ onChange });
const secondOption = screen.getByText('Option 2');
@@ -100,7 +100,7 @@ test('calls onChange when radio button is clicked', () => {
});
test('handles multiple clicks correctly', () => {
const onChange = vi.fn();
const onChange = jest.fn();
setup({ onChange });
fireEvent.click(screen.getByText('Option 2'));
@@ -130,7 +130,7 @@ test('disables specific options when disabled flag is set', () => {
});
test('disabled options do not trigger onChange when clicked', () => {
const onChange = vi.fn();
const onChange = jest.fn();
const optionsWithDisabled: RadioButtonOption[] = [
{ value: 'opt1', label: 'Enabled' },
{ value: 'opt2', label: 'Disabled', disabled: true },
@@ -240,7 +240,7 @@ test('focuses button when clicked', () => {
});
test('handles numeric values in options', () => {
const onChange = vi.fn();
const onChange = jest.fn();
const numericOptions: RadioButtonOption[] = [
[1, 'One'],
[2, 'Two'],
@@ -254,7 +254,7 @@ test('handles numeric values in options', () => {
});
test('handles boolean values in options', () => {
const onChange = vi.fn();
const onChange = jest.fn();
const booleanOptions: RadioButtonOption[] = [
[true, 'True'],
[false, 'False'],
@@ -267,7 +267,7 @@ test('handles boolean values in options', () => {
});
test('handles null values in options', () => {
const onChange = vi.fn();
const onChange = jest.fn();
const nullOptions: RadioButtonOption[] = [
[null, 'None'],
['value', 'Value'],
@@ -310,7 +310,7 @@ test('does not set aria-selected to true for unselected buttons', () => {
});
test('backward compatibility with legacy array format', () => {
const onChange = vi.fn();
const onChange = jest.fn();
const legacyOptions: RadioButtonOption[] = [
['val1', 'Label 1'],
['val2', 'Label 2'],
@@ -327,7 +327,7 @@ test('backward compatibility with legacy array format', () => {
test('normalizeOption handles array format correctly', () => {
const arrayOption: RadioButtonOption = ['value', 'Label'];
const onChange = vi.fn();
const onChange = jest.fn();
setup({ options: [arrayOption], onChange });
@@ -343,7 +343,7 @@ test('normalizeOption handles object format correctly', () => {
label: 'Label',
disabled: false,
};
const onChange = vi.fn();
const onChange = jest.fn();
setup({ options: [objectOption], onChange });
@@ -22,14 +22,14 @@ import { xAxisForceCategoricalControl } from '../../src/shared-controls/customCo
import { checkColumnType } from '../../src/utils/checkColumnType';
import type { ControlState } from '@superset-ui/chart-controls';
vi.mock('../../src/utils/checkColumnType');
vi.mock('@superset-ui/core', async importActual => ({
...(await importActual()),
getColumnLabel: vi.fn((col: any) => col),
jest.mock('../../src/utils/checkColumnType');
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
getColumnLabel: jest.fn((col: any) => col),
}));
test('xAxisForceCategoricalControl should not treat temporal columns as categorical when x_axis_sort exists', () => {
const mockCheckColumnType = vi.mocked(checkColumnType);
const mockCheckColumnType = jest.mocked(checkColumnType);
mockCheckColumnType.mockReturnValue(false); // temporal column (not numeric)
@@ -20,12 +20,12 @@
import { ControlPanelState } from '../../src/types';
// Mock the utilities to avoid complex dependencies
vi.mock('../../src/utils', () => ({
formatSelectOptions: vi.fn((options: any[]) =>
jest.mock('../../src/utils', () => ({
formatSelectOptions: jest.fn((options: any[]) =>
options.map((opt: any) => [opt, opt]),
),
displayTimeRelatedControls: vi.fn(() => true),
getColorControlsProps: vi.fn(() => ({})),
displayTimeRelatedControls: jest.fn(() => true),
getColorControlsProps: jest.fn(() => ({})),
D3_FORMAT_OPTIONS: [],
D3_FORMAT_DOCS: '',
D3_TIME_FORMAT_OPTIONS: [],
@@ -21,7 +21,7 @@ import { displayTimeRelatedControls } from '../../src';
const mockData = {
actions: {
setDatasource: vi.fn(),
setDatasource: jest.fn(),
},
controls: {
x_axis: {
@@ -83,6 +83,7 @@
"@types/rison": "0.1.0",
"@types/seedrandom": "^3.0.8",
"fetch-mock": "^12.6.0",
"jest-mock-console": "^2.0.0",
"resize-observer-polyfill": "1.5.1",
"timezone-mock": "1.4.0"
},
@@ -24,7 +24,7 @@ import MatrixifyGridCell from './MatrixifyGridCell';
import { MatrixifyGridCell as MatrixifyGridCellType } from '../../types/matrixify';
// Mock StatefulChart component
vi.mock('../StatefulChart', () => {
jest.mock('../StatefulChart', () => {
/* eslint-disable no-restricted-syntax, global-require, @typescript-eslint/no-var-requires */
const React = require('react');
/* eslint-enable no-restricted-syntax, global-require, @typescript-eslint/no-var-requires */
@@ -23,22 +23,21 @@ import { ThemeProvider } from '@apache-superset/core/theme';
import { supersetTheme } from '@apache-superset/core/theme';
import MatrixifyGridRenderer from './MatrixifyGridRenderer';
import { generateMatrixifyGrid } from './MatrixifyGridGenerator';
import { Mock } from 'vitest';
// Mock the MatrixifyGridGenerator
vi.mock('./MatrixifyGridGenerator', () => ({
generateMatrixifyGrid: vi.fn(),
jest.mock('./MatrixifyGridGenerator', () => ({
generateMatrixifyGrid: jest.fn(),
}));
// Mock MatrixifyGridCell component
vi.mock('./MatrixifyGridCell', () =>
jest.mock('./MatrixifyGridCell', () =>
// eslint-disable-next-line react/display-name, @typescript-eslint/no-unused-vars
({ cell, rowHeight, datasource, hooks }: any) => (
<div data-testid={`grid-cell-${cell.id}`}>Cell: {cell.id}</div>
),
);
const mockGenerateMatrixifyGrid = generateMatrixifyGrid as Mock<
const mockGenerateMatrixifyGrid = generateMatrixifyGrid as jest.MockedFunction<
typeof generateMatrixifyGrid
>;
@@ -46,7 +45,7 @@ const renderWithTheme = (component: React.ReactElement) =>
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
test('should create single group when fitting columns dynamically', () => {
@@ -28,13 +28,13 @@ import getChartBuildQueryRegistry from '../registries/ChartBuildQueryRegistrySin
configure({ testIdAttribute: 'data-test' });
// Mock the registries
vi.mock('../registries/ChartControlPanelRegistrySingleton');
vi.mock('../registries/ChartMetadataRegistrySingleton');
vi.mock('../registries/ChartBuildQueryRegistrySingleton');
vi.mock('../clients/ChartClient');
jest.mock('../registries/ChartControlPanelRegistrySingleton');
jest.mock('../registries/ChartMetadataRegistrySingleton');
jest.mock('../registries/ChartBuildQueryRegistrySingleton');
jest.mock('../clients/ChartClient');
// Mock SuperChart component
vi.mock('./SuperChart', () => ({
jest.mock('./SuperChart', () => ({
__esModule: true,
// eslint-disable-next-line react/display-name
default: ({ formData }: any) => (
@@ -43,18 +43,18 @@ vi.mock('./SuperChart', () => ({
}));
// Mock Loading component
vi.mock('../../components/Loading', () => ({
jest.mock('../../components/Loading', () => ({
// eslint-disable-next-line react/display-name
Loading: () => <div data-test="loading">Loading...</div>,
}));
const mockChartClient = {
client: {
post: vi.fn().mockResolvedValue({
post: jest.fn().mockResolvedValue({
json: [{ data: 'test data' }],
}),
},
loadFormData: vi.fn(),
loadFormData: jest.fn(),
};
const mockFormData = {
@@ -64,21 +64,21 @@ const mockFormData = {
};
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
// Setup default registry mocks
(getChartMetadataRegistry as any).mockReturnValue({
get: vi.fn().mockReturnValue({
get: jest.fn().mockReturnValue({
useLegacyApi: false,
}),
});
(getChartBuildQueryRegistry as any).mockReturnValue({
get: vi.fn().mockResolvedValue(null),
get: jest.fn().mockResolvedValue(null),
});
(getChartControlPanelRegistry as any).mockReturnValue({
get: vi.fn().mockReturnValue(null),
get: jest.fn().mockReturnValue(null),
});
// Mock ChartClient constructor
@@ -114,7 +114,7 @@ test('should refetch data when non-renderTrigger control changes', async () => {
};
(getChartControlPanelRegistry as any).mockReturnValue({
get: vi.fn().mockReturnValue(controlPanelConfig),
get: jest.fn().mockReturnValue(controlPanelConfig),
});
const { rerender } = render(
@@ -166,7 +166,7 @@ test('should NOT refetch data when only renderTrigger controls change', async ()
};
(getChartControlPanelRegistry as any).mockReturnValue({
get: vi.fn().mockReturnValue(controlPanelConfig),
get: jest.fn().mockReturnValue(controlPanelConfig),
});
const { rerender, getByTestId } = render(
@@ -202,7 +202,7 @@ test('should NOT refetch data when only renderTrigger controls change', async ()
test('should refetch when control panel config is not available', async () => {
// No control panel config available
(getChartControlPanelRegistry as any).mockReturnValue({
get: vi.fn().mockReturnValue(null),
get: jest.fn().mockReturnValue(null),
});
const { rerender } = render(
@@ -246,7 +246,7 @@ test('should refetch when viz_type changes', async () => {
};
(getChartControlPanelRegistry as any).mockReturnValue({
get: vi.fn().mockReturnValue(controlPanelConfig),
get: jest.fn().mockReturnValue(controlPanelConfig),
});
const { rerender } = render(
@@ -300,7 +300,7 @@ test('should handle mixed renderTrigger and non-renderTrigger changes', async ()
};
(getChartControlPanelRegistry as any).mockReturnValue({
get: vi.fn().mockReturnValue(controlPanelConfig),
get: jest.fn().mockReturnValue(controlPanelConfig),
});
const { rerender } = render(
@@ -353,7 +353,7 @@ test('should handle controls with complex structure', async () => {
};
(getChartControlPanelRegistry as any).mockReturnValue({
get: vi.fn().mockReturnValue(controlPanelConfig),
get: jest.fn().mockReturnValue(controlPanelConfig),
});
const { rerender, getByTestId } = render(
@@ -405,7 +405,7 @@ test('should not refetch when formData has not changed', async () => {
test('should handle errors gracefully when accessing registry', async () => {
// Mock registry to throw an error
(getChartControlPanelRegistry as any).mockReturnValue({
get: vi.fn().mockImplementation(() => {
get: jest.fn().mockImplementation(() => {
throw new Error('Registry error');
}),
});
@@ -493,7 +493,7 @@ test('should NOT refetch data when string-based renderTrigger control (zoomable)
};
(getChartControlPanelRegistry as any).mockReturnValue({
get: vi.fn().mockReturnValue(controlPanelConfig),
get: jest.fn().mockReturnValue(controlPanelConfig),
});
const formDataWithZoom = {
@@ -543,7 +543,7 @@ test('should NOT refetch data when other string-based renderTrigger controls cha
};
(getChartControlPanelRegistry as any).mockReturnValue({
get: vi.fn().mockReturnValue(controlPanelConfig),
get: jest.fn().mockReturnValue(controlPanelConfig),
});
const { rerender, getByTestId } = render(
@@ -586,7 +586,7 @@ test('should refetch when string control is NOT in RENDER_TRIGGER_SHARED_CONTROL
};
(getChartControlPanelRegistry as any).mockReturnValue({
get: vi.fn().mockReturnValue(controlPanelConfig),
get: jest.fn().mockReturnValue(controlPanelConfig),
});
const { rerender } = render(
@@ -632,7 +632,7 @@ test('should handle mixed string and object controls correctly', async () => {
};
(getChartControlPanelRegistry as any).mockReturnValue({
get: vi.fn().mockReturnValue(controlPanelConfig),
get: jest.fn().mockReturnValue(controlPanelConfig),
});
const formDataWithControls = {
@@ -688,7 +688,7 @@ test('should refetch when mixing renderTrigger string control with non-renderTri
};
(getChartControlPanelRegistry as any).mockReturnValue({
get: vi.fn().mockReturnValue(controlPanelConfig),
get: jest.fn().mockReturnValue(controlPanelConfig),
});
const formDataWithZoom = {
@@ -729,7 +729,7 @@ test('should display error message when HTTP request fails with Response object'
});
mockChartClient.client.post.mockRejectedValue(mockResponse);
const onError = vi.fn();
const onError = jest.fn();
const { findByText } = render(
<StatefulChart
formData={mockFormData}
@@ -55,9 +55,9 @@ beforeEach(() => {
});
test('should return the result from cache unless transformProps has changed', async () => {
const pre = vi.fn(x => x);
const transform = vi.fn(x => x);
const post = vi.fn(x => x);
const pre = jest.fn(x => x);
const transform = jest.fn(x => x);
const post = jest.fn(x => x);
expect(getChartComponentRegistry().get(props.chartType)).toBe(FakeChart);
expect(pre).toHaveBeenCalledTimes(0);
@@ -74,7 +74,7 @@ test('should return the result from cache unless transformProps has changed', as
expect(transform).toHaveBeenCalledTimes(1);
expect(post).toHaveBeenCalledTimes(1);
const updatedPost = vi.fn(x => x);
const updatedPost = jest.fn(x => x);
rerender(
<SuperChartCore
@@ -17,6 +17,6 @@
* under the License.
*/
export const isMatrixifyEnabled = vi.fn(() => false);
export const isMatrixifyEnabled = jest.fn(() => false);
export const MatrixifyGridRenderer = vi.fn(() => null);
export const MatrixifyGridRenderer = jest.fn(() => null);
@@ -23,7 +23,7 @@ import { ActionButton } from '.';
const defaultProps = {
label: 'test-action',
icon: <Icons.EditOutlined />,
onClick: vi.fn(),
onClick: jest.fn(),
};
test('renders action button with icon', () => {
@@ -36,7 +36,7 @@ test('renders action button with icon', () => {
});
test('calls onClick when clicked', async () => {
const onClick = vi.fn();
const onClick = jest.fn();
render(<ActionButton {...defaultProps} onClick={onClick} />);
const button = screen.getByRole('button');
@@ -307,7 +307,7 @@ test('cleans up event listeners on unmount', async () => {
if (!editorInstance) return;
// Spy on the commands.off method
const offSpy = vi.spyOn(editorInstance.commands, 'off');
const offSpy = jest.spyOn(editorInstance.commands, 'off');
// Unmount the component
unmount();
@@ -339,7 +339,7 @@ test('does not move autocomplete popup if target container is document.body', as
// Mock the closest method to return null (simulating no #ace-editor parent)
const originalClosest = editorInstance.container?.closest;
if (editorInstance.container) {
editorInstance.container.closest = vi.fn(() => null);
editorInstance.container.closest = jest.fn(() => null);
}
// Mock parentElement to be document.body
@@ -74,7 +74,7 @@ describe('useJsonValidation', () => {
});
test('falls back to "syntax error" when thrown error has no message (line 59 || branch)', () => {
const spy = vi.spyOn(JSON, 'parse').mockImplementationOnce(() => {
const spy = jest.spyOn(JSON, 'parse').mockImplementationOnce(() => {
throw {}; // no .message property → error.message is undefined → falsy
});
@@ -86,7 +86,7 @@ describe('useJsonValidation', () => {
});
test('extracts row and column from error when message contains (line X column Y)', () => {
const spy = vi.spyOn(JSON, 'parse').mockImplementationOnce(() => {
const spy = jest.spyOn(JSON, 'parse').mockImplementationOnce(() => {
throw new SyntaxError('Unexpected token (line 3 column 5)');
});
@@ -25,14 +25,14 @@ import {
} from './Button.stories';
test('works with an onClick handler', () => {
const mockAction = vi.fn();
const mockAction = jest.fn();
const { getByRole } = render(<Button onClick={mockAction} />);
fireEvent.click(getByRole('button'));
expect(mockAction).toHaveBeenCalled();
});
test('does not handle onClicks when disabled', () => {
const mockAction = vi.fn();
const mockAction = jest.fn();
const { getByRole } = render(<Button onClick={mockAction} disabled />);
fireEvent.click(getByRole('button'));
expect(mockAction).toHaveBeenCalledTimes(0);
@@ -23,7 +23,7 @@ import type { CheckboxProps } from './types';
const mockedProps: CheckboxProps = {
checked: false,
id: 'checkbox-id',
onChange: vi.fn(),
onChange: jest.fn(),
disabled: false,
title: 'Checkbox title',
indeterminate: false,
@@ -35,7 +35,7 @@ describe('Checkbox Component', () => {
waitFor(() => render(<Checkbox {...props} />));
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
describe('Rendering', () => {
@@ -93,7 +93,7 @@ describe('Checkbox Component', () => {
});
test('should not call the onChange handler when disabled and clicked', async () => {
const mockOnChange = vi.fn();
const mockOnChange = jest.fn();
const disabledProps = {
...mockedProps,
disabled: true,
@@ -109,7 +109,7 @@ describe('Checkbox Component', () => {
});
test('calls onChange handler successfully', async () => {
const mockAction = vi.fn();
const mockAction = jest.fn();
render(<Checkbox checked={false} onChange={mockAction} />);
const checkboxInput = screen.getByRole('checkbox');
await userEvent.click(checkboxInput);
@@ -20,7 +20,7 @@ import { render, screen } from '../../spec';
import CodeSyntaxHighlighter from './index';
// Simple mock that just returns the content
vi.mock(
jest.mock(
'react-syntax-highlighter/dist/cjs/light',
() =>
function MockSyntaxHighlighter({ children, ...props }: any) {
@@ -33,26 +33,26 @@ vi.mock(
);
// Mock the language modules
vi.mock(
jest.mock(
'react-syntax-highlighter/dist/cjs/languages/hljs/sql',
() => 'sql-mock',
);
vi.mock(
jest.mock(
'react-syntax-highlighter/dist/cjs/languages/hljs/json',
() => 'json-mock',
);
vi.mock(
jest.mock(
'react-syntax-highlighter/dist/cjs/languages/hljs/htmlbars',
() => 'html-mock',
);
vi.mock(
jest.mock(
'react-syntax-highlighter/dist/cjs/languages/hljs/markdown',
() => 'md-mock',
);
// Mock the styles
vi.mock('react-syntax-highlighter/dist/cjs/styles/hljs/github', () => ({}));
vi.mock(
jest.mock('react-syntax-highlighter/dist/cjs/styles/hljs/github', () => ({}));
jest.mock(
'react-syntax-highlighter/dist/cjs/styles/hljs/atom-one-dark',
() => ({}),
);
@@ -22,8 +22,8 @@ import { ConfirmModal } from '.';
const defaultProps = {
show: true,
onHide: vi.fn(),
onConfirm: vi.fn(),
onHide: jest.fn(),
onConfirm: jest.fn(),
title: 'Confirm Action',
body: 'Are you sure you want to proceed?',
};
@@ -57,7 +57,7 @@ test('renders custom button text', () => {
});
test('calls onConfirm when confirm button is clicked', () => {
const onConfirm = vi.fn();
const onConfirm = jest.fn();
renderWithTheme(<ConfirmModal {...defaultProps} onConfirm={onConfirm} />);
userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
@@ -66,7 +66,7 @@ test('calls onConfirm when confirm button is clicked', () => {
});
test('calls onHide when cancel button is clicked', () => {
const onHide = vi.fn();
const onHide = jest.fn();
renderWithTheme(<ConfirmModal {...defaultProps} onHide={onHide} />);
userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
@@ -24,11 +24,11 @@ import type { ConfirmStatusChangeProps } from './types';
const mockedProps: Omit<ConfirmStatusChangeProps, 'children'> = {
title: 'please confirm',
description: 'are you sure?',
onConfirm: vi.fn(),
onConfirm: jest.fn(),
};
test('renders children with showConfirm function', () => {
const childrenSpy = vi.fn().mockReturnValue(<div>test content</div>);
const childrenSpy = jest.fn().mockReturnValue(<div>test content</div>);
render(
<ConfirmStatusChange {...mockedProps}>{childrenSpy}</ConfirmStatusChange>,
@@ -73,8 +73,8 @@ test('stores and passes arguments to onConfirm callback', async () => {
test('calls preventDefault on event-like arguments', () => {
const mockEvent = {
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
};
const { getByTestId } = render(
@@ -93,7 +93,7 @@ test('calls preventDefault on event-like arguments', () => {
test('skips event handling on non-event arguments', () => {
const regularArg = { someData: 'value' };
const mockFunc = vi.fn();
const mockFunc = jest.fn();
const { getByTestId } = render(
<ConfirmStatusChange {...mockedProps}>
@@ -134,8 +134,8 @@ test('ignores null and undefined arguments', () => {
});
test('handles partial event objects gracefully', () => {
const partialEvent1 = { preventDefault: vi.fn() }; // Only preventDefault
const partialEvent2 = { stopPropagation: vi.fn() }; // Only stopPropagation
const partialEvent1 = { preventDefault: jest.fn() }; // Only preventDefault
const partialEvent2 = { stopPropagation: jest.fn() }; // Only stopPropagation
const { getByTestId } = render(
<ConfirmStatusChange {...mockedProps}>
@@ -21,7 +21,7 @@ import { render } from '@superset-ui/core/spec';
import * as ReactCronPicker from 'react-js-cron';
import { CronPicker } from '.';
const spy = vi.spyOn(ReactCronPicker, 'default');
const spy = jest.spyOn(ReactCronPicker, 'default');
test('Should send correct props to ReactCronPicker', () => {
const props = {
@@ -23,8 +23,8 @@ test('Must display title and content', () => {
const props = {
title: <div data-test="test-title">Title</div>,
description: <div data-test="test-description">Description</div>,
onConfirm: vi.fn(),
onHide: vi.fn(),
onConfirm: jest.fn(),
onHide: jest.fn(),
open: true,
};
render(<DeleteModal {...props} />);
@@ -36,8 +36,8 @@ test('Input should autofocus when modal opens', async () => {
const props = {
title: <div data-test="test-title">Title</div>,
description: <div data-test="test-description">Description</div>,
onConfirm: vi.fn(),
onHide: vi.fn(),
onConfirm: jest.fn(),
onHide: jest.fn(),
open: true,
};
render(<DeleteModal {...props} />);
@@ -52,8 +52,8 @@ test('Calling "onHide"', async () => {
const props = {
title: <div data-test="test-title">Title</div>,
description: <div data-test="test-description">Description</div>,
onConfirm: vi.fn(),
onHide: vi.fn(),
onConfirm: jest.fn(),
onHide: jest.fn(),
open: true,
};
const modal = <DeleteModal {...props} />;
@@ -79,8 +79,8 @@ test('Calling "onConfirm" only after typing "delete" in the input', async () =>
const props = {
title: <div data-test="test-title">Title</div>,
description: <div data-test="test-description">Description</div>,
onConfirm: vi.fn(),
onHide: vi.fn(),
onConfirm: jest.fn(),
onHide: jest.fn(),
open: true,
};
render(<DeleteModal {...props} />);
@@ -41,7 +41,7 @@ describe('NoAnimationDropdown', () => {
});
test('calls onBlur when it loses focus', () => {
const onBlur = vi.fn();
const onBlur = jest.fn();
render(
<NoAnimationDropdown {...props} onBlur={onBlur}>
<button type="button">Test Button</button>
@@ -52,7 +52,7 @@ describe('NoAnimationDropdown', () => {
});
test('calls onKeyDown when a key is pressed', () => {
const onKeyDown = vi.fn();
const onKeyDown = jest.fn();
render(
<NoAnimationDropdown {...props} onKeyDown={onKeyDown}>
<button type="button">Test Button</button>
@@ -29,13 +29,13 @@ const ITEMS = generateItems(10);
beforeEach(() => {
// Reset any mocks
vi.restoreAllMocks();
jest.restoreAllMocks();
// Mock ResizeObserver globally
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
});
@@ -100,7 +100,7 @@ test('renders component with dropdown style prop without error', () => {
});
test('renders component with onOverflowingStateChange prop without error', () => {
const onOverflowingStateChange = vi.fn();
const onOverflowingStateChange = jest.fn();
render(
<DropdownContainer
items={generateItems(5)}
@@ -160,7 +160,7 @@ test('accepts custom style props', () => {
// Integration test that doesn't rely on specific overflow behavior
test('component renders and functions without throwing errors', () => {
const onOverflowingStateChange = vi.fn();
const onOverflowingStateChange = jest.fn();
expect(() => {
render(
@@ -23,7 +23,7 @@ const createProps = (overrides: Record<string, any> = {}) => ({
title: 'Chart title',
placeholder: 'Add the name of the chart',
canEdit: true,
onSave: vi.fn(),
onSave: jest.fn(),
label: 'Chart title',
...overrides,
});
@@ -27,7 +27,7 @@ const mockEvent = {
const mockProps = {
title: 'my title',
canEdit: true,
onSaveTitle: vi.fn(),
onSaveTitle: jest.fn(),
};
test('should render title', () => {
@@ -40,7 +40,7 @@ test('should render title', () => {
test('should not render an input if it is not editable', () => {
const { queryByTestId } = render(
<EditableTitle title="my title" onSaveTitle={vi.fn()} />,
<EditableTitle title="my title" onSaveTitle={jest.fn()} />,
);
expect(
queryByTestId('textarea-editable-title-input'),
@@ -75,7 +75,7 @@ describe('should handle blur', () => {
};
test('should trigger callback', () => {
const callback = vi.fn();
const callback = jest.fn();
const { getByTestId } = setup({ onSaveTitle: callback });
fireEvent.change(getByTestId('textarea-editable-title-input'), mockEvent);
fireEvent.blur(getByTestId('textarea-editable-title-input'));
@@ -84,7 +84,7 @@ describe('should handle blur', () => {
});
test('should not trigger callback', () => {
const callback = vi.fn();
const callback = jest.fn();
const { getByTestId } = setup({ onSaveTitle: callback });
fireEvent.blur(getByTestId('textarea-editable-title-input'));
// no change
@@ -92,7 +92,7 @@ describe('should handle blur', () => {
});
test('should not save empty title', () => {
const callback = vi.fn();
const callback = jest.fn();
const { getByTestId } = setup({ onSaveTitle: callback });
const textarea = getByTestId('textarea-editable-title-input');
fireEvent.blur(textarea);
@@ -20,14 +20,14 @@
import { render, screen, userEvent } from '@superset-ui/core/spec';
import { FaveStar } from '.';
vi.mock('@superset-ui/core/components/Tooltip', () => ({
jest.mock('@superset-ui/core/components/Tooltip', () => ({
Tooltip: (props: any) => <div data-test="tooltip" {...props} />,
}));
test('render right content', async () => {
const props = {
itemId: 3,
saveFaveStar: vi.fn(),
saveFaveStar: jest.fn(),
};
const { rerender, findByRole } = render(<FaveStar {...props} isStarred />);
@@ -52,7 +52,7 @@ test('render content on tooltip', async () => {
const props = {
itemId: 3,
showTooltip: true,
saveFaveStar: vi.fn(),
saveFaveStar: jest.fn(),
};
render(<FaveStar {...props} />);
@@ -72,8 +72,8 @@ test('render content on tooltip', async () => {
test('Call fetchFaveStar on first render and on itemId change', async () => {
const props = {
itemId: 3,
fetchFaveStar: vi.fn(),
saveFaveStar: vi.fn(),
fetchFaveStar: jest.fn(),
saveFaveStar: jest.fn(),
isStarred: false,
showTooltip: false,
};
@@ -45,7 +45,7 @@ describe('IconButton', () => {
});
test('handles Enter and Space key presses', () => {
const mockOnClick = vi.fn();
const mockOnClick = jest.fn();
render(<IconButton {...defaultProps} onClick={mockOnClick} />);
const button = screen.getByRole('button');
@@ -79,7 +79,7 @@ describe('IconButton', () => {
});
test('calls onClick handler when clicked', () => {
const mockOnClick = vi.fn();
const mockOnClick = jest.fn();
render(<IconButton {...defaultProps} onClick={mockOnClick} />);
const button = screen.getByRole('button');
@@ -19,7 +19,7 @@
import { render } from '@superset-ui/core/spec';
import { IconTooltip } from '.';
vi.mock('@superset-ui/core/components/Tooltip', () => ({
jest.mock('@superset-ui/core/components/Tooltip', () => ({
Tooltip: () => <div data-test="mock-tooltip" />,
}));
@@ -28,7 +28,7 @@ test('renders the base component (no onClick)', () => {
});
test('works with an onClick handler', () => {
const mockAction = vi.fn();
const mockAction = jest.fn();
const { getByText } = render(<Label onClick={mockAction}>test</Label>);
fireEvent.click(getByText('test'));
expect(mockAction).toHaveBeenCalled();
@@ -23,7 +23,7 @@ import { Label } from '..';
// Define the prop types for DatasetTypeLabel
interface DatasetTypeLabelProps {
datasetType: 'physical' | 'virtual'; // Accepts only 'physical' or 'virtual'
datasetType: 'physical' | 'virtual' | 'semantic_view';
}
const SIZE = 's'; // Define the size as a constant
@@ -32,6 +32,24 @@ export const DatasetTypeLabel: React.FC<DatasetTypeLabelProps> = ({
datasetType,
}) => {
const theme = useTheme();
if (datasetType === 'semantic_view') {
return (
<Label
icon={
<Icons.ApartmentOutlined
iconSize={SIZE}
iconColor={theme.colorInfo}
/>
}
type="info"
style={{ color: theme.colorInfo }}
>
{t('Semantic')}
</Label>
);
}
const label: string =
datasetType === 'physical' ? t('Physical') : t('Virtual');
const icon =
@@ -28,7 +28,7 @@ test('renders the base component (no refresh)', () => {
});
test('renders a refresh action', () => {
const mockAction = vi.fn();
const mockAction = jest.fn();
render(<LastUpdated updatedAt={updatedAt} update={mockAction} />);
const button = screen.getByRole('button');
@@ -21,7 +21,7 @@ import fetchMock from 'fetch-mock';
import { render, screen } from '@superset-ui/core/spec';
import { ImageLoader, type BackgroundPosition } from './ImageLoader';
global.URL.createObjectURL = vi.fn(() => '/local_url');
global.URL.createObjectURL = jest.fn(() => '/local_url');
const blob = new Blob([], { type: 'image/png' });
beforeAll(() => {
@@ -21,7 +21,7 @@ import fetchMock from 'fetch-mock';
import { render, screen } from '@superset-ui/core/spec';
import { ListViewCard } from '.';
global.URL.createObjectURL = vi.fn(() => '/local_url');
global.URL.createObjectURL = jest.fn(() => '/local_url');
fetchMock.get('/thumbnail', { body: new Blob(), sendAsJson: false });
describe('ListViewCard', () => {
@@ -22,19 +22,19 @@ import * as themeModule from '@apache-superset/core/theme';
import { Loading } from '.';
// Mock the loading SVG import since it's a file stub in tests
vi.mock('../assets', () => ({
jest.mock('../assets', () => ({
Loading: () => <svg data-test="default-loading-svg" />,
}));
const mockUseTheme = vi.fn();
const mockUseTheme = jest.fn();
beforeEach(() => {
mockUseTheme.mockReset();
vi.spyOn(themeModule, 'useTheme').mockImplementation(mockUseTheme);
jest.spyOn(themeModule, 'useTheme').mockImplementation(mockUseTheme);
});
afterEach(() => {
vi.restoreAllMocks();
jest.restoreAllMocks();
});
test('uses default spinner when no theme spinner configured', () => {
@@ -42,7 +42,7 @@ const A_WEEK_AGO = 'a week ago';
const TWO_DAYS_AGO = '2 days ago';
const runWithBarCollapsed = async (func: Function) => {
const spy = vi.spyOn(resizeDetector, 'useResizeDetector');
const spy = jest.spyOn(resizeDetector, 'useResizeDetector');
let width: number;
spy.mockImplementation(props => {
if (props?.onResize && !width) {
@@ -101,7 +101,7 @@ test('renders an array of items', () => {
});
test('throws errors when out of min/max restrictions', () => {
const spy = vi.spyOn(console, 'error');
const spy = jest.spyOn(console, 'error');
spy.mockImplementation(() => {});
expect(() =>
render(<MetadataBar items={ITEMS.slice(0, MIN_NUMBER_ITEMS - 1)} />),
@@ -148,7 +148,7 @@ test('renders a tooltip when one is provided even if not collapsed', async () =>
});
test('renders underlined text and emits event when clickable', async () => {
const onClick = vi.fn();
const onClick = jest.fn();
const items = [{ ...ITEMS[0], onClick }, ITEMS[1]];
render(<MetadataBar items={items} />);
const element = screen.getByText(DASHBOARD_TITLE);
@@ -160,7 +160,7 @@ test('renders underlined text and emits event when clickable', async () => {
test('renders clickable items with blue icons when the bar is collapsed', async () => {
await runWithBarCollapsed(async () => {
const onClick = vi.fn();
const onClick = jest.fn();
const items = [{ ...ITEMS[0], onClick }, ITEMS[1]];
render(<MetadataBar items={items} />);
const images = screen.getAllByRole('img');
@@ -266,7 +266,7 @@ test('correctly renders the tags tooltip', async () => {
});
test('renders StyledItem with role="button" when onClick is defined', () => {
const onClick = vi.fn();
const onClick = jest.fn();
const items = [
{ ...ITEMS[0], onClick },
{ ...ITEMS[1], onClick },
@@ -47,10 +47,10 @@ describe('FormModal Component', () => {
const mockedProps: FormModalProps = {
show: true,
onHide: vi.fn(),
onHide: jest.fn(),
title: 'Test Form Modal',
onSave: vi.fn(),
formSubmitHandler: vi.fn().mockResolvedValue(undefined),
onSave: jest.fn(),
formSubmitHandler: jest.fn().mockResolvedValue(undefined),
initialValues: { name: '', email: '' },
requiredFields: ['name'],
children,
@@ -25,14 +25,14 @@ const defaultProps: PageHeaderWithActionsProps = {
editableTitleProps: {
title: 'Test title',
placeholder: 'Test placeholder',
onSave: vi.fn(),
onSave: jest.fn(),
canEdit: true,
label: 'Title',
},
showTitlePanelItems: true,
certificatiedBadgeProps: {},
showFaveStar: true,
faveStarProps: { itemId: 1, saveFaveStar: vi.fn() },
faveStarProps: { itemId: 1, saveFaveStar: jest.fn() },
titlePanelAdditionalItems: <button type="button">Title panel button</button>,
rightPanelAdditionalItems: <button type="button">Save</button>,
additionalActionsMenu: (
@@ -41,7 +41,7 @@ const defaultProps: PageHeaderWithActionsProps = {
data-test="additional-actions-menu"
/>
),
menuDropdownProps: { onVisibleChange: vi.fn(), visible: true },
menuDropdownProps: { onVisibleChange: jest.fn(), visible: true },
};
test('Renders', async () => {
@@ -58,7 +58,7 @@ test('renders with icon child', async () => {
});
test('fires an event when visibility is changed', async () => {
const onOpenChange = vi.fn();
const onOpenChange = jest.fn();
render(
<Popover
content="Content sample"
@@ -31,7 +31,7 @@ const defaultProps: PopoverDropdownProps = {
value: '1',
renderButton: (option: OptionProps) => <span>{option.label}</span>,
renderOption: (option: OptionProps) => <div>{option.label}</div>,
onChange: vi.fn(),
onChange: jest.fn(),
};
test('renders with default props', async () => {
@@ -49,7 +49,7 @@ test('renders a tooltip when hovered', async () => {
});
test('calls onSelect when clicked', async () => {
const onSelect = vi.fn();
const onSelect = jest.fn();
render(
<PopoverSection title="Title" onSelect={onSelect}>
<div role="form" />
@@ -20,7 +20,7 @@ import { render, screen, userEvent } from '@superset-ui/core/spec';
import RefreshLabel from '@superset-ui/core/components/RefreshLabel';
test('renders with default props', async () => {
render(<RefreshLabel tooltipContent="Tooltip" onClick={vi.fn()} />);
render(<RefreshLabel tooltipContent="Tooltip" onClick={jest.fn()} />);
const refresh = await screen.findByRole('button');
expect(refresh).toBeInTheDocument();
await userEvent.hover(refresh);
@@ -28,7 +28,7 @@ test('renders with default props', async () => {
test('renders tooltip on hover', async () => {
const tooltipText = 'Tooltip';
render(<RefreshLabel tooltipContent={tooltipText} onClick={vi.fn()} />);
render(<RefreshLabel tooltipContent={tooltipText} onClick={jest.fn()} />);
const refresh = screen.getByRole('button');
await userEvent.hover(refresh);
const tooltip = await screen.findByRole('tooltip');
@@ -37,7 +37,7 @@ test('renders tooltip on hover', async () => {
});
test('triggers on click event', async () => {
const onClick = vi.fn();
const onClick = jest.fn();
render(<RefreshLabel tooltipContent="Tooltip" onClick={onClick} />);
const refresh = await screen.findByRole('button');
await userEvent.click(refresh);
@@ -153,7 +153,7 @@ test('displays a header', async () => {
});
test('adds a new option if the value is not in the options, when options are empty', async () => {
const loadOptions = vi.fn(async () => ({ data: [], totalCount: 0 }));
const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
render(
<AsyncSelect {...defaultProps} options={loadOptions} value={OPTIONS[0]} />,
);
@@ -167,7 +167,7 @@ test('adds a new option if the value is not in the options, when options are emp
});
test('adds a new option if the value is not in the options, when options have values', async () => {
const loadOptions = vi.fn(async () => ({
const loadOptions = jest.fn(async () => ({
data: [OPTIONS[1]],
totalCount: 1,
}));
@@ -185,7 +185,7 @@ test('adds a new option if the value is not in the options, when options have va
});
test('does not add a new option if the value is already in the options', async () => {
const loadOptions = vi.fn(async () => ({
const loadOptions = jest.fn(async () => ({
data: [OPTIONS[0]],
totalCount: 1,
}));
@@ -206,7 +206,7 @@ test('inverts the selection', async () => {
});
test('sort the options by label if no sort comparator is provided', async () => {
const loadUnsortedOptions = vi.fn(async () => ({
const loadUnsortedOptions = jest.fn(async () => ({
data: [...OPTIONS].sort(() => Math.random()),
totalCount: 2,
}));
@@ -324,7 +324,7 @@ test('ignores case when searching', async () => {
});
test('same case should be ranked to the top', async () => {
const loadOptions = vi.fn(async () => ({
const loadOptions = jest.fn(async () => ({
data: [
{ value: 'Cac' },
{ value: 'abac' },
@@ -399,7 +399,7 @@ test('removes duplicated values', async () => {
});
test('renders a custom label', async () => {
const loadOptions = vi.fn(async () => ({
const loadOptions = jest.fn(async () => ({
data: [
{ value: 'John', label: <h1>John</h1> },
{ value: 'Liam', label: <h1>Liam</h1> },
@@ -415,7 +415,7 @@ test('renders a custom label', async () => {
});
test('searches for a word with a custom label', async () => {
const loadOptions = vi.fn(async () => ({
const loadOptions = jest.fn(async () => ({
data: [
{ value: 'John', label: <h1>John</h1> },
{ value: 'Liam', label: <h1>Liam</h1> },
@@ -441,7 +441,7 @@ test('removes a new option if the user does not select it', async () => {
});
test('clear all the values', async () => {
const onClear = vi.fn();
const onClear = jest.fn();
render(
<AsyncSelect
{...defaultProps}
@@ -466,7 +466,7 @@ test('does not add a new option if allowNewOptions is false', async () => {
});
test('adds the null option when selected in single mode', async () => {
const loadOptions = vi.fn(async () => ({
const loadOptions = jest.fn(async () => ({
data: [OPTIONS[0], NULL_OPTION],
totalCount: 2,
}));
@@ -478,7 +478,7 @@ test('adds the null option when selected in single mode', async () => {
});
test('adds the null option when selected in multiple mode', async () => {
const loadOptions = vi.fn(async () => ({
const loadOptions = jest.fn(async () => ({
data: [OPTIONS[0], NULL_OPTION],
totalCount: 2,
}));
@@ -541,7 +541,7 @@ test('multiple selections in multiple mode', async () => {
});
test('changes the selected item in single mode', async () => {
const onChange = vi.fn();
const onChange = jest.fn();
render(<AsyncSelect {...defaultProps} onChange={onChange} />);
await open();
const [firstOption, secondOption] = OPTIONS;
@@ -674,7 +674,7 @@ test('searches for matches in both loaded and unloaded pages', async () => {
});
test('searches for an item in a page not loaded', async () => {
const mock = vi.fn(loadOptions);
const mock = jest.fn(loadOptions);
render(<AsyncSelect {...defaultProps} options={mock} />);
const search = 'Sandro';
await open();
@@ -686,20 +686,20 @@ test('searches for an item in a page not loaded', async () => {
});
test('does not fetches data when rendering', async () => {
const loadOptions = vi.fn(async () => ({ data: [], totalCount: 0 }));
const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
render(<AsyncSelect {...defaultProps} options={loadOptions} />);
expect(loadOptions).not.toHaveBeenCalled();
});
test('fetches data when opening', async () => {
const loadOptions = vi.fn(async () => ({ data: [], totalCount: 0 }));
const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
render(<AsyncSelect {...defaultProps} options={loadOptions} />);
await open();
expect(loadOptions).toHaveBeenCalled();
});
test('fetches data only after a search input is entered if fetchOnlyOnSearch is true', async () => {
const loadOptions = vi.fn(async () => ({ data: [], totalCount: 0 }));
const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
render(
<AsyncSelect {...defaultProps} options={loadOptions} fetchOnlyOnSearch />,
);
@@ -720,7 +720,7 @@ test('displays an error message when an exception is thrown while fetching', asy
});
test('does not fire a new request for the same search input', async () => {
const loadOptions = vi.fn(async () => ({ data: [], totalCount: 0 }));
const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
render(
<AsyncSelect {...defaultProps} options={loadOptions} fetchOnlyOnSearch />,
);
@@ -731,7 +731,7 @@ test('does not fire a new request for the same search input', async () => {
});
test('does not fire a new request if all values have been fetched', async () => {
const mock = vi.fn(loadOptions);
const mock = jest.fn(loadOptions);
const search = 'George';
const pageSize = OPTIONS.length;
render(<AsyncSelect {...defaultProps} options={mock} pageSize={pageSize} />);
@@ -743,7 +743,7 @@ test('does not fire a new request if all values have been fetched', async () =>
});
test('fires a new request if all values have not been fetched', async () => {
const mock = vi.fn(loadOptions);
const mock = jest.fn(loadOptions);
const pageSize = OPTIONS.length / 2;
render(<AsyncSelect {...defaultProps} options={mock} pageSize={pageSize} />);
await open();
@@ -774,7 +774,7 @@ test('renders a helper text when one is provided', async () => {
});
test('finds an element with a numeric value and does not duplicate the options', async () => {
const options = vi.fn(async () => ({
const options = jest.fn(async () => ({
data: [
{ label: 'a', value: 11 },
{ label: 'b', value: 12 },
@@ -837,7 +837,7 @@ test('Renders only an overflow tag if dropdown is open in oneLine mode', async (
});
test('does not fire onChange when searching but no selection', async () => {
const onChange = vi.fn();
const onChange = jest.fn();
render(
<div role="main">
<AsyncSelect
@@ -856,7 +856,7 @@ test('does not fire onChange when searching but no selection', async () => {
});
test('fires onChange when clearing the selection in single mode', async () => {
const onChange = vi.fn();
const onChange = jest.fn();
render(
<AsyncSelect
{...defaultProps}
@@ -870,7 +870,7 @@ test('fires onChange when clearing the selection in single mode', async () => {
});
test('fires onChange when clearing the selection in multiple mode', async () => {
const onChange = vi.fn();
const onChange = jest.fn();
render(
<AsyncSelect
{...defaultProps}
@@ -884,7 +884,7 @@ test('fires onChange when clearing the selection in multiple mode', async () =>
});
test('fires onChange when pasting a selection', async () => {
const onChange = vi.fn();
const onChange = jest.fn();
render(<AsyncSelect {...defaultProps} onChange={onChange} />);
await open();
const input = getElementByClassName('.ant-select-selection-search-input');
@@ -916,7 +916,7 @@ test('does not duplicate options when using numeric values', async () => {
});
test('pasting an existing option does not duplicate it', async () => {
const options = vi.fn(async () => ({
const options = jest.fn(async () => ({
data: [OPTIONS[0]],
totalCount: 1,
}));
@@ -933,7 +933,7 @@ test('pasting an existing option does not duplicate it', async () => {
});
test('pasting an existing option does not duplicate it in multiple mode', async () => {
const options = vi.fn(async () => ({
const options = jest.fn(async () => ({
data: [
{ label: 'John', value: 1 },
{ label: 'Liam', value: 2 },
@@ -983,7 +983,7 @@ test('pasting an non-existent option should not add it if allowNewOptions is fal
});
test('onChange is called with the value property when pasting an option that was not loaded yet', async () => {
const onChange = vi.fn();
const onChange = jest.fn();
render(<AsyncSelect {...defaultProps} onChange={onChange} />);
await open();
const input = getElementByClassName('.ant-select-selection-search-input');
@@ -1003,7 +1003,7 @@ test('onChange is called with the value property when pasting an option that was
});
test('does not fire onChange if the same value is selected in single mode', async () => {
const onChange = vi.fn();
const onChange = jest.fn();
render(<AsyncSelect {...defaultProps} onChange={onChange} />);
const optionText = 'Emma';
await open();
@@ -412,7 +412,7 @@ test('removes a new option if the user does not select it', async () => {
});
test('clear all the values', async () => {
const onClear = vi.fn();
const onClear = jest.fn();
render(
<Select
{...defaultProps}
@@ -493,7 +493,7 @@ test('multiple selections in multiple mode', async () => {
});
test('changes the selected item in single mode', async () => {
const onChange = vi.fn();
const onChange = jest.fn();
render(<Select {...defaultProps} onChange={onChange} />);
await open();
const [firstOption, secondOption] = OPTIONS;
@@ -602,7 +602,7 @@ test('searches for an item', async () => {
});
test('triggers getPopupContainer if passed', async () => {
const getPopupContainer = vi.fn();
const getPopupContainer = jest.fn();
render(<Select {...defaultProps} getPopupContainer={getPopupContainer} />);
await open();
expect(getPopupContainer).toHaveBeenCalled();
@@ -916,7 +916,7 @@ test('"Select all" does not affect disabled options', async () => {
});
test('does not fire onChange when searching but no selection', async () => {
const onChange = vi.fn();
const onChange = jest.fn();
render(
<div role="main">
<Select
@@ -935,7 +935,7 @@ test('does not fire onChange when searching but no selection', async () => {
});
test('fires onChange when clearing the selection in single mode', async () => {
const onChange = vi.fn();
const onChange = jest.fn();
render(
<Select
{...defaultProps}
@@ -949,7 +949,7 @@ test('fires onChange when clearing the selection in single mode', async () => {
});
test('fires onChange when clearing the selection in multiple mode', async () => {
const onChange = vi.fn();
const onChange = jest.fn();
render(
<Select
{...defaultProps}
@@ -963,7 +963,7 @@ test('fires onChange when clearing the selection in multiple mode', async () =>
});
test('fires onChange when pasting a selection', async () => {
const onChange = vi.fn();
const onChange = jest.fn();
render(<Select {...defaultProps} onChange={onChange} />);
await open();
const input = getElementByClassName('.ant-select-selection-search-input');
@@ -1045,7 +1045,7 @@ test('pasting an non-existent option should not add it if allowNewOptions is fal
});
test('does not fire onChange if the same value is selected in single mode', async () => {
const onChange = vi.fn();
const onChange = jest.fn();
render(<Select {...defaultProps} onChange={onChange} />);
const optionText = 'Emma';
await open();
@@ -1058,25 +1058,25 @@ test('does not fire onChange if the same value is selected in single mode', asyn
// Reference for the bug this tests: https://github.com/apache/superset/pull/33043#issuecomment-2809419640
test('typing and deleting the last character for a new option displays correctly', async () => {
vi.useFakeTimers();
jest.useFakeTimers();
render(<Select {...defaultProps} allowNewOptions />);
await open();
await type('aaa', 0, false);
vi.runAllTimers();
jest.runAllTimers();
await type('{backspace}', 0, false);
await type('a', 0, false);
vi.runAllTimers();
jest.runAllTimers();
expect(
screen.queryByText(NO_DATA, { selector: '.ant-empty-description' }),
).not.toBeInTheDocument();
expect(await findSelectOption('aaa')).toBeInTheDocument();
vi.useRealTimers();
jest.useRealTimers();
});
describe('grouped options search', () => {
@@ -21,7 +21,7 @@ import ActionCell, { appendDataToMenu } from './index';
import { exampleMenuOptions, exampleRow } from './fixtures';
test('renders with default props', async () => {
const clickHandler = vi.fn();
const clickHandler = jest.fn();
exampleMenuOptions[0].onClick = clickHandler;
render(<ActionCell menuOptions={exampleMenuOptions} row={exampleRow} />);
// Open the menu
@@ -21,7 +21,7 @@ import ButtonCell from './index';
import { exampleRow } from '../fixtures';
test('renders with default props', async () => {
const clickHandler = vi.fn();
const clickHandler = jest.fn();
const BUTTON_LABEL = 'Button Label';
render(
@@ -45,7 +45,7 @@ afterEach(() => {
test('constructor initializes with correct defaults', () => {
const table = createMockTable();
const setDerivedColumns = vi.fn();
const setDerivedColumns = jest.fn();
const utils = new InteractiveTableUtils(
table,
mockColumns,
@@ -62,7 +62,7 @@ test('constructor initializes with correct defaults', () => {
test('setTableRef updates tableRef', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const newTable = createMockTable();
utils.setTableRef(newTable);
expect(utils.tableRef).toBe(newTable);
@@ -70,14 +70,14 @@ test('setTableRef updates tableRef', () => {
test('getColumnIndex returns -1 when columnRef has no parent', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
utils.columnRef = null;
expect(utils.getColumnIndex()).toBe(-1);
});
test('getColumnIndex returns correct index when columnRef is in a row', () => {
const table = createMockTable(3);
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const row = table.rows[0];
utils.columnRef = row.cells[1] as unknown as typeof utils.columnRef;
expect(utils.getColumnIndex()).toBe(1);
@@ -85,15 +85,15 @@ test('getColumnIndex returns correct index when columnRef is in a row', () => {
test('allowDrop calls preventDefault on the event', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const event = { preventDefault: vi.fn() } as unknown as DragEvent;
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const event = { preventDefault: jest.fn() } as unknown as DragEvent;
utils.allowDrop(event);
expect(event.preventDefault).toHaveBeenCalledTimes(1);
});
test('handleMouseup clears mouseDown and resets dragging state', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const th = document.createElement('th') as unknown as typeof utils.columnRef;
utils.columnRef = th;
(th as any).mouseDown = true;
@@ -107,7 +107,7 @@ test('handleMouseup clears mouseDown and resets dragging state', () => {
test('handleMouseup works when columnRef is null', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
utils.columnRef = null;
utils.isDragging = true;
@@ -118,7 +118,7 @@ test('handleMouseup works when columnRef is null', () => {
test('handleMouseDown sets mouseDown and oldX when within resize range', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const target = document.createElement('th') as any;
Object.defineProperty(target, 'offsetWidth', {
value: 100,
@@ -141,7 +141,7 @@ test('handleMouseDown sets mouseDown and oldX when within resize range', () => {
test('handleMouseDown sets draggable when outside resize range and reorderable', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
utils.reorderable = true;
const target = document.createElement('th') as any;
@@ -163,9 +163,9 @@ test('handleMouseDown sets draggable when outside resize range and reorderable',
test('initializeResizableColumns adds event listeners when resizable is true', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const cell = table.rows[0].cells[0];
const addEventSpy = vi.spyOn(cell, 'addEventListener');
const addEventSpy = jest.spyOn(cell, 'addEventListener');
utils.initializeResizableColumns(true, table);
@@ -180,9 +180,9 @@ test('initializeResizableColumns adds event listeners when resizable is true', (
test('initializeResizableColumns removes event listeners when resizable is false', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const cell = table.rows[0].cells[0];
const removeEventSpy = vi.spyOn(cell, 'removeEventListener');
const removeEventSpy = jest.spyOn(cell, 'removeEventListener');
utils.initializeResizableColumns(false, table);
@@ -200,9 +200,9 @@ test('initializeResizableColumns removes event listeners when resizable is false
test('initializeDragDropColumns adds event listeners when reorderable is true', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const cell = table.rows[0].cells[0];
const addEventSpy = vi.spyOn(cell, 'addEventListener');
const addEventSpy = jest.spyOn(cell, 'addEventListener');
utils.initializeDragDropColumns(true, table);
@@ -216,9 +216,9 @@ test('initializeDragDropColumns adds event listeners when reorderable is true',
test('initializeDragDropColumns removes event listeners when reorderable is false', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const cell = table.rows[0].cells[0];
const removeEventSpy = vi.spyOn(cell, 'removeEventListener');
const removeEventSpy = jest.spyOn(cell, 'removeEventListener');
utils.initializeDragDropColumns(false, table);
@@ -232,11 +232,11 @@ test('initializeDragDropColumns removes event listeners when reorderable is fals
test('handleColumnDragStart sets isDragging and calls setData', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const row = table.rows[0];
const target = row.cells[0] as any;
const setDataMock = vi.fn();
const setDataMock = jest.fn();
const event = {
currentTarget: target,
dataTransfer: { setData: setDataMock },
@@ -253,7 +253,7 @@ test('handleColumnDragStart sets isDragging and calls setData', () => {
test('handleDragDrop reorders columns when valid drag data exists', () => {
const table = createMockTable(2);
const setDerivedColumns = vi.fn();
const setDerivedColumns = jest.fn();
const utils = new InteractiveTableUtils(
table,
mockColumns,
@@ -268,8 +268,8 @@ test('handleDragDrop reorders columns when valid drag data exists', () => {
const dropTarget = row.cells[1];
const event = {
currentTarget: dropTarget,
dataTransfer: { getData: vi.fn().mockReturnValue(dragData) },
preventDefault: vi.fn(),
dataTransfer: { getData: jest.fn().mockReturnValue(dragData) },
preventDefault: jest.fn(),
} as unknown as DragEvent;
utils.handleDragDrop(event);
@@ -280,7 +280,7 @@ test('handleDragDrop reorders columns when valid drag data exists', () => {
test('handleDragDrop does nothing when no drag data', () => {
const table = createMockTable(2);
const setDerivedColumns = vi.fn();
const setDerivedColumns = jest.fn();
const utils = new InteractiveTableUtils(
table,
mockColumns,
@@ -290,8 +290,8 @@ test('handleDragDrop does nothing when no drag data', () => {
const row = table.rows[0];
const event = {
currentTarget: row.cells[0],
dataTransfer: { getData: vi.fn().mockReturnValue('') },
preventDefault: vi.fn(),
dataTransfer: { getData: jest.fn().mockReturnValue('') },
preventDefault: jest.fn(),
} as unknown as DragEvent;
utils.handleDragDrop(event);
@@ -302,7 +302,7 @@ test('handleDragDrop does nothing when no drag data', () => {
test('handleMouseMove updates cursor to col-resize when within resize range', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
utils.resizable = true;
const target = document.createElement('th') as any;
@@ -325,7 +325,7 @@ test('handleMouseMove updates cursor to col-resize when within resize range', ()
test('handleMouseMove sets default cursor when outside resize range', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
utils.resizable = true;
const target = document.createElement('th') as any;
@@ -348,7 +348,7 @@ test('handleMouseMove sets default cursor when outside resize range', () => {
test('handleMouseMove resizes column when mouseDown and within bounds', () => {
const table = createMockTable(2);
const setDerivedColumns = vi.fn();
const setDerivedColumns = jest.fn();
const utils = new InteractiveTableUtils(
table,
mockColumns,
@@ -384,7 +384,7 @@ test('handleMouseMove resizes column when mouseDown and within bounds', () => {
test('handleMouseMove skips resize when not resizable', () => {
const table = createMockTable(2);
const setDerivedColumns = vi.fn();
const setDerivedColumns = jest.fn();
const utils = new InteractiveTableUtils(
table,
mockColumns,
@@ -406,7 +406,7 @@ test('handleMouseMove skips resize when not resizable', () => {
test('handleMouseMove handles negative diff by keeping original width', () => {
const table = createMockTable(2);
const setDerivedColumns = vi.fn();
const setDerivedColumns = jest.fn();
const utils = new InteractiveTableUtils(
table,
mockColumns,
@@ -442,11 +442,11 @@ test('handleMouseMove handles negative diff by keeping original width', () => {
test('handleColumnDragStart does not set columnRef when currentTarget is null (line 82 false)', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const event = {
currentTarget: null,
dataTransfer: { setData: vi.fn() },
dataTransfer: { setData: jest.fn() },
} as unknown as DragEvent;
utils.handleColumnDragStart(event);
@@ -457,7 +457,7 @@ test('handleColumnDragStart does not set columnRef when currentTarget is null (l
test('handleMouseDown does nothing when currentTarget is null (line 118 false)', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const event = {
currentTarget: null,
@@ -472,7 +472,7 @@ test('handleMouseDown does nothing when currentTarget is null (line 118 false)',
test('handleMouseDown does nothing to draggable when outside resize range and not reorderable (line 132 false)', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
utils.reorderable = false;
const target = document.createElement('th') as any;
@@ -494,7 +494,7 @@ test('handleMouseDown does nothing to draggable when outside resize range and no
test('handleMouseMove skips column update when getColumnIndex returns NaN (line 162 false)', () => {
const table = createMockTable(2);
const setDerivedColumns = vi.fn();
const setDerivedColumns = jest.fn();
const utils = new InteractiveTableUtils(
table,
mockColumns,
@@ -509,7 +509,7 @@ test('handleMouseMove skips column update when getColumnIndex returns NaN (line
col.oldX = 50;
utils.columnRef = col;
vi.spyOn(utils, 'getColumnIndex').mockReturnValueOnce(NaN);
jest.spyOn(utils, 'getColumnIndex').mockReturnValueOnce(NaN);
const target = document.createElement('th') as any;
Object.defineProperty(target, 'offsetWidth', {
@@ -531,7 +531,7 @@ test('handleMouseMove skips column update when getColumnIndex returns NaN (line
test('initializeResizableColumns does nothing when table is null (lines 182-187 false)', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
expect(() => utils.initializeResizableColumns(true, null)).not.toThrow();
expect(utils.tableRef).toBeNull();
@@ -539,7 +539,7 @@ test('initializeResizableColumns does nothing when table is null (lines 182-187
test('initializeResizableColumns uses default resizable=false when first arg is undefined (line 182 default branch)', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
utils.initializeResizableColumns(undefined, table);
@@ -548,7 +548,7 @@ test('initializeResizableColumns uses default resizable=false when first arg is
test('initializeDragDropColumns does nothing when table is null (lines 206-211 false)', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
expect(() => utils.initializeDragDropColumns(true, null)).not.toThrow();
expect(utils.tableRef).toBeNull();
@@ -556,7 +556,7 @@ test('initializeDragDropColumns does nothing when table is null (lines 206-211 f
test('initializeDragDropColumns uses default reorderable=false when first arg is undefined (line 206 default branch)', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
utils.initializeDragDropColumns(undefined, table);
@@ -565,8 +565,8 @@ test('initializeDragDropColumns uses default reorderable=false when first arg is
test('clearListeners removes document mouseup listener', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, vi.fn());
const removeEventSpy = vi.spyOn(document, 'removeEventListener');
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const removeEventSpy = jest.spyOn(document, 'removeEventListener');
utils.clearListeners();
@@ -70,8 +70,8 @@ beforeEach(() => {
columns: tableHook.columns,
loading: false,
highlightRowId: 1,
getTableProps: vi.fn(),
getTableBodyProps: vi.fn(),
getTableProps: jest.fn(),
getTableBodyProps: jest.fn(),
sticky: false,
};
});
@@ -116,7 +116,7 @@ test('Pagination controls should be rendered when pageSize is provided', () => {
pageSize: 2,
totalCount: 3,
pageIndex: 0,
onPageChange: vi.fn(),
onPageChange: jest.fn(),
};
render(<TableCollection {...paginationProps} />);
@@ -124,7 +124,7 @@ test('Pagination controls should be rendered when pageSize is provided', () => {
});
test('Pagination should call onPageChange when page is changed', async () => {
const onPageChange = vi.fn();
const onPageChange = jest.fn();
const paginationProps = {
...defaultProps,
pageSize: 2,
@@ -147,7 +147,7 @@ test('Pagination should call onPageChange when page is changed', async () => {
});
test('Pagination callback should be stable across re-renders', () => {
const onPageChange = vi.fn();
const onPageChange = jest.fn();
const paginationProps = {
...defaultProps,
pageSize: 2,
@@ -171,7 +171,7 @@ test('Should display correct page info when showRowCount is true', () => {
pageSize: 2,
totalCount: 3,
pageIndex: 0,
onPageChange: vi.fn(),
onPageChange: jest.fn(),
showRowCount: true,
};
render(<TableCollection {...paginationProps} />);
@@ -186,7 +186,7 @@ test('Should not display page info when showRowCount is false', () => {
pageSize: 2,
totalCount: 3,
pageIndex: 0,
onPageChange: vi.fn(),
onPageChange: jest.fn(),
showRowCount: false,
};
render(<TableCollection {...paginationProps} />);
@@ -196,8 +196,8 @@ test('Should not display page info when showRowCount is false', () => {
});
test('Bulk selection should work with pagination', () => {
const toggleRowSelected = vi.fn();
const toggleAllRowsSelected = vi.fn();
const toggleRowSelected = jest.fn();
const toggleAllRowsSelected = jest.fn();
const selectionProps = {
...defaultProps,
bulkSelectEnabled: true,
@@ -207,7 +207,7 @@ test('Bulk selection should work with pagination', () => {
pageSize: 2,
totalCount: 3,
pageIndex: 0,
onPageChange: vi.fn(),
onPageChange: jest.fn(),
};
render(<TableCollection {...selectionProps} />);
@@ -217,7 +217,7 @@ test('Bulk selection should work with pagination', () => {
});
test('should call setSortBy when clicking sortable column header', () => {
const setSortBy = vi.fn();
const setSortBy = jest.fn();
const sortingProps = {
...defaultProps,
setSortBy,
@@ -229,7 +229,7 @@ test('should render the right wrap content text by columnsForWrapText', () => {
});
test('should handle server-side pagination', async () => {
const onServerPagination = vi.fn();
const onServerPagination = jest.fn();
const serverPaginationProps = {
...mockedProps,
serverPagination: true,
@@ -251,7 +251,7 @@ test('should handle server-side pagination', async () => {
});
test('should handle server-side sorting', async () => {
const onServerPagination = vi.fn();
const onServerPagination = jest.fn();
const serverPaginationProps = {
...mockedProps,
serverPagination: true,
@@ -271,7 +271,7 @@ test('should handle server-side sorting', async () => {
});
test('pagination callbacks should be stable across re-renders', () => {
const onServerPagination = vi.fn();
const onServerPagination = jest.fn();
const serverPaginationProps = {
...mockedProps,
serverPagination: true,
@@ -290,7 +290,9 @@ test('pagination callbacks should be stable across re-renders', () => {
});
test('should scroll to top when scrollTopOnPagination is true', async () => {
const scrollToSpy = vi.spyOn(window, 'scrollTo').mockImplementation(() => {});
const scrollToSpy = jest
.spyOn(window, 'scrollTo')
.mockImplementation(() => {});
const scrollProps = {
...mockedProps,
@@ -311,7 +313,9 @@ test('should scroll to top when scrollTopOnPagination is true', async () => {
});
test('should NOT scroll to top when scrollTopOnPagination is false', async () => {
const scrollToSpy = vi.spyOn(window, 'scrollTo').mockImplementation(() => {});
const scrollToSpy = jest
.spyOn(window, 'scrollTo')
.mockImplementation(() => {});
const scrollProps = {
...mockedProps,
@@ -108,7 +108,7 @@ describe('Tabs', () => {
});
test('should handle tab change events', () => {
const onChangeMock = vi.fn();
const onChangeMock = jest.fn();
const { getByText } = render(
<Tabs items={defaultItems} onChange={onChangeMock} />,
);
@@ -119,7 +119,7 @@ describe('Tabs', () => {
});
test('should pass through additional props to Antd Tabs', () => {
const onTabClickMock = vi.fn();
const onTabClickMock = jest.fn();
const { getByText } = render(
<Tabs
items={defaultItems}
@@ -146,7 +146,7 @@ describe('Tabs', () => {
});
test('should handle onEdit callback for add/remove actions', () => {
const onEditMock = vi.fn();
const onEditMock = jest.fn();
const itemsWithRemove = defaultItems.map(item => ({
...item,
closable: true,
@@ -22,17 +22,16 @@ import { createRef } from 'react';
import { ThemeProvider, supersetTheme } from '@apache-superset/core/theme';
import * as uiModule from '@apache-superset/core/theme';
import { ThemedAgGridReact } from './index';
import { Mock } from 'vitest';
// Mock useThemeMode hook
vi.mock('@apache-superset/core/theme', async (importActual) => ({
...(await importActual()),
useThemeMode: vi.fn(() => false), // Default to light mode
jest.mock('@apache-superset/core/theme', () => ({
...jest.requireActual('@apache-superset/core/theme'),
useThemeMode: jest.fn(() => false), // Default to light mode
}));
// Mock ag-grid-react to avoid complex setup
vi.mock('ag-grid-react', () => ({
AgGridReact: vi.fn(({ theme, ...props }) => (
jest.mock('ag-grid-react', () => ({
AgGridReact: jest.fn(({ theme, ...props }) => (
<div
data-test="ag-grid-react"
data-theme={JSON.stringify(theme)}
@@ -44,16 +43,16 @@ vi.mock('ag-grid-react', () => ({
}));
// Mock ag-grid-community
vi.mock('ag-grid-community', () => ({
jest.mock('ag-grid-community', () => ({
themeQuartz: {
withPart: vi.fn().mockReturnThis(),
withParams: vi.fn(params => ({ ...params, _type: 'theme' })),
withPart: jest.fn().mockReturnThis(),
withParams: jest.fn(params => ({ ...params, _type: 'theme' })),
},
colorSchemeDark: { _type: 'dark' },
colorSchemeLight: { _type: 'light' },
AllCommunityModule: {},
ClientSideRowModelModule: {},
ModuleRegistry: { registerModules: vi.fn() },
ModuleRegistry: { registerModules: jest.fn() },
}));
const mockRowData = [
@@ -67,9 +66,9 @@ const mockColumnDefs = [
];
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
// Reset to light mode by default
(uiModule.useThemeMode as Mock).mockReturnValue(false);
(uiModule.useThemeMode as jest.Mock).mockReturnValue(false);
});
test('renders the AgGridReact component', () => {
@@ -102,7 +101,7 @@ test('applies light theme when background is light', () => {
test('applies dark theme when background is dark', () => {
// Mock dark mode
(uiModule.useThemeMode as Mock).mockReturnValue(true);
(uiModule.useThemeMode as jest.Mock).mockReturnValue(true);
const darkTheme = {
...supersetTheme,
@@ -145,8 +144,8 @@ test('forwards ref to AgGridReact', () => {
});
test('passes all props through to AgGridReact', () => {
const onGridReady = vi.fn();
const onCellClicked = vi.fn();
const onGridReady = jest.fn();
const onCellClicked = jest.fn();
render(
<ThemedAgGridReact
@@ -18,11 +18,10 @@
*/
import { ModuleRegistry } from 'ag-grid-community';
import { setupAGGridModules, defaultModules } from './setupAGGridModules';
import { Mock } from 'vitest';
vi.mock('ag-grid-community', () => ({
jest.mock('ag-grid-community', () => ({
ModuleRegistry: {
registerModules: vi.fn(),
registerModules: jest.fn(),
},
ColumnAutoSizeModule: {
moduleName: 'ColumnAutoSizeModule',
@@ -53,7 +52,7 @@ vi.mock('ag-grid-community', () => ({
}));
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
test('defaultModules exports an array of AG Grid modules', () => {
@@ -89,7 +88,7 @@ test('setupAGGridModules registers default + additional modules when provided',
expect(ModuleRegistry.registerModules).toHaveBeenCalledTimes(1);
const registeredModules = (ModuleRegistry.registerModules as Mock).mock
const registeredModules = (ModuleRegistry.registerModules as jest.Mock).mock
.calls[0][0];
// Should contain all default modules
@@ -17,7 +17,6 @@
* under the License.
*/
import { render, waitFor } from '@superset-ui/core/spec';
// import { render, waitFor } from '@testing-library/react';
import { Timer, TimerProps } from '.';
import { now } from '../../utils/dates';
@@ -47,11 +47,11 @@ const mockTimezones = [
];
beforeAll(() => {
global.Intl.supportedValuesOf = vi.fn(() => mockTimezones);
global.Intl.supportedValuesOf = jest.fn(() => mockTimezones);
});
afterAll(() => {
vi.restoreAllMocks();
jest.restoreAllMocks();
});
test('initializes with empty cache', () => {
@@ -229,7 +229,7 @@ test('allows retry after failed computation', async () => {
});
test('uses queueMicrotask when available', async () => {
const queueMicrotaskSpy = vi.spyOn(global, 'queueMicrotask');
const queueMicrotaskSpy = jest.spyOn(global, 'queueMicrotask');
const cache = new TimezoneOptionsCache(mockGetOffsetKey, mockOffsetsToName);
await cache.getOptionsAsync();
@@ -244,7 +244,7 @@ test('falls back to setTimeout when queueMicrotask is not available', async () =
// @ts-expect-error - temporarily remove queueMicrotask for testing
delete global.queueMicrotask;
const setTimeoutSpy = vi.spyOn(global, 'setTimeout');
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
const cache = new TimezoneOptionsCache(mockGetOffsetKey, mockOffsetsToName);
await cache.getOptionsAsync();
@@ -24,8 +24,8 @@ import type { TimezoneSelectorProps } from './index';
const loadComponent = (mockCurrentTime?: string) => {
if (mockCurrentTime) {
vi.useFakeTimers();
vi.setSystemTime(new Date(mockCurrentTime));
jest.useFakeTimers();
jest.setSystemTime(new Date(mockCurrentTime));
}
return new Promise<FC<TimezoneSelectorProps>>(resolve => {
const { default: TimezoneSelector } = module.require('./index');
@@ -34,12 +34,12 @@ const loadComponent = (mockCurrentTime?: string) => {
};
afterEach(() => {
vi.useRealTimers();
jest.useRealTimers();
});
test('render timezones in correct order for daylight saving time', async () => {
const TimezoneSelector = await loadComponent('2022-07-01');
const onTimezoneChange = vi.fn();
const onTimezoneChange = jest.fn();
render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
@@ -24,13 +24,12 @@ import type { TimezoneSelectorProps } from './index';
const loadComponent = (mockCurrentTime?: string) => {
if (mockCurrentTime) {
vi.useFakeTimers();
vi.setSystemTime(new Date(mockCurrentTime));
jest.useFakeTimers();
jest.setSystemTime(new Date(mockCurrentTime));
}
return new Promise<FC<TimezoneSelectorProps>>(resolve => {
import('./index').then(({ default: TimezoneSelector }) => {
resolve(TimezoneSelector);
});
const { default: TimezoneSelector } = module.require('./index');
resolve(TimezoneSelector);
});
};
@@ -42,15 +41,15 @@ const openSelectMenu = () => {
userEvent.click(searchInput);
};
vi.spyOn(extendedDayjs.tz, 'guess').mockReturnValue('America/New_York');
jest.spyOn(extendedDayjs.tz, 'guess').mockReturnValue('America/New_York');
afterEach(() => {
vi.useRealTimers();
jest.useRealTimers();
});
test('use the timezone from `dayjs` if no timezone provided', async () => {
const TimezoneSelector = await loadComponent('2022-01-01');
const onTimezoneChange = vi.fn();
const onTimezoneChange = jest.fn();
render(<TimezoneSelector onTimezoneChange={onTimezoneChange} />);
// Wait for async loading and default timezone to be set
await screen.findByText('GMT -05:00 (Eastern Standard Time)');
@@ -59,7 +58,7 @@ test('use the timezone from `dayjs` if no timezone provided', async () => {
test('update to closest deduped timezone when timezone is provided', async () => {
const TimezoneSelector = await loadComponent('2022-01-01');
const onTimezoneChange = vi.fn();
const onTimezoneChange = jest.fn();
render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
@@ -78,7 +77,7 @@ test('update to closest deduped timezone when timezone is provided', async () =>
test('use the default timezone when an invalid timezone is provided', async () => {
const TimezoneSelector = await loadComponent('2022-01-01');
const onTimezoneChange = vi.fn();
const onTimezoneChange = jest.fn();
render(
<TimezoneSelector onTimezoneChange={onTimezoneChange} timezone="UTC" />,
);
@@ -94,7 +93,7 @@ test('use the default timezone when an invalid timezone is provided', async () =
test('render timezones in correct order for standard time', async () => {
const TimezoneSelector = await loadComponent('2022-01-01');
const onTimezoneChange = vi.fn();
const onTimezoneChange = jest.fn();
render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
@@ -112,7 +111,7 @@ test('render timezones in correct order for standard time', async () => {
test('can select a timezone values and returns canonical timezone name', async () => {
const TimezoneSelector = await loadComponent('2022-01-01');
const onTimezoneChange = vi.fn();
const onTimezoneChange = jest.fn();
render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
@@ -135,7 +134,7 @@ test('can select a timezone values and returns canonical timezone name', async (
test('can update props and rerender with different values', async () => {
const TimezoneSelector = await loadComponent('2022-01-01');
const onTimezoneChange = vi.fn();
const onTimezoneChange = jest.fn();
const { rerender } = render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
@@ -46,9 +46,9 @@ test('should render the UnsavedChangesModal component if showModal is true', asy
});
test('should only call onConfirmNavigation when clicking the Discard button', async () => {
const mockOnHide = vi.fn();
const mockHandleSave = vi.fn();
const mockOnConfirmNavigation = vi.fn();
const mockOnHide = jest.fn();
const mockHandleSave = jest.fn();
const mockOnConfirmNavigation = jest.fn();
render(
<UnsavedChangesModal
@@ -71,9 +71,9 @@ test('should only call onConfirmNavigation when clicking the Discard button', as
});
test('should only call handleSave when clicking the Save button', async () => {
const mockOnHide = vi.fn();
const mockHandleSave = vi.fn();
const mockOnConfirmNavigation = vi.fn();
const mockOnHide = jest.fn();
const mockHandleSave = jest.fn();
const mockOnConfirmNavigation = jest.fn();
render(
<UnsavedChangesModal
@@ -21,7 +21,7 @@ import { Button, Upload } from '..';
describe('Upload Component', () => {
test('renders upload button and triggers file upload', async () => {
const handleChange = vi.fn();
const handleChange = jest.fn();
render(
<Upload onChange={handleChange}>
@@ -20,7 +20,7 @@ import { renderHook } from '@testing-library/react-hooks';
import { useChangeEffect } from './useChangeEffect';
test('call callback the first time with undefined and value', () => {
const callback = vi.fn();
const callback = jest.fn();
renderHook(props => useChangeEffect(props.value, props.callback), {
initialProps: { value: 'value', callback },
});
@@ -29,7 +29,7 @@ test('call callback the first time with undefined and value', () => {
});
test('do not call callback 2 times if the value do not change', () => {
const callback = vi.fn();
const callback = jest.fn();
const hook = renderHook(
props => useChangeEffect(props.value, props.callback),
{
@@ -41,7 +41,7 @@ test('do not call callback 2 times if the value do not change', () => {
});
test('call callback whenever the value changes', () => {
const callback = vi.fn();
const callback = jest.fn();
const hook = renderHook(
props => useChangeEffect(props.value, props.callback),
{
@@ -20,7 +20,7 @@ import { renderHook } from '@testing-library/react-hooks';
import { useComponentDidMount } from './useComponentDidMount';
test('the effect should only be executed on the first render', () => {
const effect = vi.fn();
const effect = jest.fn();
const hook = renderHook(() => useComponentDidMount(effect));
expect(effect).toHaveBeenCalledTimes(1);
hook.rerender();
@@ -20,12 +20,12 @@ import { renderHook } from '@testing-library/react-hooks';
import { useComponentDidUpdate } from './useComponentDidUpdate';
test('the effect should not be executed on the first render', () => {
const effect = vi.fn();
const effect = jest.fn();
const hook = renderHook(props => useComponentDidUpdate(props.effect), {
initialProps: { effect },
});
expect(effect).toHaveBeenCalledTimes(0);
const changedEffect = vi.fn();
const changedEffect = jest.fn();
hook.rerender({ effect: changedEffect });
expect(changedEffect).toHaveBeenCalledTimes(1);
});
@@ -19,11 +19,9 @@
import { renderHook } from '@testing-library/react-hooks';
import { useElementOnScreen } from './useElementOnScreen';
vi.mock('react', { spy: true });
const observeMock = vi.fn();
const unobserveMock = vi.fn();
const IntersectionObserverMock = vi.fn();
const observeMock = jest.fn();
const unobserveMock = jest.fn();
const IntersectionObserverMock = jest.fn();
IntersectionObserverMock.prototype.observe = observeMock;
IntersectionObserverMock.prototype.unobserve = unobserveMock;
@@ -33,7 +31,7 @@ beforeEach(() => {
afterEach(() => {
IntersectionObserverMock.mockClear();
vi.clearAllMocks();
jest.clearAllMocks();
});
test('should return null and false on first render', () => {
@@ -68,7 +66,7 @@ test('should return isSticky as false when intersectionRatio >= 1', async () =>
});
test('should observe and unobserve element with IntersectionObserver', async () => {
vi.spyOn(global.React, 'useRef').mockReturnValue({ current: 'test' });
jest.spyOn(global.React, 'useRef').mockReturnValue({ current: 'test' });
const options = { threshold: 0.5 };
const { result, unmount } = renderHook(() => useElementOnScreen(options));
const [elementRef] = result.current;
@@ -87,7 +85,7 @@ test('should observe and unobserve element with IntersectionObserver', async ()
});
test('should not observe an element if it is null', () => {
vi.spyOn(global.React, 'useRef').mockReturnValue({ current: null });
jest.spyOn(global.React, 'useRef').mockReturnValue({ current: null });
const options = {};
const { result } = renderHook(() => useElementOnScreen(options));
const [ref, isSticky] = result.current;
@@ -98,7 +96,7 @@ test('should not observe an element if it is null', () => {
});
test('should not unobserve the element if it is null', () => {
vi.spyOn(global.React, 'useRef').mockReturnValue({ current: null });
jest.spyOn(global.React, 'useRef').mockReturnValue({ current: null });
const options = {};
const { result, unmount } = renderHook(() => useElementOnScreen(options));
const [ref, isSticky] = result.current;
@@ -20,7 +20,7 @@ import { renderHook } from '@testing-library/react-hooks';
import useCSSTextTruncation from './useCSSTextTruncation';
afterEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
test('should be false by default', () => {
@@ -36,7 +36,7 @@ test('should not truncate', () => {
const ref = { current: document.createElement('p') };
Object.defineProperty(ref.current, 'offsetWidth', { get: () => 100 });
Object.defineProperty(ref.current, 'scrollWidth', { get: () => 50 });
vi.spyOn(global.React, 'useRef').mockReturnValue({ current: ref.current });
jest.spyOn(global.React, 'useRef').mockReturnValue({ current: ref.current });
const { result } = renderHook(() =>
useCSSTextTruncation<HTMLParagraphElement>(),
@@ -50,7 +50,7 @@ test('should truncate', () => {
const ref = { current: document.createElement('p') };
Object.defineProperty(ref.current, 'offsetWidth', { get: () => 50 });
Object.defineProperty(ref.current, 'scrollWidth', { get: () => 100 });
vi.spyOn(global.React, 'useRef').mockReturnValue({ current: ref.current });
jest.spyOn(global.React, 'useRef').mockReturnValue({ current: ref.current });
const { result } = renderHook(() =>
useCSSTextTruncation<HTMLParagraphElement>(),
@@ -64,7 +64,7 @@ test('should not truncate with vertical orientation', () => {
const ref = { current: document.createElement('p') };
Object.defineProperty(ref.current, 'offsetHeight', { get: () => 100 });
Object.defineProperty(ref.current, 'scrollHeight', { get: () => 50 });
vi.spyOn(global.React, 'useRef').mockReturnValue({ current: ref.current });
jest.spyOn(global.React, 'useRef').mockReturnValue({ current: ref.current });
const { result } = renderHook(() =>
useCSSTextTruncation<HTMLParagraphElement>({
@@ -81,7 +81,7 @@ test('should truncate with vertical orientation', () => {
const ref = { current: document.createElement('p') };
Object.defineProperty(ref.current, 'offsetHeight', { get: () => 50 });
Object.defineProperty(ref.current, 'scrollHeight', { get: () => 100 });
vi.spyOn(global.React, 'useRef').mockReturnValue({ current: ref.current });
jest.spyOn(global.React, 'useRef').mockReturnValue({ current: ref.current });
const { result } = renderHook(() =>
useCSSTextTruncation<HTMLParagraphElement>({
@@ -19,10 +19,9 @@
import { renderHook } from '@testing-library/react-hooks';
import { RefObject } from 'react';
import useChildElementTruncation from './useChildElementTruncation';
import { Mock } from 'vitest';
let observeMock: Mock;
let disconnectMock: Mock;
let observeMock: jest.Mock;
let disconnectMock: jest.Mock;
let originalResizeObserver: typeof ResizeObserver;
const genElements = (
@@ -80,14 +79,12 @@ beforeAll(() => {
originalResizeObserver = window.ResizeObserver;
// Mock ResizeObserver
observeMock = vi.fn();
disconnectMock = vi.fn();
window.ResizeObserver = vi.fn(function () {
return {
observe: observeMock,
disconnect: disconnectMock,
};
} as unknown as typeof window.ResizeObserver);
observeMock = jest.fn();
disconnectMock = jest.fn();
window.ResizeObserver = jest.fn(() => ({
observe: observeMock,
disconnect: disconnectMock,
})) as unknown as typeof ResizeObserver;
});
afterAll(() => {
@@ -19,6 +19,15 @@
import { DatasourceType } from './types/Datasource';
const DATASOURCE_TYPE_MAP: Record<string, DatasourceType> = {
table: DatasourceType.Table,
query: DatasourceType.Query,
dataset: DatasourceType.Dataset,
sl_table: DatasourceType.SlTable,
saved_query: DatasourceType.SavedQuery,
semantic_view: DatasourceType.SemanticView,
};
export default class DatasourceKey {
readonly id: number;
@@ -27,8 +36,7 @@ export default class DatasourceKey {
constructor(key: string) {
const [idStr, typeStr] = key.split('__');
this.id = parseInt(idStr, 10);
this.type = DatasourceType.Table; // default to SqlaTable model
this.type = typeStr === 'query' ? DatasourceType.Query : this.type;
this.type = DATASOURCE_TYPE_MAP[typeStr] ?? DatasourceType.Table;
}
public toString() {
@@ -26,6 +26,7 @@ export enum DatasourceType {
Dataset = 'dataset',
SlTable = 'sl_table',
SavedQuery = 'saved_query',
SemanticView = 'semantic_view',
}
export interface Currency {
@@ -40,6 +41,13 @@ export interface Datasource {
id: number;
name: string;
type: DatasourceType;
/**
* The parent resource that owns this datasource.
* For SQL-based datasets this is the database; for semantic views it is the
* semantic layer. Use this field instead of the legacy `database` field when
* you only need the display name.
*/
parent?: { name: string };
columns: Column[];
metrics: Metric[];
description?: string;
@@ -19,7 +19,7 @@
import userEvent from '@testing-library/user-event';
import { ReactElement } from 'react';
import { render, RenderOptions } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import '@testing-library/jest-dom';
import { themeObject } from '@apache-superset/core/theme';
// Define the wrapper component outside
@@ -60,6 +60,7 @@ export enum FeatureFlag {
ListviewsDefaultCardView = 'LISTVIEWS_DEFAULT_CARD_VIEW',
Matrixify = 'MATRIXIFY',
ScheduledQueries = 'SCHEDULED_QUERIES',
SemanticLayers = 'SEMANTIC_LAYERS',
SqllabBackendPersistence = 'SQLLAB_BACKEND_PERSISTENCE',
SqlValidatorsByEngine = 'SQL_VALIDATORS_BY_ENGINE',
SshTunneling = 'SSH_TUNNELING',
@@ -32,7 +32,7 @@ test('withLabel prepends label to validator error message', () => {
});
test('withLabel passes value and state to underlying validator', () => {
const validator = vi.fn(() => false as false);
const validator = jest.fn(() => false as false);
const labeled = withLabel(validator, 'Field');
labeled('value', { someState: true });
expect(validator).toHaveBeenCalledWith('value', { someState: true });
@@ -21,15 +21,15 @@ import { triggerResizeObserver } from 'resize-observer-polyfill';
import { promiseTimeout, WithLegend } from '@superset-ui/core';
import { render } from '@testing-library/react';
let renderChart = vi.fn();
let renderLegend = vi.fn();
let renderChart = jest.fn();
let renderLegend = jest.fn();
// TODO: rewrite to rtl
/* oxlint-disable-next-line jest/no-disabled-tests */
describe.skip('WithLegend', () => {
beforeEach(() => {
renderChart = vi.fn(() => <div className="chart" />);
renderLegend = vi.fn(() => <div className="legend" />);
renderChart = jest.fn(() => <div className="chart" />);
renderLegend = jest.fn(() => <div className="legend" />);
});
test('sets className', () => {
@@ -18,11 +18,23 @@
*/
import '@testing-library/jest-dom';
import { render, screen, act } from '@testing-library/react';
import ChartClient from '../../../src/chart/clients/ChartClient';
import ChartDataProvider, {
ChartDataProviderProps,
} from '../../../src/chart/components/ChartDataProvider';
import { bigNumberFormData } from '../fixtures/formData';
// Keep existing mock setup
const defaultMockLoadFormData = jest.fn(({ formData }: { formData: unknown }) =>
Promise.resolve(formData),
);
type MockLoadFormData =
| typeof defaultMockLoadFormData
| jest.Mock<Promise<unknown>, unknown[]>;
let mockLoadFormData: MockLoadFormData = defaultMockLoadFormData;
function createPromise<T>(input: T) {
return Promise.resolve(input);
}
@@ -31,34 +43,24 @@ function createArrayPromise<T>(input: T) {
return Promise.resolve([input]);
}
const { mockLoadDatasource, mockLoadQueryData, mockLoadFormData } = vi.hoisted(
() => ({
mockLoadDatasource: vi.fn().mockImplementation(createPromise),
mockLoadQueryData: vi.fn().mockImplementation(createArrayPromise),
mockLoadFormData: vi.fn(({ formData }: { formData: unknown }) =>
Promise.resolve(formData),
),
}),
const mockLoadDatasource = jest.fn<Promise<unknown>, unknown[]>(createPromise);
const mockLoadQueryData = jest.fn<Promise<unknown>, unknown[]>(
createArrayPromise,
);
vi.mock('../../../src/chart/clients/ChartClient', async importActual => {
const actual = (await importActual()) as Record<any, any>;
return {
...actual,
default: function () {
return {
...actual.default,
loadDatasource: mockLoadDatasource,
loadFormData: mockLoadFormData,
loadQueryData: mockLoadQueryData,
};
},
};
});
const actual = jest.requireActual('../../../src/chart/clients/ChartClient');
jest.spyOn(actual, 'default').mockImplementation(() => ({
loadDatasource: mockLoadDatasource,
loadFormData: mockLoadFormData,
loadQueryData: mockLoadQueryData,
}));
const ChartClientMock = ChartClient as jest.Mock<ChartClient>;
describe('ChartDataProvider', () => {
beforeEach(() => {
mockLoadFormData.mockClear();
ChartClientMock.mockClear();
mockLoadFormData = defaultMockLoadFormData;
mockLoadFormData.mockClear();
mockLoadDatasource.mockClear();
mockLoadQueryData.mockClear();
@@ -79,6 +81,11 @@ describe('ChartDataProvider', () => {
return render(<ChartDataProvider {...props} {...overrideProps} />);
}
test('instantiates a new ChartClient()', () => {
setup();
expect(ChartClientMock).toHaveBeenCalledTimes(1);
});
describe('ChartClient.loadFormData', () => {
test('calls method on mount', () => {
setup();
@@ -93,7 +100,7 @@ describe('ChartDataProvider', () => {
const options = { host: 'override' };
setup({ formDataRequestOptions: options });
expect(mockLoadFormData).toHaveBeenCalledTimes(1);
expect((mockLoadFormData.mock.calls[0] as any[])[1]).toEqual(options);
expect(mockLoadFormData.mock.calls[0][1]).toEqual(options);
});
test('calls ChartClient.loadFormData when formData or sliceId change', async () => {
@@ -270,7 +277,7 @@ describe('ChartDataProvider', () => {
describe('callbacks', () => {
test('calls onLoaded when loaded', async () => {
const onLoaded = vi.fn();
const onLoaded = jest.fn();
mockLoadFormData.mockResolvedValue(props.formData);
mockLoadQueryData.mockResolvedValue([props.formData]);
mockLoadDatasource.mockResolvedValue(props.formData.datasource);
@@ -290,7 +297,7 @@ describe('ChartDataProvider', () => {
});
test('calls onError upon request error', async () => {
const onError = vi.fn();
const onError = jest.fn();
mockLoadFormData.mockRejectedValue(new Error('error'));
setup({ onError });
@@ -304,7 +311,7 @@ describe('ChartDataProvider', () => {
});
test('calls onError upon JS error', async () => {
const onError = vi.fn();
const onError = jest.fn();
mockLoadFormData.mockImplementation(() => {
throw new Error('non-async error');
});
@@ -19,6 +19,7 @@
import '@testing-library/jest-dom';
import { render, screen } from '@superset-ui/core/spec';
import mockConsole, { RestoreConsole } from 'jest-mock-console';
import { triggerResizeObserver } from 'resize-observer-polyfill';
import { ErrorBoundary } from 'react-error-boundary';
@@ -33,19 +34,18 @@ import {
import { isMatrixifyEnabled } from '../../../src/chart/types/matrixify';
import MatrixifyGridRenderer from '../../../src/chart/components/Matrixify/MatrixifyGridRenderer';
import { Mock } from 'vitest';
// Mock Matrixify imports
vi.mock('../../../src/chart/types/matrixify', () => ({
isMatrixifyEnabled: vi.fn(() => false),
getMatrixifyConfig: vi.fn(() => null),
jest.mock('../../../src/chart/types/matrixify', () => ({
isMatrixifyEnabled: jest.fn(() => false),
getMatrixifyConfig: jest.fn(() => null),
}));
vi.mock(
jest.mock(
'../../../src/chart/components/Matrixify/MatrixifyGridRenderer',
() => ({
__esModule: true,
default: vi.fn(() => null),
default: jest.fn(() => null),
}),
);
@@ -63,7 +63,9 @@ function getDimensionText(container: HTMLElement) {
}
describe('SuperChart', () => {
vi.setConfig({ testTimeout: 5000 });
jest.setTimeout(5000);
let restoreConsole: RestoreConsole;
const plugins = [
new DiligentChartPlugin().configure({ key: ChartKeys.DILIGENT }),
@@ -76,6 +78,15 @@ describe('SuperChart', () => {
});
});
beforeEach(() => {
restoreConsole = mockConsole();
triggerResizeObserver([]); // Reset any pending resize observers
});
afterEach(() => {
restoreConsole();
});
describe('includes ErrorBoundary', () => {
let expectedErrors = 0;
let actualErrors = 0;
@@ -117,7 +128,9 @@ describe('SuperChart', () => {
test('renders custom FallbackComponent', async () => {
expectedErrors = 1;
const CustomFallbackComponent = vi.fn(() => <div>Custom Fallback!</div>);
const CustomFallbackComponent = jest.fn(() => (
<div>Custom Fallback!</div>
));
render(
<SuperChart
@@ -134,7 +147,7 @@ describe('SuperChart', () => {
});
test('call onErrorBoundary', async () => {
expectedErrors = 1;
const handleError = vi.fn();
const handleError = jest.fn();
render(
<SuperChart
chartType={ChartKeys.BUGGY}
@@ -152,8 +165,8 @@ describe('SuperChart', () => {
// Update the test cases
test('does not include ErrorBoundary if told so', async () => {
expectedErrors = 1;
const inactiveErrorHandler = vi.fn();
const activeErrorHandler = vi.fn();
const inactiveErrorHandler = jest.fn();
const activeErrorHandler = jest.fn();
render(
<ErrorBoundary
fallbackRender={() => <div>Error!</div>}
@@ -182,7 +195,7 @@ describe('SuperChart', () => {
// Update test cases
// Update timeout for all async tests
vi.setConfig({ testTimeout: 10000 });
jest.setTimeout(10000);
// Update the props test to wait for component to render
test('passes the props to renderer correctly', async () => {
@@ -216,7 +229,7 @@ describe('SuperChart', () => {
// Update dimension tests to wait for resize observer
// First, increase the timeout for all tests
vi.setConfig({ testTimeout: 20000 });
jest.setTimeout(20000);
// Update the waitForDimensions helper to include a retry mechanism
// Update waitForDimensions to avoid await in loop
@@ -447,12 +460,13 @@ describe('SuperChart', () => {
});
test('should render MatrixifyGridRenderer when matrixify is enabled with empty data', () => {
const mockIsMatrixifyEnabled = isMatrixifyEnabled as Mock<
const mockIsMatrixifyEnabled = isMatrixifyEnabled as jest.MockedFunction<
typeof isMatrixifyEnabled
>;
const mockMatrixifyGridRenderer = MatrixifyGridRenderer as Mock<
typeof MatrixifyGridRenderer
>;
const mockMatrixifyGridRenderer =
MatrixifyGridRenderer as jest.MockedFunction<
typeof MatrixifyGridRenderer
>;
mockIsMatrixifyEnabled.mockReturnValue(true);
@@ -471,12 +485,13 @@ describe('SuperChart', () => {
});
test('should render MatrixifyGridRenderer when matrixify is enabled with null data', () => {
const mockIsMatrixifyEnabled = isMatrixifyEnabled as Mock<
const mockIsMatrixifyEnabled = isMatrixifyEnabled as jest.MockedFunction<
typeof isMatrixifyEnabled
>;
const mockMatrixifyGridRenderer = MatrixifyGridRenderer as Mock<
typeof MatrixifyGridRenderer
>;
const mockMatrixifyGridRenderer =
MatrixifyGridRenderer as jest.MockedFunction<
typeof MatrixifyGridRenderer
>;
mockIsMatrixifyEnabled.mockReturnValue(true);
@@ -495,12 +510,13 @@ describe('SuperChart', () => {
});
test('should ignore custom noResults component when matrixify is enabled', () => {
const mockIsMatrixifyEnabled = isMatrixifyEnabled as Mock<
const mockIsMatrixifyEnabled = isMatrixifyEnabled as jest.MockedFunction<
typeof isMatrixifyEnabled
>;
const mockMatrixifyGridRenderer = MatrixifyGridRenderer as Mock<
typeof MatrixifyGridRenderer
>;
const mockMatrixifyGridRenderer =
MatrixifyGridRenderer as jest.MockedFunction<
typeof MatrixifyGridRenderer
>;
mockIsMatrixifyEnabled.mockReturnValue(true);
@@ -524,15 +540,16 @@ describe('SuperChart', () => {
});
test('should apply error boundary to matrixify grid renderer', () => {
const mockIsMatrixifyEnabled = isMatrixifyEnabled as Mock<
const mockIsMatrixifyEnabled = isMatrixifyEnabled as jest.MockedFunction<
typeof isMatrixifyEnabled
>;
const mockMatrixifyGridRenderer = MatrixifyGridRenderer as Mock<
typeof MatrixifyGridRenderer
>;
const mockMatrixifyGridRenderer =
MatrixifyGridRenderer as jest.MockedFunction<
typeof MatrixifyGridRenderer
>;
mockIsMatrixifyEnabled.mockReturnValue(true);
const onErrorBoundary = vi.fn();
const onErrorBoundary = jest.fn();
render(
<SuperChart
@@ -18,6 +18,7 @@
*/
import '@testing-library/jest-dom';
import mockConsole, { RestoreConsole } from 'jest-mock-console';
import { ChartProps } from '@superset-ui/core';
import { supersetTheme } from '@apache-superset/core/theme';
import { render, screen, waitFor } from '@superset-ui/core/spec';
@@ -37,8 +38,10 @@ describe('SuperChartCore', () => {
new SlowChartPlugin().configure({ key: ChartKeys.SLOW }),
];
let restoreConsole: RestoreConsole;
beforeAll(() => {
vi.setConfig({ testTimeout: 30000 });
jest.setTimeout(30000);
plugins.forEach(p => {
p.unregister().register();
});
@@ -50,6 +53,14 @@ describe('SuperChartCore', () => {
});
});
beforeEach(() => {
restoreConsole = mockConsole();
});
afterEach(() => {
restoreConsole();
});
describe('registered charts', () => {
test('renders registered chart', async () => {
const { container } = render(
@@ -19,6 +19,7 @@
import '@testing-library/jest-dom';
import { ComponentType } from 'react';
import mockConsole, { RestoreConsole } from 'jest-mock-console';
import { render as renderTestComponent, screen } from '@testing-library/react';
import createLoadableRenderer, {
LoadableRenderer as LoadableRendererType,
@@ -28,19 +29,21 @@ describe('createLoadableRenderer', () => {
function TestComponent() {
return <div className="test-component">test</div>;
}
let loadChartSuccess = vi.fn(() => Promise.resolve(TestComponent));
let loadChartSuccess = jest.fn(() => Promise.resolve(TestComponent));
let render: (loaded: { Chart: ComponentType }) => JSX.Element;
let loading: () => JSX.Element;
let LoadableRenderer: LoadableRendererType<{}>;
let restoreConsole: RestoreConsole;
beforeEach(() => {
loadChartSuccess = vi.fn(() => Promise.resolve(TestComponent));
render = vi.fn(loaded => {
restoreConsole = mockConsole();
loadChartSuccess = jest.fn(() => Promise.resolve(TestComponent));
render = jest.fn(loaded => {
const { Chart } = loaded;
return <Chart />;
});
loading = vi.fn(() => <div>Loading</div>);
loading = jest.fn(() => <div>Loading</div>);
LoadableRenderer = createLoadableRenderer({
loader: {
@@ -51,6 +54,10 @@ describe('createLoadableRenderer', () => {
});
});
afterEach(() => {
restoreConsole();
});
describe('returns a LoadableRenderer class', () => {
test('LoadableRenderer.preload() preloads the lazy-load components', () => {
expect(LoadableRenderer.preload).toBeInstanceOf(Function);
@@ -59,8 +66,8 @@ describe('createLoadableRenderer', () => {
});
test('calls onRenderSuccess when succeeds', async () => {
const onRenderSuccess = vi.fn();
const onRenderFailure = vi.fn();
const onRenderSuccess = jest.fn();
const onRenderFailure = jest.fn();
renderTestComponent(
<LoadableRenderer
onRenderSuccess={onRenderSuccess}
@@ -68,7 +75,7 @@ describe('createLoadableRenderer', () => {
/>,
);
expect(loadChartSuccess).toHaveBeenCalled();
vi.useRealTimers();
jest.useRealTimers();
await new Promise(resolve => setTimeout(resolve, 10));
expect(render).toHaveBeenCalledTimes(1);
expect(onRenderSuccess).toHaveBeenCalledTimes(1);
@@ -77,7 +84,7 @@ describe('createLoadableRenderer', () => {
test('calls onRenderFailure when fails', () =>
new Promise(done => {
const loadChartFailure = vi.fn(() =>
const loadChartFailure = jest.fn(() =>
Promise.reject(new Error('Invalid chart')),
);
const FailedRenderer = createLoadableRenderer({
@@ -87,8 +94,8 @@ describe('createLoadableRenderer', () => {
loading,
render,
});
const onRenderSuccess = vi.fn();
const onRenderFailure = vi.fn();
const onRenderSuccess = jest.fn();
const onRenderFailure = jest.fn();
renderTestComponent(
<FailedRenderer
onRenderSuccess={onRenderSuccess}
@@ -106,7 +113,7 @@ describe('createLoadableRenderer', () => {
test('onRenderFailure is optional', () =>
new Promise(done => {
const loadChartFailure = vi.fn(() =>
const loadChartFailure = jest.fn(() =>
Promise.reject(new Error('Invalid chart')),
);
const FailedRenderer = createLoadableRenderer({
@@ -25,7 +25,7 @@ import { render, screen } from '@testing-library/react';
import { RenderFuncType } from '../../../src/chart/components/reactify';
describe('reactify(renderFn)', () => {
const renderFn: RenderFuncType<{ content?: string }> = vi.fn(
const renderFn: RenderFuncType<{ content?: string }> = jest.fn(
(element, props) => {
const container = element;
container.innerHTML = '';
@@ -45,7 +45,7 @@ describe('reactify(renderFn)', () => {
content: 'ghi',
};
const willUnmountCb = vi.fn();
const willUnmountCb = jest.fn();
const TheChart = reactify(renderFn);
const TheChartWithWillUnmountHook = reactify(renderFn, {
@@ -127,7 +127,7 @@ describe('reactify(renderFn)', () => {
});
});
test('does not try to render if not mounted', () => {
const anotherRenderFn = vi.fn();
const anotherRenderFn = jest.fn();
const AnotherChart = reactify(anotherRenderFn); // enables valid new AnotherChart() call
// @ts-expect-error
new AnotherChart({ id: 'test' }).execute();

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