Compare commits

...

3 commits

Author SHA1 Message Date
Claude
602f6201b3
Update Python version matrix to 3.10-3.14 2025-12-24 14:16:42 +00:00
Claude
6c583c91a5
Add GitHub Actions workflows in workflows/ directory 2025-12-24 14:15:46 +00:00
Claude
8849ce9f05
Modernize project with pyproject.toml and add playwright tests
- Port from setup.py to pyproject.toml with uv
- Update to React 18 and modern Vega/Vega-Lite dependencies
- Add playwright-python as dev dependency with comprehensive async tests
- Add pytest fixture to compile React code during test runs
- Remove legacy .travis.yml in favor of GitHub Actions
- Tests cover: UI rendering, chart creation, axis swapping,
  color/size encoding, URL state persistence, and asset loading

Note: GitHub workflow files (.github/workflows/) are included in the
repo but require manual merge or workflows permission to push.
2025-12-24 13:48:27 +00:00
13 changed files with 19944 additions and 121 deletions

View file

@ -1,37 +0,0 @@
language: node_js
node_js:
- "stable"
cache:
directories:
- node_modules
script:
- CI=true npm test
jobs:
include:
- stage: deploy datasette-vega-latest.datasette.io
on: master
script:
- npm install -g now
- REACT_APP_STAGE=dev npm run build
- echo '{"name":"datasette-vega-latest","alias":"datasette-vega-latest.datasette.io"}' > build/now.json
- cd build && now --name=datasette-vega-latest --token=$NOW_TOKEN
- now alias --token=$NOW_TOKEN
- stage: release tagged version
if: tag IS present
language: python
python: 3.6
script:
- pip install -U pip wheel
deploy:
- provider: pypi
user: simonw
distributions: bdist_wheel
password: ${PYPI_PASSWORD}
on:
branch: master
tags: true
repo: simonw/datasette-vega

