Skip to content

Windows / Linux continuous integration

Version 0.1.18 hardens the GitHub Actions matrix introduced in 0.1.17 in .github/workflows/ci.yml. Quality checks, Chromium scenarios and npm package production are separated so failures can be attributed to a precise boundary.

Quality matrix

Both ubuntu-latest and windows-latest run:

bash
bun install --frozen-lockfile
bun run ci:quality

ci:quality covers, among other checks:

  • static and runtime invariants;
  • Nuxt/Vue typecheck and E2E TypeScript typecheck;
  • VitePress documentation build;
  • playground Nuxt/Nitro build;
  • JavaScript, CSS and font budgets;
  • SSR build of a distinct Nuxt host application;
  • npm package file-manifest verification.

The lockfile is mandatory. Any package.json change must be followed by bun install and a committed bun.lock.

Playwright server isolation

Every browser command goes through scripts/run-playwright.mjs. The wrapper selects one free port from its own PID and then passes the exact same QFORM_E2E_PORT and QFORM_E2E_BASE_URL to the runner, the Nuxt server and all Playwright workers. This prevents a second playwright.config.ts evaluation from calculating a different Windows port. The port can still be fixed explicitly:

powershell
$env:QFORM_E2E_PORT = '4721'
bun run e2e:ci:functional

The server is ready only when /qform-e2e-ready responds. reuseExistingServer is disabled, so a stale Nuxt, VitePress or unrelated service cannot create a full suite of false missing-route failures.

The first mount of a SPA route can keep an empty #__nuxt shell while Vite transforms the client graph. The route helper now waits up to 120 seconds for the semantic page marker with commit navigation, without reloading after 45 seconds and cancelling the active compilation. release:browser:check runs axe and visual coverage in one Playwright session. The wrapper then waits for the isolated port to be released before returning control to the next command.

To test an application that was started explicitly:

powershell
$env:QFORM_E2E_BASE_URL = 'http://127.0.0.1:4173'
$env:QFORM_E2E_NO_SERVER = '1'
bun run e2e:ci:functional

Visual baseline bootstrap

ci:quality runs static checks, typechecks, builds, budgets, the host application and package verification without requiring PNG files that may not exist yet in a clean archive.

Initialize them explicitly once:

bash
bun run ci:quality
bun run e2e:ci:functional
bun run e2e:visual:bootstrap
# manually inspect tests/e2e/__screenshots__
bun run e2e:snapshots:check
bun run release:browser:check

release:check and release:browser:check remain strict when a baseline is missing. CI never updates snapshots automatically.

Chromium matrix

CI runs Chromium on both operating systems:

  • Linux: functional scenarios, migrations, templates, Theme Builder and axe audit;
  • Windows: axe audit and the complete visual suite using validated Chromium baselines.

Playwright screenshots are sensitive to font rendering. The official pixel baselines therefore remain Windows baselines, while Linux validates the same main workflows without pixel comparison.

Release-candidate host application

tests/host-app is a separate Nuxt application:

ts
export default defineNuxtConfig({
  extends: ['../../'],
  ssr: true,
})

It imports public subpaths only:

ts
import type { FormBuilderSchema } from '@vevedh/qform-builder-layer/types'
import type { QFormBuilderThemeInput } from '@vevedh/qform-builder-layer/theme'

Its build verifies that:

  • the layer works outside the playground;
  • SSR remains enabled;
  • FormBuilder and FormViewer are auto-imported;
  • public theme and type contracts resolve through package exports;
  • no internal app/... source import is required.

Local command:

bash
bun run host:build:check

Before the build, host:clean invokes scripts/clean-host-generated.mjs. This Node cleaner removes only tests/host-app/.nuxt and tests/host-app/.output, with bounded retries for transient Windows errors (ENOTEMPTY, EPERM, EBUSY). It does not rely on rm -rf, whose Windows implementation can fail on Nitro-generated node_modules junctions.

Bundle budget

After the playground build:

bash
bun run bundle:budget:inspect

Current limits are:

AssetIndividual budget
JavaScript800,000 bytes
JavaScript gzip300,000 bytes
CSS300,000 bytes
CSS gzip70,000 bytes
Font512,000 bytes
Aggregate fonts650,000 bytes

The report is written to .artifacts/bundle-budget.json.

