Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: CI

on:
push:
branches: [main]
pull_request:
# Lets CI be run by hand on any branch, from the Actions tab.
workflow_dispatch:

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
ci:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

# package.json has no packageManager field yet, so pnpm's version is pinned
# here. Once the field lands, drop the version input — pnpm/action-setup
# reads the version out of package.json on its own.
- uses: pnpm/action-setup@v4
with:
version: 10.11.0

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile

- name: Lint
run: pnpm lint

- name: Typecheck
run: pnpm check

# Everything below belongs before the unit tests: the `client` project in
# vite.config.ts runs them in Chromium, through vitest's browser mode.
- name: Resolve Playwright version
id: playwright
run: |
version=$(node -p "require('@playwright/test/package.json').version")
echo "version=$version" >> "$GITHUB_OUTPUT"

- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright.outputs.version }}

# The browser binaries are cached, the OS-level libraries they need are not,
# so a cache hit still has to run install-deps.
- name: Install Chromium
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps chromium

- name: Install Chromium system dependencies
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps chromium

- name: Unit tests
run: pnpm test:unit -- --run

# No `pnpm test:e2e` step yet: the repository has no *.e2e.ts files, and
# playwright test exits 1 with "No tests found". Bring the step back with the
# first real e2e test (task 16, 16-test-coverage.md) — the script installs
# chromium itself and starts the preview server on port 4173.

# vite build + svelte-package + publint.
- name: Build package
run: pnpm build
104 changes: 104 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Publishing to npm is manual ONLY, through the Run workflow button.
# No push, tag or release triggers: publishing needs external sign-off, so
# nothing here may publish on its own.
#
# Before the first real run (dry_run = false):
# 1. add an NPM_TOKEN secret under Settings → Secrets and variables → Actions
# (an automation token with publish rights);
# 2. make sure permission to publish has been given.
#
# Every release starts with a version bump in package.json and an entry in
# CHANGELOG.md: npm versions are immutable, a number cannot be published twice.
# The "Verify version is not published yet" step checks that before the build —
# including on a dry run, where `npm publish --dry-run` never asks the registry.
#
# The Run workflow button only appears once this file is on the main branch.

name: Release

on:
workflow_dispatch:
inputs:
dry_run:
description: 'Rehearse without publishing: build the package and print the tarball contents'
type: boolean
default: true

jobs:
release:
runs-on: ubuntu-latest

permissions:
# npm needs id-token to generate provenance.
id-token: write
contents: read

steps:
- uses: actions/checkout@v4

# pnpm's version is pinned — see the comment in ci.yml.
- uses: pnpm/action-setup@v4
with:
version: 10.11.0

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
registry-url: 'https://registry.npmjs.org'

- run: pnpm install --frozen-lockfile

# Ahead of the build and the tests, so that a taken version number fails in
# seconds instead of after several minutes of work.
- name: Verify version is not published yet
run: |
name=$(node -p "require('./package.json').name")
version=$(node -p "require('./package.json').version")
echo "Preparing to publish $name@$version"

set +e
output=$(npm view "$name@$version" version 2>&1)
status=$?
set -e

if [ $status -eq 0 ]; then
echo "::error::$name@$version is already published — bump the version in package.json"
exit 1
fi
if ! echo "$output" | grep -q "E404"; then
echo "::error::Could not check the version against the registry"
echo "$output"
exit 1
fi
echo "Version $version is free"

# The same checks ci.yml runs: workflow_dispatch can be launched from any
# branch, so a green CI on main proves nothing about what is being published.
- name: Lint
run: pnpm lint

- name: Typecheck
run: pnpm check

# The `client` project in vite.config.ts runs its tests in Chromium.
- name: Install Chromium for vitest browser mode
run: pnpm exec playwright install --with-deps chromium

- name: Unit tests
run: pnpm test:unit -- --run

- name: Build package
run: pnpm build

# A dry run never exercises provenance — the attestation is only created on a
# real upload — so the flag belongs on the publishing step alone.
- name: Publish (dry run)
if: inputs.dry_run
run: npm publish --dry-run --access public

- name: Publish to npm
if: ${{ !inputs.dry_run }}
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ from a cold start in ~90ms, in ~45 KB min.
`svelte-uplot` is the ~3 KB layer that makes it feel like a Svelte component:
declarative, reactive, autosizing, SSR-safe — with **zero** abstraction over uPlot's own API.

[![CI](https://github.com/twister55/svelte-uplot/actions/workflows/ci.yml/badge.svg)](https://github.com/twister55/svelte-uplot/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/svelte-uplot?logo=npm&color=cb3837)](https://www.npmjs.com/package/svelte-uplot)
[![gzip](https://img.shields.io/badge/runtime-3.1_kB_gzip-brightgreen)](#why-this-one)
[![Svelte 5](https://img.shields.io/badge/Svelte-5%20runes-ff3e00?logo=svelte&logoColor=white)](https://svelte.dev)
Expand Down
22 changes: 18 additions & 4 deletions src/lib/autosize.svelte.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ function chartEl(container: HTMLElement) {
return container.querySelector<HTMLDivElement>('[data-testid="chart"]')!;
}

/** uPlot's own legend — the part of the overhead that rewraps with the width */
function legendHeight(u: uPlot) {
return u.root.querySelector<HTMLElement>('.u-legend')!.offsetHeight;
}

function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
Expand Down Expand Up @@ -162,14 +167,23 @@ describe('autosize', () => {
await laidOut(charts[0]);
await expect.poll(() => charts[0].root.offsetHeight, { timeout: 2000 }).toBe(240);
const oneRow = charts[0].height;
const oneRowLegend = legendHeight(charts[0]);

chartEl(container).style.width = '200px';
await expect.poll(() => charts[0].width, { timeout: 2000 }).toBe(200);

// the legend now needs a row it did not need before, and the whole chart still
// has to fit the 240px it was given
await expect.poll(() => charts[0].root.offsetHeight, { timeout: 2000 }).toBe(240);
expect(charts[0].height).toBeLessThan(oneRow);
// The legend has rewrapped by now — the width is what it wraps to, and the
// convergence passes that follow only move the height. How many rows it takes
// at 200px is a font metric, and a platform whose glyphs are a little wider
// needs more of them, so what the plot owes the legend is measured, not assumed.
const grew = legendHeight(charts[0]) - oneRowLegend;
expect(grew).toBeGreaterThan(0);

// The plot gives up exactly that much, instead of keeping the height it was
// sized to before the rewrap. Clamped at zero: a legend taller than the box
// leaves the plotting area nothing, and the root then outgrows the element —
// the degenerate case the next test starts from.
await expect.poll(() => charts[0].height, { timeout: 2000 }).toBe(Math.max(0, oneRow - grew));
});

it('takes the plot height back once the legend can unwrap again', async () => {
Expand Down
Loading