mirror of
https://github.com/apache/superset.git
synced 2026-08-12 11:11:01 +00:00
Compare commits
61
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6eeb107b27 | ||
|
|
e9a1513c39 | ||
|
|
cb979b01e2 | ||
|
|
34f062c0a4 | ||
|
|
727c61aa71 | ||
|
|
cb35bdf643 | ||
|
|
b0a3661611 | ||
|
|
76dec98f33 | ||
|
|
bde6bb1962 | ||
|
|
dc1fafce95 | ||
|
|
4b6cb09ed1 | ||
|
|
21b9ce3562 | ||
|
|
b96dd6cf99 | ||
|
|
03f2530b9d | ||
|
|
8b9a0eae33 | ||
|
|
7c9ce279a9 | ||
|
|
cac8656988 | ||
|
|
99893f75c4 | ||
|
|
72ad2acb86 | ||
|
|
71f04979e9 | ||
|
|
b7a7d83ea5 | ||
|
|
3f0d302b56 | ||
|
|
c7f4c1b818 | ||
|
|
51c6708caa | ||
|
|
8a9764a4b5 | ||
|
|
e7cb3c5a1e | ||
|
|
a86424bca3 | ||
|
|
6ca5464d27 | ||
|
|
0f08f016d2 | ||
|
|
65fb2ff834 | ||
|
|
d659089c59 | ||
|
|
5e046a857c | ||
|
|
36554237aa | ||
|
|
6f93e1cbb1 | ||
|
|
913259299e | ||
|
|
2351e0ead7 | ||
|
|
8c6f211003 | ||
|
|
0e3d78817f | ||
|
|
f0c8304e24 | ||
|
|
80233aed46 | ||
|
|
6f350428df | ||
|
|
548ccfde44 | ||
|
|
596008203c | ||
|
|
ff46c86df3 | ||
|
|
4e30638024 | ||
|
|
efa9159cc8 | ||
|
|
14668f37bd | ||
|
|
27a2466855 | ||
|
|
e35c6946ec | ||
|
|
12c5bfa0a5 | ||
|
|
0303a234a3 | ||
|
|
09e9927652 | ||
|
|
3f9ea361bb | ||
|
|
f1047140ee | ||
|
|
15e3ab4493 | ||
|
|
755aa2e32f | ||
|
|
17d1ed7353 | ||
|
|
9c1bcb70d0 | ||
|
|
6d7cfac8b2 | ||
|
|
31754a39c9 | ||
|
|
bde48e563e |
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Vendored
+6
@@ -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,
|
||||
|
||||
@@ -285,6 +285,7 @@ module = [
|
||||
"superset.tags.filters",
|
||||
"superset.commands.security.update",
|
||||
"superset.commands.security.create",
|
||||
"superset.semantic_layers.api",
|
||||
]
|
||||
warn_unused_ignores = false
|
||||
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -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,
|
||||
|
||||
@@ -19,8 +19,7 @@
|
||||
"__webpack_public_path__": "writable",
|
||||
"__webpack_init_sharing__": "readonly",
|
||||
"__webpack_share_scopes__": "readonly",
|
||||
"jest": "readonly",
|
||||
"vi": "readonly"
|
||||
"jest": "readonly"
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
|
||||
Generated
+393
-1715
File diff suppressed because it is too large
Load Diff
@@ -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:');
|
||||
|
||||
+3
-3
@@ -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" />,
|
||||
}));
|
||||
|
||||
|
||||
+2
-2
@@ -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',
|
||||
|
||||
+4
-4
@@ -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" />,
|
||||
}));
|
||||
|
||||
|
||||
+10
-10
@@ -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 });
|
||||
|
||||
|
||||
+5
-5
@@ -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)
|
||||
|
||||
|
||||
+4
-4
@@ -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: [],
|
||||
|
||||
+1
-1
@@ -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"
|
||||
},
|
||||
|
||||
+1
-1
@@ -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 */
|
||||
|
||||
+5
-6
@@ -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', () => {
|
||||
|
||||
+25
-25
@@ -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}
|
||||
|
||||
+4
-4
@@ -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);
|
||||
|
||||
+2
-2
@@ -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');
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+4
-4
@@ -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);
|
||||
|
||||
+7
-7
@@ -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',
|
||||
() => ({}),
|
||||
);
|
||||
|
||||
+4
-4
@@ -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' }));
|
||||
|
||||
+7
-7
@@ -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}>
|
||||
|
||||
+1
-1
@@ -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 = {
|
||||
|
||||
+8
-8
@@ -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} />);
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+7
-7
@@ -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(
|
||||
|
||||
+1
-1
@@ -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,
|
||||
});
|
||||
|
||||
+5
-5
@@ -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);
|
||||
|
||||
+5
-5
@@ -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,
|
||||
};
|
||||
|
||||
+2
-2
@@ -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');
|
||||
|
||||
+1
-1
@@ -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();
|
||||
|
||||
+19
-1
@@ -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 =
|
||||
|
||||
+1
-1
@@ -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');
|
||||
|
||||
+1
-1
@@ -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(() => {
|
||||
|
||||
+1
-1
@@ -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', () => {
|
||||
|
||||
+5
-5
@@ -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,
|
||||
|
||||
+3
-3
@@ -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"
|
||||
|
||||
+1
-1
@@ -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 () => {
|
||||
|
||||
+1
-1
@@ -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" />
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
+27
-27
@@ -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', () => {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+43
-43
@@ -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();
|
||||
|
||||
|
||||
+11
-11
@@ -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,
|
||||
|
||||
+9
-5
@@ -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,
|
||||
|
||||
+14
-15
@@ -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
|
||||
|
||||
+4
-5
@@ -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';
|
||||
|
||||
|
||||
+4
-4
@@ -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();
|
||||
|
||||
+4
-4
@@ -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}
|
||||
|
||||
+12
-13
@@ -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}
|
||||
|
||||
+6
-6
@@ -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}>
|
||||
|
||||
+3
-3
@@ -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),
|
||||
{
|
||||
|
||||
+1
-1
@@ -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();
|
||||
|
||||
+2
-2
@@ -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);
|
||||
});
|
||||
|
||||
+7
-9
@@ -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;
|
||||
|
||||
+5
-5
@@ -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>({
|
||||
|
||||
+8
-11
@@ -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 });
|
||||
|
||||
+4
-4
@@ -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', () => {
|
||||
|
||||
+34
-27
@@ -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');
|
||||
});
|
||||
|
||||
+47
-30
@@ -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
|
||||
|
||||
+12
-1
@@ -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(
|
||||
|
||||
+18
-11
@@ -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
Reference in New Issue
Block a user