Skip to content

fix(scroll): keep bottom glued when a reaction grows a mid-viewport row #1148

fix(scroll): keep bottom glued when a reaction grows a mid-viewport row

fix(scroll): keep bottom glued when a reaction grows a mid-viewport row #1148

Workflow file for this run

name: CI
on:
pull_request:
branches: [main, devel]
jobs:
test:
name: Test
runs-on: ubuntu-latest
# Backstop against a hung step (default GitHub timeout is 6h). A healthy run is ~7min.
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build SDK
run: npm run build:sdk
- name: Run tests
run: npm test
- name: Type check
run: npm run typecheck
- name: Lint SDK
run: npm run lint --workspace=@fluux/sdk
- name: Lint App
run: npm run lint --workspace=@xmpp/fluux
- name: Check for unstable Zustand selectors
run: |
# Zustand selectors passed to useStore/useSyncExternalStore MUST return
# referentially stable values. If a selector creates a new object, array,
# Set, or closure on every call, Object.is() comparison fails and React
# enters an infinite re-render loop (error #185: Maximum update depth
# exceeded).
#
# Dangerous patterns detected by this check:
#
# 1. Closure-returning selectors — a new function is created on every call:
# useRoomStore((s) => (jid) => s.rooms.has(jid)) // BAD
# Fix: use imperative store access instead:
# useCallback((jid) => roomStore.getState().rooms.has(jid), [])
#
# 2. Fallback with || [] — creates a new empty array each time:
# useIgnoreStore((s) => s.ignoredUsers[jid] || []) // BAD
# Fix: use a module-level stable reference:
# const EMPTY: T[] = []
# useIgnoreStore((s) => s.ignoredUsers[jid] ?? EMPTY)
#
# 3. Fallback with new Set()/new Map() — same issue with non-primitives:
# return get().votedPollIds.get(jid) ?? new Set() // BAD
# Fix: const EMPTY_SET = new Set<string>()
# return get().votedPollIds.get(jid) ?? EMPTY_SET
ERRORS=0
# Pattern 1: Closure-returning selectors (arrow function inside selector)
if grep -rn --include='*.ts' --include='*.tsx' \
-E 'use\w+Store\(\(s\) => \(' \
packages/ apps/ \
| grep -v 'node_modules' | grep -v '.test.' | grep -v '// '; then
echo "::error::Found closure-returning Zustand selector(s). These create a new function on every store update, causing infinite re-render loops."
ERRORS=1
fi
# Pattern 2: || [] fallback in store selectors
if grep -rn --include='*.ts' --include='*.tsx' \
-E 'use\w+Store\(\(s\) =>.*\|\| \[\]' \
packages/ apps/ \
| grep -v 'node_modules' | grep -v '.test.'; then
echo "::error::Found || [] fallback in Zustand selector(s). Use a stable empty reference (const EMPTY = []) with ?? instead."
ERRORS=1
fi
# Pattern 3: ?? new Set/Map/Array in store getters used as selectors
if grep -rn --include='*.ts' --include='*.tsx' \
-E '\?\? new (Set|Map|Array)\(\)' \
packages/fluux-sdk/src/stores/ \
| grep -v 'node_modules' | grep -v '.test.'; then
echo "::error::Found ?? new Set/Map/Array() in store code. Use a module-level EMPTY constant instead."
ERRORS=1
fi
if [ "$ERRORS" -ne 0 ]; then
exit 1
fi
echo "No unstable Zustand selector patterns found."
e2e-scroll:
name: Scroll invariants (e2e)
runs-on: ubuntu-latest
# Backstop against a hung dev server / browser install (default GitHub timeout is 6h).
# A healthy run is ~7min; the Playwright OS-dep apt install can be slow on an unlucky
# mirror, so this is set generously — it only fires on a genuine hang.
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm ci
# The Playwright webServer runs `npm run dev` (vite), which imports @fluux/sdk.
# The dev build is required because the harness relies on import.meta.env.DEV
# test seams (__fluuxGetVirtOffset / __fluuxTriggerLoadOlder) that are stripped
# from production builds.
- name: Build SDK
run: npm run build:sdk
# Cache the browser binaries (~/.cache/ms-playwright) so a slow CDN can't add minutes to
# the download. Caches created in pull_request runs are scoped to that PR, so the shared
# copy is seeded from main by playwright-cache.yml; restore-keys lets a Playwright bump
# start from the previous cache and download only the changed browsers.
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install chromium webkit
# The OS-level deps (apt) live on the ephemeral runner, so `install-deps` must run every
# time — but the azure apt mirror can be very slow at certain times of day, so the
# downloaded .deb files are cached and pre-seeded into apt's archive dir. apt validates
# cached debs by hash and re-downloads only what changed, so a stale cache degrades
# gracefully. Keyed on the runner image version (package versions roll with it, ~weekly);
# restore-only here — the shared cache is seeded from main by playwright-cache.yml.
- name: Compute apt package cache key
id: apt-cache-key
run: echo "key=apt-debs-$ImageOS-$ImageVersion" >> "$GITHUB_OUTPUT"
- name: Restore apt package cache
uses: actions/cache/restore@v6
with:
path: ~/.apt-debs
key: ${{ steps.apt-cache-key.outputs.key }}
restore-keys: |
apt-debs-
- name: Seed apt archives from cache
run: |
if [ -d "$HOME/.apt-debs" ]; then
sudo cp "$HOME"/.apt-debs/*.deb /var/cache/apt/archives/ 2>/dev/null || true
echo "Seeded $(ls "$HOME"/.apt-debs/*.deb 2>/dev/null | wc -l) cached .deb file(s)"
fi
- name: Install Playwright OS dependencies
run: npx playwright install-deps chromium webkit
# CI=true is set by GitHub Actions → the config enables retries(2), forbidOnly,
# and the github + html reporters. Blocks the PR if any invariant regresses.
- name: Run scroll invariants (chromium + webkit)
run: npm run test:scroll
- name: Upload Playwright report on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: playwright-scroll-report
path: playwright-report/
retention-days: 7
rust:
name: Rust
runs-on: ubuntu-latest
# Backstop against a hung build/test (default GitHub timeout is 6h). A cached run is ~5min.
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf \
libxss-dev \
libdbus-1-dev
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: apps/fluux/src-tauri
- name: Run tests
working-directory: apps/fluux/src-tauri
run: cargo test --locked
- name: Clippy
working-directory: apps/fluux/src-tauri
run: cargo clippy --locked -- -D warnings
- name: Check generated files are up to date
run: git diff --exit-code apps/fluux/src-tauri/gen/