mirror of
https://github.com/apache/superset.git
synced 2026-05-16 21:35:08 +00:00
Compare commits
13 Commits
sec/resolv
...
fix/playwr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d2b655c22 | ||
|
|
29b94ced71 | ||
|
|
736a51c13f | ||
|
|
34c28f7b76 | ||
|
|
62c86abcd1 | ||
|
|
caa357e0d2 | ||
|
|
cc21683118 | ||
|
|
114d88468b | ||
|
|
48c0bea906 | ||
|
|
a46925d431 | ||
|
|
0df9cc986a | ||
|
|
ade901ed04 | ||
|
|
1e2d0b5f5b |
44
superset-frontend/package-lock.json
generated
44
superset-frontend/package-lock.json
generated
@@ -102,7 +102,7 @@
|
||||
"geostyler-style": "11.0.2",
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"google-auth-library": "^10.6.2",
|
||||
"immer": "^11.1.7",
|
||||
"immer": "^11.1.8",
|
||||
"interweave": "^13.1.1",
|
||||
"jquery": "^4.0.0",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
@@ -121,7 +121,7 @@
|
||||
"query-string": "9.3.1",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^18.2.0",
|
||||
"react-arborist": "^3.5.0",
|
||||
"react-arborist": "^3.6.1",
|
||||
"react-checkbox-tree": "^1.8.0",
|
||||
"react-diff-viewer-continued": "^4.2.2",
|
||||
"react-dnd": "^11.1.3",
|
||||
@@ -23972,9 +23972,9 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fast-xml-builder": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz",
|
||||
"integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz",
|
||||
"integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -23983,7 +23983,8 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-expression-matcher": "^1.1.3"
|
||||
"path-expression-matcher": "^1.5.0",
|
||||
"xml-naming": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
@@ -27206,9 +27207,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "11.1.7",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.7.tgz",
|
||||
"integrity": "sha512-LFVFtAROHcDy1er5UI6nodRFnZ2SgdCXhfNSI+DpObO8N7Pur/muBGsjzH5wpnFHCYhYVQxZskCkV4koQ//3/Q==",
|
||||
"version": "11.1.8",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz",
|
||||
"integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -41343,9 +41344,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-arborist": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-arborist/-/react-arborist-3.5.0.tgz",
|
||||
"integrity": "sha512-FdXOICSt7P2h+Pxin1ULN02b4qrXJznNcshgwwWVtuYMLWSJcD245PQ4HOSj/Lr2T1uEegmnEm5Lbns2hUUsqg==",
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-arborist/-/react-arborist-3.6.1.tgz",
|
||||
"integrity": "sha512-h2/sPz6PXL79h7mOWjCA6Y5WNUKmA0kL8Uh6RYZQbYk7UOFBd86Jeoga4RjHMBYpOWpBPYrOJOE3HbIPUETp8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-dnd": "^14.0.3",
|
||||
@@ -49463,6 +49464,21 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-naming": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz",
|
||||
"integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-utils": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz",
|
||||
@@ -51011,7 +51027,7 @@
|
||||
"acorn": "^8.16.0",
|
||||
"d3-array": "^3.2.4",
|
||||
"lodash": "^4.18.1",
|
||||
"zod": "^4.4.3"
|
||||
"zod": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
@@ -51212,7 +51228,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/d3-scale": "^4.0.9",
|
||||
"d3-cloud": "^1.2.8",
|
||||
"d3-cloud": "^1.2.9",
|
||||
"d3-scale": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
"geostyler-style": "11.0.2",
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"google-auth-library": "^10.6.2",
|
||||
"immer": "^11.1.7",
|
||||
"immer": "^11.1.8",
|
||||
"interweave": "^13.1.1",
|
||||
"jquery": "^4.0.0",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
@@ -202,7 +202,7 @@
|
||||
"query-string": "9.3.1",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^18.2.0",
|
||||
"react-arborist": "^3.5.0",
|
||||
"react-arborist": "^3.6.1",
|
||||
"react-checkbox-tree": "^1.8.0",
|
||||
"react-diff-viewer-continued": "^4.2.2",
|
||||
"react-dnd": "^11.1.3",
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"react-js-cron": "^5.2.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-resize-detector": "^7.1.2",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"react-ultimate-pagination": "^1.3.2",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"acorn": "^8.16.0",
|
||||
"d3-array": "^3.2.4",
|
||||
"lodash": "^4.18.1",
|
||||
"zod": "^4.4.3"
|
||||
"zod": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@math.gl/web-mercator": "^4.1.0",
|
||||
"mapbox-gl": "^3.23.1",
|
||||
"maplibre-gl": "^5.24.0",
|
||||
"react-map-gl": "^8.1.0",
|
||||
"react-map-gl": "^8.1.1",
|
||||
"supercluster": "^8.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/d3-scale": "^4.0.9",
|
||||
"d3-cloud": "^1.2.8",
|
||||
"d3-cloud": "^1.2.9",
|
||||
"d3-scale": "^4.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
30
superset-websocket/package-lock.json
generated
30
superset-websocket/package-lock.json
generated
@@ -23,7 +23,7 @@
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^25.7.0",
|
||||
"@types/node": "^25.8.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.59.3",
|
||||
@@ -1798,13 +1798,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz",
|
||||
"integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==",
|
||||
"version": "25.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
|
||||
"integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.21.0"
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/stack-utils": {
|
||||
@@ -6237,9 +6237,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz",
|
||||
"integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==",
|
||||
"version": "7.24.6",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -7894,12 +7894,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "25.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz",
|
||||
"integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==",
|
||||
"version": "25.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
|
||||
"integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"undici-types": "~7.21.0"
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"@types/stack-utils": {
|
||||
@@ -11063,9 +11063,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz",
|
||||
"integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==",
|
||||
"version": "7.24.6",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||
"dev": true
|
||||
},
|
||||
"unix-dgram": {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^25.7.0",
|
||||
"@types/node": "^25.8.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.59.3",
|
||||
|
||||
@@ -353,10 +353,10 @@ class OAuth2RedirectError(SupersetErrorException):
|
||||
|
||||
See the `OAuth2RedirectMessage.tsx` component for more details of how this
|
||||
information is handled.
|
||||
|
||||
TODO (betodealmeida): change status to 403.
|
||||
"""
|
||||
|
||||
status = 403
|
||||
|
||||
def __init__(self, url: str, tab_id: str, redirect_uri: str):
|
||||
super().__init__(
|
||||
SupersetError(
|
||||
|
||||
@@ -26,7 +26,6 @@ import logging
|
||||
import math
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from superset.commands.chart.data.get_data_command import ChartDataCommand
|
||||
from superset.mcp_service.chart.schemas import (
|
||||
ASCIIPreview,
|
||||
ChartError,
|
||||
@@ -78,6 +77,7 @@ def generate_preview_from_form_data(
|
||||
"""
|
||||
try:
|
||||
# Execute query to get data
|
||||
from superset.commands.chart.data.get_data_command import ChartDataCommand
|
||||
from superset.connectors.sqla.models import SqlaTable
|
||||
from superset.extensions import db
|
||||
|
||||
|
||||
@@ -10403,7 +10403,7 @@ msgstr "Běží"
|
||||
|
||||
#, fuzzy, python-format
|
||||
msgid "Running block %(block_num)s out of %(block_count)s"
|
||||
msgstr "Spouští se příkaz %(statement_num)s z %(statement_count)s"
|
||||
msgstr "Spouští se příkaz %(block_num)s z %(block_count)s"
|
||||
|
||||
msgid "SAT"
|
||||
msgstr "SO"
|
||||
|
||||
@@ -28,6 +28,11 @@ logger = logging.getLogger(__name__)
|
||||
# Time to wait after scrolling for content to settle and load (in milliseconds)
|
||||
SCROLL_SETTLE_TIMEOUT_MS = 1000
|
||||
|
||||
try:
|
||||
from playwright.sync_api import TimeoutError as PlaywrightTimeout
|
||||
except ImportError:
|
||||
PlaywrightTimeout = Exception
|
||||
|
||||
if TYPE_CHECKING:
|
||||
try:
|
||||
from playwright.sync_api import Page
|
||||
@@ -80,7 +85,10 @@ def combine_screenshot_tiles(screenshot_tiles: list[bytes]) -> bytes:
|
||||
|
||||
|
||||
def take_tiled_screenshot(
|
||||
page: "Page", element_name: str, tile_height: int
|
||||
page: "Page",
|
||||
element_name: str,
|
||||
tile_height: int,
|
||||
load_wait: int = 60,
|
||||
) -> bytes | None:
|
||||
"""
|
||||
Take a tiled screenshot of a large dashboard by scrolling and capturing sections.
|
||||
@@ -89,6 +97,7 @@ def take_tiled_screenshot(
|
||||
page: Playwright page object
|
||||
element_name: CSS class name of the element to screenshot
|
||||
tile_height: Height of each tile in pixels
|
||||
load_wait: Seconds to wait for charts to load per tile (default 60)
|
||||
|
||||
Returns:
|
||||
Combined screenshot bytes or None if failed
|
||||
@@ -139,6 +148,31 @@ def take_tiled_screenshot(
|
||||
)
|
||||
# Wait for scroll to settle and content to load
|
||||
page.wait_for_timeout(SCROLL_SETTLE_TIMEOUT_MS)
|
||||
# Wait for any loading spinners visible in the current viewport to clear.
|
||||
# Only check viewport-visible spinners to avoid blocking on
|
||||
# virtualization placeholders rendered for off-screen charts.
|
||||
try:
|
||||
page.wait_for_function(
|
||||
"""() => {
|
||||
const els = document.querySelectorAll('.loading');
|
||||
for (const el of els) {
|
||||
const r = el.getBoundingClientRect();
|
||||
if (r.top < window.innerHeight && r.bottom > 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}""",
|
||||
timeout=load_wait * 1000,
|
||||
)
|
||||
except PlaywrightTimeout:
|
||||
logger.warning(
|
||||
"Timed out waiting for visible spinners to clear on tile %s/%s "
|
||||
"(load_wait=%ss)",
|
||||
i + 1,
|
||||
num_tiles,
|
||||
load_wait,
|
||||
)
|
||||
|
||||
# Calculate what portion of the element we want to capture for this tile
|
||||
tile_start_in_element = i * tile_height
|
||||
|
||||
@@ -295,21 +295,6 @@ class WebDriverPlaywright(WebDriverProxy):
|
||||
url,
|
||||
)
|
||||
raise
|
||||
try:
|
||||
# charts took too long to load
|
||||
logger.debug(
|
||||
"Wait for loading element of charts to be gone at url: %s", url
|
||||
)
|
||||
page.wait_for_function(
|
||||
"() => document.querySelectorAll('.loading').length === 0",
|
||||
timeout=self._screenshot_load_wait * 1000,
|
||||
)
|
||||
except PlaywrightTimeout:
|
||||
logger.warning(
|
||||
"Timed out waiting for charts to load at url %s", url
|
||||
)
|
||||
raise
|
||||
|
||||
selenium_animation_wait = app.config[
|
||||
"SCREENSHOT_SELENIUM_ANIMATION_WAIT"
|
||||
]
|
||||
@@ -368,7 +353,12 @@ class WebDriverPlaywright(WebDriverProxy):
|
||||
page.set_viewport_size(
|
||||
{"height": tile_height, "width": viewport_width}
|
||||
)
|
||||
img = take_tiled_screenshot(page, element_name, tile_height)
|
||||
img = take_tiled_screenshot(
|
||||
page,
|
||||
element_name,
|
||||
tile_height,
|
||||
load_wait=self._screenshot_load_wait,
|
||||
)
|
||||
if img is None:
|
||||
logger.warning(
|
||||
(
|
||||
@@ -380,10 +370,50 @@ class WebDriverPlaywright(WebDriverProxy):
|
||||
page, element, element_name
|
||||
)
|
||||
else:
|
||||
# Standard screenshot captures the full element including
|
||||
# below-the-fold content, so wait for all spinners globally.
|
||||
try:
|
||||
logger.debug(
|
||||
"Wait for loading element of charts to be gone"
|
||||
" at url: %s",
|
||||
url,
|
||||
)
|
||||
page.wait_for_function(
|
||||
"() => document.querySelectorAll("
|
||||
"'.loading').length === 0",
|
||||
timeout=self._screenshot_load_wait * 1000,
|
||||
)
|
||||
except PlaywrightTimeout:
|
||||
logger.warning(
|
||||
"Timed out waiting for charts to load at url %s "
|
||||
"(SCREENSHOT_LOAD_WAIT=%ss)",
|
||||
url,
|
||||
self._screenshot_load_wait,
|
||||
)
|
||||
raise
|
||||
img = WebDriverPlaywright._get_screenshot(
|
||||
page, element, element_name
|
||||
)
|
||||
else:
|
||||
# Standard screenshot captures the full element including
|
||||
# below-the-fold content, so wait for all spinners globally.
|
||||
try:
|
||||
logger.debug(
|
||||
"Wait for loading element of charts to be gone at url: %s",
|
||||
url,
|
||||
)
|
||||
page.wait_for_function(
|
||||
"() => document.querySelectorAll('.loading').length === 0",
|
||||
timeout=self._screenshot_load_wait * 1000,
|
||||
)
|
||||
except PlaywrightTimeout:
|
||||
logger.warning(
|
||||
"Timed out waiting for charts to load at url %s "
|
||||
"(SCREENSHOT_LOAD_WAIT=%ss)",
|
||||
url,
|
||||
self._screenshot_load_wait,
|
||||
)
|
||||
raise
|
||||
img = WebDriverPlaywright._get_screenshot(
|
||||
page, element, element_name
|
||||
)
|
||||
|
||||
@@ -2250,7 +2250,7 @@ def test_catalogs_with_oauth2(
|
||||
security_manager.get_catalogs_accessible_by_user.return_value = {"db2"}
|
||||
|
||||
response = client.get("/api/v1/database/1/catalogs/")
|
||||
assert response.status_code == 500
|
||||
assert response.status_code == 403
|
||||
assert response.json == {
|
||||
"errors": [
|
||||
{
|
||||
@@ -2351,7 +2351,7 @@ def test_schemas_with_oauth2(
|
||||
security_manager.get_schemas_accessible_by_user.return_value = {"schema2"}
|
||||
|
||||
response = client.get("/api/v1/database/1/schemas/")
|
||||
assert response.status_code == 500
|
||||
assert response.status_code == 403
|
||||
assert response.json == {
|
||||
"errors": [
|
||||
{
|
||||
|
||||
@@ -19,6 +19,47 @@
|
||||
Tests for preview_utils query context column building.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
|
||||
from superset.mcp_service.chart import preview_utils
|
||||
|
||||
|
||||
def _imports_chart_data_command(node: ast.Import | ast.ImportFrom) -> bool:
|
||||
blocked_module = "superset.commands.chart.data.get_data_command"
|
||||
|
||||
if isinstance(node, ast.Import):
|
||||
return any(
|
||||
alias.name == blocked_module or alias.name.startswith(f"{blocked_module}.")
|
||||
for alias in node.names
|
||||
)
|
||||
|
||||
module = node.module or ""
|
||||
return (
|
||||
module == blocked_module
|
||||
or module.startswith(f"{blocked_module}.")
|
||||
or (
|
||||
module == "superset.commands.chart.data"
|
||||
and any(alias.name == "get_data_command" for alias in node.names)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_preview_utils_does_not_top_level_import_chart_data_command():
|
||||
"""preview_utils constants should stay safe to import before app setup."""
|
||||
source_path = inspect.getsourcefile(preview_utils) or preview_utils.__file__
|
||||
source = Path(source_path).read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
top_level_imports = [
|
||||
node for node in tree.body if isinstance(node, (ast.Import, ast.ImportFrom))
|
||||
]
|
||||
|
||||
assert preview_utils.SUPPORTED_FORM_DATA_PREVIEW_FORMATS == frozenset(
|
||||
{"ascii", "table", "vega_lite"}
|
||||
)
|
||||
assert not any(_imports_chart_data_command(node) for node in top_level_imports)
|
||||
|
||||
|
||||
class TestPreviewUtilsColumnBuilding:
|
||||
"""Tests for x_axis + groupby column building in generate_preview_from_form_data.
|
||||
|
||||
@@ -1007,12 +1007,12 @@ class TestChartDataCommandValidation:
|
||||
mock_dataset = MagicMock()
|
||||
mock_dataset.id = 10
|
||||
|
||||
# ChartDataCommand is module-level import in preview_utils;
|
||||
# db and QueryContextFactory are local imports inside the function.
|
||||
# ChartDataCommand, db, and QueryContextFactory are local imports inside
|
||||
# the function so preview_utils stays safe to import before app setup.
|
||||
with (
|
||||
patch("superset.extensions.db") as mock_db,
|
||||
patch(
|
||||
"superset.mcp_service.chart.preview_utils.ChartDataCommand",
|
||||
"superset.commands.chart.data.get_data_command.ChartDataCommand",
|
||||
return_value=mock_command,
|
||||
),
|
||||
patch(
|
||||
@@ -1061,7 +1061,7 @@ class TestChartDataCommandValidation:
|
||||
with (
|
||||
patch("superset.extensions.db") as mock_db,
|
||||
patch(
|
||||
"superset.mcp_service.chart.preview_utils.ChartDataCommand",
|
||||
"superset.commands.chart.data.get_data_command.ChartDataCommand",
|
||||
return_value=mock_command,
|
||||
),
|
||||
patch(
|
||||
|
||||
@@ -320,3 +320,54 @@ class TestTakeTiledScreenshot:
|
||||
# Each wait should use the scroll settle timeout constant
|
||||
for call in mock_page.wait_for_timeout.call_args_list:
|
||||
assert call[0][0] == SCROLL_SETTLE_TIMEOUT_MS
|
||||
|
||||
def test_per_tile_spinner_wait_uses_viewport_check(self, mock_page):
|
||||
"""wait_for_function polls viewport-visible spinners after each scroll."""
|
||||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||||
take_tiled_screenshot(
|
||||
mock_page, "dashboard", tile_height=2000, load_wait=30
|
||||
)
|
||||
|
||||
# 3 tiles → 3 wait_for_function calls, one per tile
|
||||
assert mock_page.wait_for_function.call_count == 3
|
||||
|
||||
# Each call uses viewport-scoped JS and the load_wait timeout
|
||||
for call in mock_page.wait_for_function.call_args_list:
|
||||
js = call[0][0]
|
||||
assert "getBoundingClientRect" in js
|
||||
assert "window.innerHeight" in js
|
||||
assert call[1]["timeout"] == 30 * 1000
|
||||
|
||||
def test_per_tile_spinner_timeout_logs_warning_and_continues(self, mock_page):
|
||||
"""A per-tile spinner timeout logs a warning but still takes the screenshot."""
|
||||
from superset.utils.screenshot_utils import PlaywrightTimeout
|
||||
|
||||
timeout = PlaywrightTimeout()
|
||||
mock_page.wait_for_function.side_effect = timeout
|
||||
|
||||
with patch("superset.utils.screenshot_utils.logger") as mock_logger:
|
||||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||||
result = take_tiled_screenshot(
|
||||
mock_page, "dashboard", tile_height=2000, load_wait=30
|
||||
)
|
||||
|
||||
# Screenshot should still proceed (non-fatal)
|
||||
assert result is not None
|
||||
# Warning logged for each tile that timed out
|
||||
assert mock_logger.warning.call_count == 3
|
||||
mock_logger.warning.assert_any_call(
|
||||
"Timed out waiting for visible spinners to clear on tile %s/%s "
|
||||
"(load_wait=%ss)",
|
||||
1,
|
||||
3,
|
||||
30,
|
||||
)
|
||||
|
||||
def test_load_wait_default_is_sixty_seconds(self):
|
||||
"""load_wait defaults to 60 to match SCREENSHOT_LOAD_WAIT config default."""
|
||||
import inspect
|
||||
|
||||
from superset.utils.screenshot_utils import take_tiled_screenshot
|
||||
|
||||
sig = inspect.signature(take_tiled_screenshot)
|
||||
assert sig.parameters["load_wait"].default == 60
|
||||
|
||||
@@ -744,8 +744,9 @@ class TestWebDriverPlaywrightErrorHandling:
|
||||
|
||||
assert exc_info.value is timeout
|
||||
mock_logger.warning.assert_any_call(
|
||||
"Timed out waiting for charts to load at url %s",
|
||||
"Timed out waiting for charts to load at url %s (SCREENSHOT_LOAD_WAIT=%ss)",
|
||||
"http://example.com",
|
||||
60,
|
||||
)
|
||||
|
||||
@patch("superset.utils.webdriver.PLAYWRIGHT_AVAILABLE", True)
|
||||
|
||||
Reference in New Issue
Block a user