Version 0.1.17 removes the material-icons-outlined and material-symbols-outlined families. The few prefixed icons were replaced with standard Material Icons equivalents. Iconify remains bundled through the controlled UnoCSS catalog.

Verified npm artifact

Three commands are available:

bash
bun run pack:verify
bun run pack:artifact
bun run package:ci:artifact

pack:verify inspects the tarball file list without keeping it. pack:artifact produces the .tgz file, package-manifest.json and the tarball SHA-256 digest in .artifacts/. package:ci:artifact is the command used by GitHub Actions: it cleans .artifacts, builds the tarball, runs the npm dry-run from that tarball, creates release-candidate-evidence.json, then verifies that the three uploaded files really exist.

The gate rejects .playground, tests, .github, internal patch journals, ZIP archives and lockfiles.

Local validation before pushing

bash
bun run clean
bun install
bun run ci:quality
bun run e2e:ci:functional
bun run release:browser:check
bun run package:ci:artifact

All twelve visual baselines must exist and be reviewed before running release:browser:check.

Verify workspace completeness

bash
bun run workspace:check

An overlay is incremental. When extracted into an empty directory, package.json may declare commands whose scripts and test targets are absent. The completeness guard fails before CI and lists the missing boundaries. Initialize or recover from the full archive first, then apply the overlay.

Final release-candidate gate

The workflow exposes an Automated release candidate gate job that depends on the quality, Chromium and package jobs. It provides one branch-protection target for automated checks; manual evidence is enforced by release:evidence:verify.

The local equivalent is:

bash
bun run release:candidate:check

api:contract:check enforces the API freeze during quality jobs, while typecheck:contract compiles an external consumer.

RC evidence ledger — 0.1.21

Before bun install --frozen-lockfile, every CI job now runs:

bash
node scripts/check-bun-lockfile.mjs

This step provides an actionable diagnostic when package.json and the root bun.lock workspace diverge. It does not replace frozen installation, which remains authoritative.

The package job then generates, in the same job and before upload-artifact:

bash
bun run package:ci:artifact

This command retains .artifacts/release-candidate-evidence.json with the tarball and its manifest. The ledger may remain blocked during ordinary CI while manual review is pending. Strict local promotion requires:

bash
bun run release:evidence:verify

See Release candidate preflight.

Illustrated-guide artifact

The Linux browser job regenerates the guide after the functional suite:

bash
bun run docs:guide:ci

This command produces all twelve FR/EN screenshots, refreshes their SHA-256 manifest and runs strict verification. GitHub Actions then uploads docs/public/guide/screenshots/ as the qform-automated-user-guide artifact for review before integration.

FormKit 2.1 and polished-panel acceptance

Version 0.1.33 keeps 2.1.0 as the exact development baseline for @formkit/core, @formkit/i18n, @formkit/inputs, @formkit/nuxt, @formkit/utils and @formkit/vue. Peer dependencies remain bounded to ^2.1.0: compatible FormKit 2 updates are accepted, while a future FormKit 3 line requires an explicit patch and a new validation matrix.

Run the focused contracts with:

bash
bun run formkit:upgrade:check

The new left and right panel treatment intentionally changes pixels. The candidate archive keeps the former images only as bootstrap references, and the strict release gate must remain blocked until the authoritative Windows 11 regeneration is complete:

powershell
bun run clean
bun install --frozen-lockfile
bun run formkit:upgrade:check
bun run e2e:visual:update
# manually inspect all 12 PNG files in tests/e2e/__screenshots__
bun run docs:screenshots
# inspect all 12 FR/EN guide screenshots
bun run release:browser:check
bun run docs:screenshots:strict
bun run release:publish:check

Commit PNG files only after inspecting light and dark modes, keyboard states, the dedicated QEditor/QSelect/QRating/QBtn settings panels and both drawer reflow states. Do not raise the global visual threshold to accept the redesign.

Second polished-pass acceptance criteria

The Windows 11 visual review requires a clearer hierarchy without changing behavior:

  • flat, compact left catalog with permanent search and filtered counts;
  • primary accents reserved for hover, focus and selection;
  • bounded fluid widths computed from the actual builder container;
  • a selected-field header showing the user label, technical name and adapter type;
  • stronger priority for common settings groups without removing advanced settings;
  • the same neutral shell for field, column and step configuration.

