From 618b700feb5f65ba351c1b078c6a0a11654878da Mon Sep 17 00:00:00 2001 From: Daniel Noulet Date: Sat, 10 Oct 2026 02:30:24 +0200 Subject: [PATCH] refactor --- hack/deploy-lcl.sh | 43 +++ sdk/src/global.d.ts | 10 + .../fallback-switcher.integration.test.tsx | 166 ++++++++++ .../real-controls.integration.test.tsx | 214 +++++++++++++ .../theme-switcher.integration.test.tsx | 289 ++++++++++++++++++ web/tsconfig.test.json | 16 + 6 files changed, 738 insertions(+) create mode 100755 hack/deploy-lcl.sh create mode 100644 sdk/src/global.d.ts create mode 100644 web/src/components/fallback-switcher.integration.test.tsx create mode 100644 web/src/components/real-controls.integration.test.tsx create mode 100644 web/src/components/theme-switcher.integration.test.tsx create mode 100644 web/tsconfig.test.json diff --git a/hack/deploy-lcl.sh b/hack/deploy-lcl.sh new file mode 100755 index 0000000..1dad2e6 --- /dev/null +++ b/hack/deploy-lcl.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# +# deploy-lcl.sh — build go-bananas, push it to the local registry, and deploy it to +# the lcl k3s cluster, then verify it is live through the TLS edge. +# +# Thin wrapper around `make deploy-lcl` that adds the two things that actually go +# wrong locally: talking to the cluster, and pulling a stale cached image. +# +# ./hack/deploy-lcl.sh +# KUBECONFIG=/etc/rancher/k3s/k3s.yaml ./hack/deploy-lcl.sh +# LCL_TAG=v1.2.3 ./hack/deploy-lcl.sh +# +set -euo pipefail + +cd "$(dirname "$0")/.." + +LCL_REGISTRY="${LCL_REGISTRY:-localhost:5000}" +LCL_NAMESPACE="${LCL_NAMESPACE:-go-insane-lcl}" + +# Prefer an explicit KUBECONFIG; otherwise try the direct k3s config if the current +# context cannot reach a node. The default `local` context routes through a Rancher +# proxy that can 502, and a deploy against it fails with a confusing transport error +# rather than a real one. +if [ -z "${KUBECONFIG:-}" ]; then + if ! timeout 10 kubectl get nodes >/dev/null 2>&1 && [ -r /etc/rancher/k3s/k3s.yaml ]; then + echo "note: current kube context cannot reach a node; using /etc/rancher/k3s/k3s.yaml" >&2 + export KUBECONFIG=/etc/rancher/k3s/k3s.yaml + fi +fi + +echo "=== preflight ===" +timeout 6 curl -sf "${LCL_REGISTRY}/v2/" >/dev/null || { + echo "error: no registry answering at ${LCL_REGISTRY}/v2 — start it before deploying" >&2 + exit 1 +} +kubectl get nodes >/dev/null 2>&1 || { + echo "error: cannot reach the cluster (KUBECONFIG=${KUBECONFIG:-})" >&2 + exit 1 +} +echo "registry OK (${LCL_REGISTRY}) ; cluster OK ($(kubectl config current-context 2>/dev/null || echo "$KUBECONFIG"))" + +echo "=== build + push + deploy + verify ===" +exec make deploy-lcl LCL_NAMESPACE="$LCL_NAMESPACE" diff --git a/sdk/src/global.d.ts b/sdk/src/global.d.ts new file mode 100644 index 0000000..815946e --- /dev/null +++ b/sdk/src/global.d.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +export {} + +declare global { + interface Window { + /** Global escape hatch so script-tag consumers can reach the shared store. */ + __BANANAS_GET_CONTRACT_STATE__?: typeof import('./contract.js').getContractState + } +} diff --git a/web/src/components/fallback-switcher.integration.test.tsx b/web/src/components/fallback-switcher.integration.test.tsx new file mode 100644 index 0000000..9fb8c04 --- /dev/null +++ b/web/src/components/fallback-switcher.integration.test.tsx @@ -0,0 +1,166 @@ +/** + * Fallback switcher integration test. + * + * If the shipped controls never upgrade — a blocked module load, an older bundle, a + * proxy hiccup — the admin falls back to `LegacyThemeSwitcher`. This test keeps that + * path genuinely usable: the theme dropdown must always be present (even in the admin + * chrome), and choosing a theme or toggling light/dark must apply the document + * attributes the preview stylesheet is scoped to and persist the choice. + * + * It renders the REAL `ThemeSwitcher` under the REAL `AdminThemeProvider` with + * `loadRuntimeControls` mocked to never resolve, so `controlsReady` stays false and the + * fallback is what renders. + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +import { AdminThemeProvider } from './admin-theme' +import { ThemeSwitcher } from './shell' + +vi.mock('../lib/runtime-controls', () => ({ + // Never resolves: the shipped controls never upgrade, so the fallback stays in place. + loadRuntimeControls: () => new Promise(() => undefined), +})) + +vi.mock('../lib/api', () => ({ + getManifest: vi.fn(() => Promise.resolve({ apiVersion: '1', baseUrl: '/rt/v1', themes: [] })), + getSession: vi.fn(() => Promise.resolve({ authenticated: true, mode: 'dev' })), + updatePreferences: vi.fn(() => Promise.resolve({ selectedTheme: '', selectedAppearance: '' })), + resetPreferences: vi.fn(() => Promise.resolve({ selectedTheme: '', selectedAppearance: '' })), +})) + +const api = await import('../lib/api') + +const THEME_ATTR = 'data-bananas-theme' +const APPEARANCE_ATTR = 'data-bananas-appearance' + +function renderSwitcher() { + const client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } }) + return render( + + + + + , + ) +} + +// The dropdown is populated from the runtime manifest, which arrives asynchronously. A +// controlled