mirror of
https://github.com/penpot/penpot.git
synced 2026-09-21 03:16:15 +00:00
* ✨ 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>
75 lines
3.0 KiB
TypeScript
75 lines
3.0 KiB
TypeScript
import { createTestSuite, TestRunObserver, TestResult } from "../composable-tests";
|
|
|
|
// In-sandbox CI entry point. Built as a standalone IIFE bundle (dist/headless.js)
|
|
// and evaluated inside a real Penpot plugin sandbox by the out-of-sandbox driver
|
|
// `ci/run-ci.ts` (note: a different `ci/` directory). It runs the composable test
|
|
// suite without any UI and reports each result through `console.log` markers that
|
|
// the Playwright driver parses. The marker protocol mirrors the one used by the
|
|
// plugin-api-test-suite.
|
|
|
|
/** What the driver receives for each finished test. */
|
|
interface ReportedResult {
|
|
/** The composite case identifier, e.g. "MainEditSyncs-2". */
|
|
identifier: string;
|
|
/** The test's descriptive name (identifies the variant's choices). */
|
|
name: string;
|
|
passed: boolean;
|
|
durationMs: number;
|
|
error?: string;
|
|
/** The applied steps, for diagnosing failures from the CI log. */
|
|
transcript?: readonly string[];
|
|
}
|
|
|
|
async function main() {
|
|
const suite = createTestSuite();
|
|
const tree = suite.tree();
|
|
|
|
// Composite identifiers ("Identifier-N", 1-based) for reporting, and the
|
|
// id order matching the tree — the same addressing the panel UI uses.
|
|
const identifierById = new Map<string, string>();
|
|
const orderedIds: string[] = [];
|
|
for (const group of tree.groups) {
|
|
group.tests.forEach((test, i) => {
|
|
identifierById.set(test.id, `${group.identifier}-${i + 1}`);
|
|
orderedIds.push(test.id);
|
|
});
|
|
}
|
|
|
|
// Set by the driver from TEST_FILTER: run only tests whose composite
|
|
// identifier contains the given substring (case-insensitive), e.g.
|
|
// "MainEditSyncs" (whole case) or "MainEditSyncs-2" (single variant).
|
|
const filter = (
|
|
globalThis as unknown as { __COMPOSABLE_SUITE_FILTER__?: string }
|
|
).__COMPOSABLE_SUITE_FILTER__?.toLowerCase();
|
|
const ids = filter ? orderedIds.filter((id) => identifierById.get(id)!.toLowerCase().includes(filter)) : orderedIds;
|
|
|
|
const startedAt = new Map<string, number>();
|
|
let failed = 0;
|
|
|
|
const observer: TestRunObserver = {
|
|
onTestStarted(id: string): void {
|
|
startedAt.set(id, Date.now());
|
|
},
|
|
onTestFinished(id: string, result: TestResult): void {
|
|
if (!result.passed) failed++;
|
|
const reported: ReportedResult = {
|
|
identifier: identifierById.get(id) ?? id,
|
|
name: result.name,
|
|
passed: result.passed,
|
|
durationMs: Date.now() - (startedAt.get(id) ?? Date.now()),
|
|
error: result.errorMessage,
|
|
transcript: result.passed ? undefined : result.transcript,
|
|
};
|
|
console.log("__TEST_RESULT__ " + JSON.stringify(reported));
|
|
},
|
|
};
|
|
|
|
await suite.run(ids, observer);
|
|
console.log("__TEST_DONE__ " + JSON.stringify({ total: ids.length, failed }));
|
|
}
|
|
|
|
main().catch((err: unknown) => {
|
|
const message = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
console.log("__TEST_FATAL__ " + JSON.stringify({ message }));
|
|
});
|