The public contract must remain identical. Visual acceptance still requires authoritative screenshot regeneration on Windows 11.

Axe contrast and selected-field DOM contract

The Windows 11 replay identified two precise contracts: inactive tabs must not retain Quasar's default opacity, and the rename scenario must not depend on an HTML heading level. The stronger secondary token is #526174 in light mode and #cbd5e1 in dark mode. It exceeds 4.5:1 against #ffffff, #f1f5f9 and #fbf5f0.

The selected field technical name is exposed through:

css
[data-qform-selected-field-name]

E2E tests must use this stable contract. After changing panel colors, rerun bun run e2e:visual:update and inspect the baselines before continuing.

Responsive auto-fit drawers

The default mode does not use the global browser viewport. A ResizeObserver measures the actual FormBuilder container, including builders embedded in cards, tabs or host application panes.

The resolver protects these bounds:

  • left drawer: 220 to 320 px while persistent;
  • right drawer: 280 to 480 px while persistent, with a preferred minimum of 360 px;
  • central canvas: at least 480 px while both drawers are open;
  • below the minimum capacity, drawers become overlays and retain a 16 px edge gap;
  • on narrow layouts, opening one drawer closes the other to prevent overlap.

The properties drawer uses auto-fit grids, min-width: 0 controls and container queries: alignment groups move from four to two columns below 390 px, while selectors fall back to one column below 300 px. Both drawers keep one internal scrolling region and must not create horizontal page overflow.

Run the dedicated gate with:

powershell
bun run drawers:responsive:check

The accessibility Playwright scenario covers 1024 px, 860 px and 375 px layouts, protected canvas width, persistent/overlay switching and mobile drawer mutual exclusion. The Styles scenario additionally measures the drawer at 1600 px, 1024 px and 375 px and rejects visible horizontal overflow in its QScrollArea and any Quasar control escaping the view; the diagnostic JSON may keep a local scroll surface. The width is compared with the value resolved from the real builder container rather than a viewport-derived threshold.

A single scrollbar in the left drawer

The left drawer shell is a bounded flex column. Primary tabs remain fixed and the single QScrollArea consumes only the remaining height. It must never keep Quasar's fit class, because adding 100% height below the tabs recreates drawer overflow and therefore a second native scrollbar.

The stable browser contract is:

css
[data-qform-elements-scroll-area]

After any catalog change, visually confirm that only one vertical scrollbar is visible on the left, then rerun bun run formkit:compat:check and the Playwright suite. The outer drawer must remain overflow: hidden, while the inner area keeps scrolling for elements, templates and the tree.

QSelect runtime interoperability and Windows 11 HMR

The playground must expose the HMR WebSocket on the same public port as Nuxt. The port is resolved from the CLI --port option, then NUXT_PORT or PORT; 3000 is only the local fallback. The browser must never connect directly to Vite's private 5173 port, and the configuration must keep accepting Playwright's randomized isolated ports.

The FormKit q-select custom input reads the live context._value. Before handing that value to Quasar or committing it through node.input(), the adapter normalizes single/multiple models and detaches reactive or null-prototype objects that String(value) cannot convert. When emitValue is enabled, only the expected optionValue path is retained.

Focused Windows validation:

powershell
bun run clean
bun install --frozen-lockfile
bun run formkit:upgrade:check
bun dev

On /, switch to preview, change Objet de la demande, then type into Message. The browser console must not contain any of these messages:

text
[vite] failed to connect to websocket
Cannot convert object to primitive value
Cannot read properties of null (reading 'emitsOptions')

Finish with bun run e2e:visual:update. The existing QSelect scenario verifies the host-controlled scalar value, a subsequent input update and the absence of pageerror; the matrix must remain 24/24.

npm artifact in GitHub Actions

The Package artifact job creates evidence with bun run package:ci:artifact, then uploads it from the canonical .artifacts directory. Because this directory starts with a dot, every actions/upload-artifact step that targets .artifacts/*.tgz, .artifacts/package-manifest.json or .artifacts/release-candidate-evidence.json must keep include-hidden-files: true. Without that option, the upload can fail with No files were found with the provided path even when package generation succeeded.

QForm Builder — Nuxt 4, Quasar and FormKit layer for dynamic forms.