Dr. Dominik Jain caacd482af
Add composable test framework plugin (applied to component tests, runs in CI) (#10679)
*  Add plugin with composable test framework and component tests

The plugin provides a framework for writing composable tests against
the Plugin API, and applies it to systematic end-to-end testing of
component semantics.

The framework's core ideas: a test is written once as a composition of
operations over a starting configuration; choice points among the
operations (optional steps, alternatives) expand the composition into
a full sweep of test variants, so a single case definition yields
broad combinatorial coverage; and the operations drive the real Plugin
API with real change propagation, testing the full production
implementation.

The initial application is a suite of component test cases covering
synchronization, overrides, swap slots and variants — the
TypeScript/e2e continuation of the ClojureScript composable test suite
(frontend_tests.composable_tests). Several cases originate from
reproducing real defects (e.g. #10109 and the swap-slot corruptions).

Tests run from an interactive panel in Penpot: cases are listed with
plain-language descriptions, tests can be run selectively, results
stream in live, and every checkbox carries a stable DOM id — so the
panel can equally be driven programmatically (the basis for running
the suite in CI), as documented in the plugin's README.

Lives at plugins/apps/composable-test-suite as a regular member of the
plugins workspace (init script, start:plugin:composable-test-suite,
shared dev port 4202, covered by build:plugins via the new
./apps/*-test-suite filter).

Related to #10584.

AI-assisted-by: claude-fable-5

*  Run the composable test suite headlessly in CI

Adds a headless run mode for the composable test suite, following the
plugin-api-test-suite's CI architecture, and a workflow that runs it as
a per-PR gate.

An in-sandbox entry (src/ci/headless.ts) runs the suite without the
panel UI — the framework's runner was UI-free by construction, so no
refactoring was needed — and streams each result through console
markers, addressed by the same composite identifiers the panel uses
(e.g. MainEditSyncs-2), with durations and, on failure, the error and
the applied-steps transcript. It is built as a single self-executing
bundle and evaluated directly inside a real Penpot plugin sandbox by
the driver (ci/run-ci.ts), so no plugin dev server or port is involved.

The driver needs no backend and no login: it serves the prebuilt
frontend bundle via the frontend e2e static server and intercepts every
backend RPC with Playwright fixtures. The mocked backend is not a
limitation for this suite — everything it asserts is frontend store
logic executed in memory — which the full run confirms: all 48 tests
behave identically to the interactive panel, including variants and
swap slots, with the single (currently expected) failure of
MainEditSyncs-2 reproducing bug #10109 under the mock.

TEST_FILTER selects tests by identifier substring; CI_TIMEOUT_MS bounds
the run. The mock harness mirrors the frontend e2e harness (see the
provenance note in the driver).

Related to #10584.

AI-assisted-by: claude-fable-5

* 📚 Restructure the composable-tests memory around both suites

Present the composable component tests top-down: the shared framework
principles upfront, then the two implementations — the ClojureScript
suite in the frontend test tree and the TypeScript suite in the plugin,
which tests fully end-to-end with a slightly more elaborate set of
abstractions — and the plugin's headless CI run, pointing to the
plugin's README for operational details. Also records this session's
additions (geometry operations, case N, the CI harness).

AI-assisted-by: claude-fable-5

* 📎 Refine the PR-description conventions in the creating-prs memory

Encourage digestible descriptions: bullet items over prose (grouped by
area with bold lead-ins for larger PRs) and no manual line wraps, since
the rendered markdown adapts to the viewport. Also drop the outdated
'MCP' from the standard Note line.

AI-assisted-by: claude-fable-5

* 🔧 Set Prettier endOfLine to auto in plugins workspace

Prettier defaults to endOfLine "lf", which is incompatible with
checkouts on Windows that use core.autocrlf=true

* 🐛 Fix problems with suite

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-07-24 09:59:18 +02:00

191 lines
4.2 KiB
CSS

@import "@penpot/plugin-styles/styles.css";
body {
margin: 0;
padding: 0;
}
.plugin-container {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
padding: var(--spacing-12) var(--spacing-8);
box-sizing: border-box;
color: var(--foreground-primary);
user-select: none;
-webkit-user-select: none;
}
/* ── toolbar + summary ──────────────────────────────────────────── */
.toolbar {
display: flex;
gap: var(--spacing-8);
}
.summary {
font-weight: 600;
color: var(--foreground-secondary);
}
/* ── tree / groups ──────────────────────────────────────────────── */
.tree {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
}
.group {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
.group-header,
.row-header {
display: flex;
align-items: center;
gap: var(--spacing-8);
padding: var(--spacing-8);
border-radius: var(--spacing-8);
background-color: var(--background-tertiary);
}
.group-header {
background-color: var(--background-quaternary);
font-weight: 600;
}
.caret {
flex: 0 0 auto;
width: 8px;
color: var(--foreground-secondary);
transition: transform 0.1s ease;
}
.caret::before {
content: "▸";
}
.group[data-expanded="true"] .caret {
transform: rotate(90deg);
}
.row-header {
cursor: pointer;
}
.row-header:hover {
background-color: var(--background-quaternary);
}
.group-rows {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
padding-left: var(--spacing-16);
}
/* the [hidden] attribute must win over the display rules above */
.group-rows[hidden],
.row-details[hidden] {
display: none;
}
.row-name {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.group-name {
flex: 0 0 auto;
white-space: nowrap;
}
/* passed / failed counts, at the right edge of the group header */
.group-passfail {
flex: 0 0 auto;
margin-left: auto;
font-weight: 400;
color: var(--foreground-secondary);
}
/* the full description in the unfolded section: its own box, wrapping freely
and growing to whatever height the text needs */
.group-description {
padding: var(--spacing-8);
border-radius: var(--spacing-8);
background-color: var(--background-tertiary);
color: var(--foreground-secondary);
white-space: normal;
}
.group-count,
.row-status {
flex: 0 0 auto;
color: var(--foreground-secondary);
}
.row-status {
font-style: italic;
}
/* ── status circle ──────────────────────────────────────────────── */
.status-circle {
flex: 0 0 auto;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: var(--background-quaternary);
border: 1px solid var(--foreground-secondary);
}
/* "in progress": the theme's accent (as used by the buttons) — neutral between
passed (green) and failed (red); the palette's --warning-500 is a red-orange
that reads as a failure */
.status-circle[data-state="running"] {
background-color: var(--accent-primary);
border-color: var(--accent-primary);
}
.status-circle[data-state="passed"] {
background-color: var(--success-500);
border-color: var(--success-500);
}
.status-circle[data-state="failed"] {
background-color: var(--error-500);
border-color: var(--error-500);
}
/* ── fold-out details ───────────────────────────────────────────── */
.row-details {
padding: var(--spacing-8) var(--spacing-8) var(--spacing-8) var(--spacing-24);
color: var(--foreground-secondary);
}
.details-hint {
color: var(--foreground-secondary);
font-style: italic;
}
.details-error {
color: var(--error-500);
white-space: pre-wrap;
margin-bottom: var(--spacing-8);
}
.details-steps {
margin: 0;
padding-left: var(--spacing-16);
}
.details-steps li {
margin: 0;
}