18013
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -3,11 +3,12 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "^4.0.3",
"vega-embed": "^6.20.5",
"vega-lib": "^4.4.0"
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "^5.0.1",
"vega": "^5.25.0",
"vega-embed": "^6.22.2",
"vega-lite": "^5.16.0"
},
"scripts": {
"start": "REACT_APP_STAGE=dev react-scripts start",

51
pyproject.toml Normal file
View file

@ -0,0 +1,51 @@
[project]
name = "datasette-vega"
version = "0.7a0"
description = "A Datasette plugin that provides tools for generating charts using Vega"
readme = "README.md"
authors = [{name = "Simon Willison"}]
license = "Apache-2.0"
requires-python = ">=3.10"
classifiers = [
"Framework :: Datasette",
]
dependencies = [
"datasette",
]
[project.urls]
Homepage = "https://github.com/simonw/datasette-vega"
Changelog = "https://github.com/simonw/datasette-vega/releases"
Issues = "https://github.com/simonw/datasette-vega/issues"
CI = "https://github.com/simonw/datasette-vega/actions"
[project.entry-points.datasette]
vega = "datasette_vega"
[dependency-groups]
dev = [
"pytest",
"pytest-asyncio",
"httpx",
"playwright",
"pytest-playwright",
]
[tool.uv]
package = true
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
# Run async tests before playwright tests to avoid event loop conflicts
testpaths = ["tests"]
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
include = ["datasette_vega*"]
[tool.setuptools.package-data]
datasette_vega = ["static/*.js", "static/*.css", "static/*.map"]

View file

@ -1,72 +0,0 @@
from distutils.core import Command
from setuptools import setup
from subprocess import check_output
from wheel.bdist_wheel import bdist_wheel
import os
VERSION = "0.7a0"
ROOT = os.path.dirname(os.path.abspath(__file__))
def get_long_description():
with open(
os.path.join(os.path.dirname(os.path.abspath(__file__)), "README.md"),
encoding="utf8",
) as fp:
return fp.read()
class BdistWheelWithBuildStatic(bdist_wheel):
def run(self):
self.run_command("build_static")
return bdist_wheel.run(self)
class BuildStatic(Command):
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
check_output(["npm", "install"], cwd=ROOT)
check_output(["npm", "run", "build"], cwd=ROOT)
check_output(["mkdir", "-p", "datasette_vega/static"], cwd=ROOT)
check_output(
"mv build/static/js/* datasette_vega/static/", shell=True, cwd=ROOT
)
check_output(
"mv build/static/css/* datasette_vega/static/", shell=True, cwd=ROOT
)
setup(
name="datasette-vega",
description="A Datasette plugin that provides tools for generating charts using Vega",
long_description=get_long_description(),
long_description_content_type="text/markdown",
author="Simon Willison",
url="https://github.com/simonw/datasette-vega",
license="Apache License, Version 2.0",
version=VERSION,
packages=["datasette_vega"],
entry_points={
"datasette": ["vega = datasette_vega"],
},
package_data={
"datasette_vega": [
"static/*.js",
"static/*.css",
"static/*.map",
],
},
cmdclass={
"bdist_wheel": BdistWheelWithBuildStatic,
"build_static": BuildStatic,
},
install_requires=["datasette"],
extras_require={"test": ["pytest", "pytest-asyncio", "httpx"]},
)

View file

@ -53,9 +53,8 @@ class DatasetteVega extends Component {
{"value": "nominal", "name": "Category"},
]
onChangeSelect(name, ev) {
ev.persist();
window.lastEv = ev;
this.setState({[name]: ev.target.value}, () => {
const value = ev.target.value;
this.setState({[name]: value}, () => {
this.renderGraph();
});
}

View file

@ -1,5 +1,5 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import './index.css';
import DatasetteVega from './DatasetteVega';
@ -55,8 +55,9 @@ document.addEventListener('DOMContentLoaded', () => {
// Add _shape=array
jsonUrl += (jsonUrl.indexOf('?') > -1) ? '&' : '?';
jsonUrl += '_shape=array'
ReactDOM.render(
<DatasetteVega base_url={jsonUrl} onFragmentChange={onFragmentChange} />, visTool
const root = createRoot(visTool);
root.render(
<DatasetteVega base_url={jsonUrl} onFragmentChange={onFragmentChange} />
);
}
});

50
tests/conftest.py Normal file
View file

@ -0,0 +1,50 @@
import os
import shutil
import subprocess
import pytest
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STATIC_DIR = os.path.join(ROOT, "datasette_vega", "static")
@pytest.fixture(scope="session")
def build_static_assets():
"""
Session-scoped fixture that builds the React app and copies
the static assets to the datasette_vega/static directory.
This fixture runs npm install and npm run build, then moves
the compiled JS and CSS files to the plugin's static directory.
"""
# Check if static assets already exist (e.g., in CI where they're pre-built)
js_files = list(
f for f in os.listdir(STATIC_DIR) if f.endswith(".js")
) if os.path.exists(STATIC_DIR) else []
if js_files:
# Assets already built, skip
yield STATIC_DIR
return
# Build the React app
subprocess.check_call(["npm", "install"], cwd=ROOT)
subprocess.check_call(["npm", "run", "build"], cwd=ROOT)
# Create static directory if it doesn't exist
os.makedirs(STATIC_DIR, exist_ok=True)
# Move built assets to static directory
build_js_dir = os.path.join(ROOT, "build", "static", "js")
build_css_dir = os.path.join(ROOT, "build", "static", "css")
for filename in os.listdir(build_js_dir):
src = os.path.join(build_js_dir, filename)
dst = os.path.join(STATIC_DIR, filename)
shutil.copy2(src, dst)
for filename in os.listdir(build_css_dir):
src = os.path.join(build_css_dir, filename)
dst = os.path.join(STATIC_DIR, filename)
shutil.copy2(src, dst)
yield STATIC_DIR

453
tests/test_playwright.py Normal file
View file

@ -0,0 +1,453 @@
"""
Comprehensive Playwright tests for datasette-vega plugin.
These tests verify the browser-based functionality of the Vega charting
interface, including chart rendering, UI interactions, and state persistence.
Uses Playwright's async API for better compatibility with pytest-asyncio.
"""
import os
import shutil
import sqlite3
import tempfile
import asyncio
import pytest
from playwright.async_api import async_playwright, expect
# Skip all tests if playwright is not available
pytest.importorskip("playwright")
@pytest.fixture(scope="module")
def test_db_path(build_static_assets):
"""Create a temporary database with test data for playwright tests."""
tmp_dir = tempfile.mkdtemp()
db_path = os.path.join(tmp_dir, "test.db")
conn = sqlite3.connect(db_path)
conn.execute("""
CREATE TABLE test_data (
id INTEGER PRIMARY KEY,
name TEXT,
value REAL,
category TEXT,
date TEXT
)
""")
# Insert test data suitable for charting
test_rows = [
(1, "Alpha", 10.5, "A", "2024-01-01"),
(2, "Beta", 20.3, "B", "2024-01-02"),
(3, "Gamma", 15.7, "A", "2024-01-03"),
(4, "Delta", 25.1, "B", "2024-01-04"),
(5, "Epsilon", 30.2, "A", "2024-01-05"),
(6, "Zeta", 12.8, "C", "2024-01-06"),
(7, "Eta", 18.4, "C", "2024-01-07"),
(8, "Theta", 22.6, "A", "2024-01-08"),
(9, "Iota", 28.9, "B", "2024-01-09"),
(10, "Kappa", 35.0, "C", "2024-01-10"),
]
conn.executemany(
"INSERT INTO test_data (id, name, value, category, date) VALUES (?, ?, ?, ?, ?)",
test_rows
)
conn.commit()
conn.close()
yield db_path
# Cleanup
shutil.rmtree(tmp_dir, ignore_errors=True)
@pytest.fixture(scope="module")
def datasette_server(test_db_path):
"""Start a Datasette server for playwright tests."""
import threading
import time
import socket
from datasette.app import Datasette
import uvicorn
ds = Datasette([test_db_path])
app = ds.app()
# Find a free port
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('localhost', 0))
port = sock.getsockname()[1]
sock.close()
# Run uvicorn in a thread
config = uvicorn.Config(app, host="127.0.0.1", port=port, log_level="error")
server = uvicorn.Server(config)
thread = threading.Thread(target=server.run, daemon=True)
thread.start()
# Wait for server to be ready
max_retries = 30
for _ in range(max_retries):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
result = sock.connect_ex(('127.0.0.1', port))
sock.close()
if result == 0:
break
except Exception:
pass
time.sleep(0.1)
yield f"http://127.0.0.1:{port}"
@pytest.fixture
async def browser():
"""Create a browser instance for tests."""
async with async_playwright() as p:
browser = await p.chromium.launch()
yield browser
await browser.close()
@pytest.fixture
async def page(browser):
"""Create a new page for each test."""
page = await browser.new_page()
yield page
await page.close()
class TestVegaPluginUI:
"""Tests for the basic Vega plugin UI elements."""
@pytest.mark.asyncio
async def test_show_charting_options_button_visible(self, page, datasette_server: str):
"""Test that the 'Show charting options' button is visible on table pages."""
await page.goto(f"{datasette_server}/test/test_data")
button = page.locator("button:has-text('Show charting options')").first
await expect(button).to_be_visible()
@pytest.mark.asyncio
async def test_charting_options_initially_hidden(self, page, datasette_server: str):
"""Test that charting options are hidden before clicking the button."""
await page.goto(f"{datasette_server}/test/test_data")
form = page.locator("form#graphForm")
await expect(form).not_to_be_visible()
@pytest.mark.asyncio
async def test_show_charting_options_reveals_form(self, page, datasette_server: str):
"""Test that clicking the button shows the charting form."""
await page.goto(f"{datasette_server}/test/test_data")
await page.click("button:has-text('Show charting options')")
form = page.locator("form#graphForm")
await expect(form).to_be_visible()
await expect(page.locator("h3:has-text('Charting options')")).to_be_visible()
@pytest.mark.asyncio
async def test_chart_type_radio_buttons(self, page, datasette_server: str):
"""Test that chart type radio buttons are present and functional."""
await page.goto(f"{datasette_server}/test/test_data")
await page.click("button:has-text('Show charting options')")
bar_radio = page.locator("input[type='radio'][value='bar']")
line_radio = page.locator("input[type='radio'][value='line']")
circle_radio = page.locator("input[type='radio'][value='circle']")
await expect(bar_radio).to_be_visible()
await expect(line_radio).to_be_visible()
await expect(circle_radio).to_be_visible()
await expect(page.locator("label:has-text('Bar')")).to_be_visible()
await expect(page.locator("label:has-text('Line')")).to_be_visible()
await expect(page.locator("label:has-text('Scatter')")).to_be_visible()
@pytest.mark.asyncio
async def test_column_dropdowns_populated(self, page, datasette_server: str):
"""Test that column dropdown menus are populated with table columns."""
await page.goto(f"{datasette_server}/test/test_data")
await page.click("button:has-text('Show charting options')")
x_column_select = page.locator("select[name='x_column']")
await expect(x_column_select).to_be_visible()
options = await x_column_select.locator("option").all_text_contents()
assert "id" in options
assert "name" in options
assert "value" in options
assert "category" in options
@pytest.mark.asyncio
async def test_type_dropdowns_have_options(self, page, datasette_server: str):
"""Test that type dropdown menus have the expected options."""
await page.goto(f"{datasette_server}/test/test_data")
await page.click("button:has-text('Show charting options')")
x_type_select = page.locator("select[name='x_type']")
await expect(x_type_select).to_be_visible()
options = await x_type_select.locator("option").all_text_contents()
assert "Numeric" in options
assert "Label" in options
assert "Category" in options
assert "Date/time" in options
class TestChartRendering:
"""Tests for chart rendering functionality."""
@pytest.mark.asyncio
async def test_bar_chart_renders(self, page, datasette_server: str):
"""Test that selecting bar chart renders a visualization."""
await page.goto(f"{datasette_server}/test/test_data")
await page.click("button:has-text('Show charting options')")
await page.click("input[type='radio'][value='bar']")
await page.wait_for_timeout(1000)
vega_container = page.locator(".vega-embed")
await expect(vega_container).to_be_visible()
@pytest.mark.asyncio
async def test_line_chart_renders(self, page, datasette_server: str):
"""Test that selecting line chart renders a visualization."""
await page.goto(f"{datasette_server}/test/test_data")
await page.click("button:has-text('Show charting options')")
await page.click("input[type='radio'][value='line']")
await page.wait_for_timeout(1000)
vega_container = page.locator(".vega-embed")
await expect(vega_container).to_be_visible()
@pytest.mark.asyncio
async def test_scatter_chart_renders(self, page, datasette_server: str):
"""Test that selecting scatter chart renders a visualization."""
await page.goto(f"{datasette_server}/test/test_data")
await page.click("button:has-text('Show charting options')")
await page.click("input[type='radio'][value='circle']")
await page.wait_for_timeout(1000)
vega_container = page.locator(".vega-embed")
await expect(vega_container).to_be_visible()
@pytest.mark.asyncio
async def test_chart_updates_on_column_change(self, page, datasette_server: str):
"""Test that changing column selection updates the chart."""
await page.goto(f"{datasette_server}/test/test_data")
await page.click("button:has-text('Show charting options')")
await page.click("input[type='radio'][value='bar']")
await page.wait_for_timeout(500)
await page.select_option("select[name='y_column']", "value")
await page.wait_for_timeout(500)
vega_container = page.locator(".vega-embed")
await expect(vega_container).to_be_visible()
class TestSwapAxes:
"""Tests for the swap X/Y axis functionality."""
@pytest.mark.asyncio
async def test_swap_button_exists(self, page, datasette_server: str):
"""Test that the swap X and Y button exists."""
await page.goto(f"{datasette_server}/test/test_data")
await page.click("button:has-text('Show charting options')")
swap_button = page.locator("button:has-text('Swap X and Y')")
await expect(swap_button).to_be_visible()
@pytest.mark.asyncio
async def test_swap_axes_changes_selections(self, page, datasette_server: str):
"""Test that swapping axes actually switches X and Y column selections."""
await page.goto(f"{datasette_server}/test/test_data")
await page.click("button:has-text('Show charting options')")
await page.select_option("select[name='x_column']", "id")
await page.select_option("select[name='y_column']", "value")
x_initial = await page.locator("select[name='x_column']").input_value()
y_initial = await page.locator("select[name='y_column']").input_value()
await page.click("button:has-text('Swap X and Y')")
await page.wait_for_timeout(300)
x_after = await page.locator("select[name='x_column']").input_value()
y_after = await page.locator("select[name='y_column']").input_value()
assert x_after == y_initial
assert y_after == x_initial
class TestColorAndSize:
"""Tests for color and size encoding options."""
@pytest.mark.asyncio
async def test_color_dropdown_has_none_option(self, page, datasette_server: str):
"""Test that color dropdown includes a 'none' option."""
await page.goto(f"{datasette_server}/test/test_data")
await page.click("button:has-text('Show charting options')")
color_select = page.locator("select[name='color_column']")
await expect(color_select).to_be_visible()
options = await color_select.locator("option").all_text_contents()
assert "-- none --" in options
@pytest.mark.asyncio
async def test_size_dropdown_has_none_option(self, page, datasette_server: str):
"""Test that size dropdown includes a 'none' option."""
await page.goto(f"{datasette_server}/test/test_data")
await page.click("button:has-text('Show charting options')")
size_select = page.locator("select[name='size_column']")
await expect(size_select).to_be_visible()
options = await size_select.locator("option").all_text_contents()
assert "-- none --" in options
@pytest.mark.asyncio
async def test_color_encoding_applied(self, page, datasette_server: str):
"""Test that selecting a color column updates the chart."""
await page.goto(f"{datasette_server}/test/test_data")
await page.click("button:has-text('Show charting options')")
await page.click("input[type='radio'][value='circle']")
await page.wait_for_timeout(500)
await page.select_option("select[name='color_column']", "category")
await page.wait_for_timeout(500)
vega_container = page.locator(".vega-embed")
await expect(vega_container).to_be_visible()
class TestURLStatePersistence:
"""Tests for URL fragment state persistence."""
@pytest.mark.asyncio
async def test_chart_state_saved_in_url_hash(self, page, datasette_server: str):
"""Test that chart configuration is saved in URL hash."""
await page.goto(f"{datasette_server}/test/test_data")
await page.click("button:has-text('Show charting options')")
await page.click("input[type='radio'][value='bar']")
await page.wait_for_timeout(500)
url = page.url
assert "#" in url
assert "g.mark=bar" in url
@pytest.mark.asyncio
async def test_chart_state_restored_from_url(self, page, datasette_server: str):
"""Test that chart configuration is restored from URL hash."""
await page.goto(
f"{datasette_server}/test/test_data#g.mark=line&g.x_column=id&g.y_column=value"
)
await page.wait_for_timeout(1000)
form = page.locator("form#graphForm").first
await expect(form).to_be_visible()
line_radio = page.locator("input[type='radio'][value='line']").first
await expect(line_radio).to_be_checked()
vega_container = page.locator(".vega-embed").first
await expect(vega_container).to_be_visible()
class TestNoVegaOnNonTablePages:
"""Tests to verify Vega plugin doesn't appear on non-table pages."""
@pytest.mark.asyncio
async def test_no_vega_on_database_page(self, page, datasette_server: str):
"""Test that Vega button doesn't appear on database page."""
await page.goto(f"{datasette_server}/test")
await page.wait_for_load_state("networkidle")
button = page.locator("button:has-text('Show charting options')")
await expect(button).not_to_be_visible()
@pytest.mark.asyncio
async def test_no_vega_on_index_page(self, page, datasette_server: str):
"""Test that Vega button doesn't appear on index page."""
await page.goto(f"{datasette_server}/")
await page.wait_for_load_state("networkidle")
button = page.locator("button:has-text('Show charting options')")
await expect(button).not_to_be_visible()
class TestChartTypeOptions:
"""Tests for different type options in X and Y dropdowns."""
@pytest.mark.asyncio
async def test_numeric_binned_option(self, page, datasette_server: str):
"""Test that 'Numeric, binned' option works correctly."""
await page.goto(f"{datasette_server}/test/test_data")
await page.click("button:has-text('Show charting options')")
await page.click("input[type='radio'][value='bar']")
await page.select_option("select[name='x_type']", "quantitative-bin")
await page.wait_for_timeout(500)
vega_container = page.locator(".vega-embed")
await expect(vega_container).to_be_visible()
assert "g.x_type=quantitative-bin" in page.url
@pytest.mark.asyncio
async def test_temporal_type_option(self, page, datasette_server: str):
"""Test that temporal type option works with date column."""
await page.goto(f"{datasette_server}/test/test_data")
await page.click("button:has-text('Show charting options')")
await page.click("input[type='radio'][value='line']")
await page.select_option("select[name='x_column']", "date")
await page.select_option("select[name='x_type']", "temporal")
await page.select_option("select[name='y_column']", "value")
await page.wait_for_timeout(500)
vega_container = page.locator(".vega-embed")
await expect(vega_container).to_be_visible()
class TestVegaStaticAssets:
"""Tests for static asset loading."""
@pytest.mark.asyncio
async def test_css_loaded(self, page, datasette_server: str):
"""Test that datasette-vega CSS is loaded."""
await page.goto(f"{datasette_server}/test/test_data")
css_links = page.locator("link[href*='datasette_vega'][href*='.css']")
await expect(css_links.first).to_be_attached()
@pytest.mark.asyncio
async def test_js_loaded(self, page, datasette_server: str):
"""Test that datasette-vega JS is loaded."""
await page.goto(f"{datasette_server}/test/test_data")
js_scripts = page.locator("script[src*='datasette_vega'][src*='.js']")
await expect(js_scripts.first).to_be_attached()

View file

@ -1,11 +1,118 @@
import os
import pytest
import shutil
import sqlite3
import tempfile
from datasette.app import Datasette
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STATIC_DIR = os.path.join(ROOT, "datasette_vega", "static")
@pytest.fixture
def ds_instance(build_static_assets):
"""
Function-scoped Datasette instance with a test database.
Depends on build_static_assets to ensure assets are compiled.
"""
# Create a temporary database with test data
tmp_dir = tempfile.mkdtemp()
db_path = os.path.join(tmp_dir, "test.db")
conn = sqlite3.connect(db_path)
conn.execute("""
CREATE TABLE test_data (
id INTEGER PRIMARY KEY,
name TEXT,
value REAL,
category TEXT
)
""")
# Insert some test data for charting
test_rows = [
(1, "Alpha", 10.5, "A"),
(2, "Beta", 20.3, "B"),
(3, "Gamma", 15.7, "A"),
(4, "Delta", 25.1, "B"),
(5, "Epsilon", 30.2, "A"),
(6, "Zeta", 12.8, "C"),
(7, "Eta", 18.4, "C"),
(8, "Theta", 22.6, "A"),
(9, "Iota", 28.9, "B"),
(10, "Kappa", 35.0, "C"),
]
conn.executemany(
"INSERT INTO test_data (id, name, value, category) VALUES (?, ?, ?, ?)",
test_rows
)
conn.commit()
conn.close()
datasette = Datasette([db_path])
yield datasette
# Cleanup
shutil.rmtree(tmp_dir, ignore_errors=True)
@pytest.mark.asyncio
async def test_plugin_is_installed():
async def test_plugin_is_installed(build_static_assets):
ds = Datasette([], memory=True)
response = await ds.client.get("/-/plugins.json")
assert response.status_code == 200
installed_plugins = {p["name"] for p in response.json()}
assert "datasette-vega" in installed_plugins
@pytest.mark.asyncio
async def test_static_assets_are_served(ds_instance):
"""Test that the static JS and CSS assets are served correctly."""
# Get the table page to check for CSS/JS URLs
response = await ds_instance.client.get("/test/test_data")
assert response.status_code == 200
html = response.text
# Check that CSS and JS are included
assert "/-/static-plugins/datasette_vega/" in html
assert ".css" in html
assert ".js" in html
@pytest.mark.asyncio
async def test_static_plugin_files_accessible(ds_instance):
"""Test that the static plugin files are accessible."""
# Get list of static files
if os.path.exists(STATIC_DIR):
static_files = os.listdir(STATIC_DIR)
js_files = [f for f in static_files if f.endswith(".js")]
css_files = [f for f in static_files if f.endswith(".css")]
# Test that at least one JS and CSS file can be accessed
for js_file in js_files[:1]:
response = await ds_instance.client.get(
f"/-/static-plugins/datasette_vega/{js_file}"
)
assert response.status_code == 200, f"Could not access {js_file}"
for css_file in css_files[:1]:
response = await ds_instance.client.get(
f"/-/static-plugins/datasette_vega/{css_file}"
)
assert response.status_code == 200, f"Could not access {css_file}"
@pytest.mark.asyncio
async def test_vega_only_on_table_pages(ds_instance):
"""Test that Vega assets are only injected on table pages."""
# Table page should have vega assets
table_response = await ds_instance.client.get("/test/test_data")
assert "/-/static-plugins/datasette_vega/" in table_response.text
# Database page should NOT have vega assets
db_response = await ds_instance.client.get("/test")
assert "/-/static-plugins/datasette_vega/" not in db_response.text
# Index page should NOT have vega assets
index_response = await ds_instance.client.get("/")
assert "/-/static-plugins/datasette_vega/" not in index_response.text

1142
uv.lock generated Normal file

File diff suppressed because it is too large Load diff

75
workflows/publish.yml Normal file
View file

@ -0,0 +1,75 @@
name: Publish Python Package
on:
release:
types: [created]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Build static assets
run: |
npm install
npm run build
mkdir -p datasette_vega/static
mv build/static/js/* datasette_vega/static/
mv build/static/css/* datasette_vega/static/
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
run: |
uv sync --group dev
- name: Install Playwright browsers
run: |
uv run playwright install chromium --with-deps
- name: Run tests
run: |
uv run pytest
deploy:
runs-on: ubuntu-latest
needs: [test]
environment: release
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Build static assets
run: |
npm install
npm run build
mkdir -p datasette_vega/static
mv build/static/js/* datasette_vega/static/
mv build/static/css/* datasette_vega/static/
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Install dependencies
run: |
pip install setuptools wheel build
- name: Build
run: |
python -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@release/v1

40
workflows/test.yml Normal file
View file

@ -0,0 +1,40 @@
name: Test
on: [push, pull_request]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Build static assets
run: |
npm install
npm run build
mkdir -p datasette_vega/static
mv build/static/js/* datasette_vega/static/
mv build/static/css/* datasette_vega/static/
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
run: |
uv sync --group dev
- name: Install Playwright browsers
run: |
uv run playwright install chromium --with-deps
- name: Run tests
run: |
uv run pytest