The handle transform composed rotation incorrectly, so handles blew out in size when an ellipse was rotated.
Fixes#10069
Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
* ✨ 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>
Use Penpot's shared image type list for image fill uploads and add an integration test covering SVG file selection.
AI-assisted-by: gpt-5.6-sol
Signed-off-by: Lucas Ozdemir <lulu.58@outlook.fr>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
The change-password form validated the existing password against the 8-character policy, locking out accounts whose current password predates it.
Closes#10626
Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
The Color tokens picker showed token sets in their raw definition
order (ascending precedence), so the lowest-precedence set appeared
first and the highest-precedence (last-defined, winning) set
appeared last. This is the opposite of what's useful: users care
most about which set is currently winning, so that one should be at
the top.
get-sets returns sets in definition order and the picker's
grouped-tokens-by-set pipeline (add-tokens-to-sets ->
filter-active-sets -> filter-non-empty-sets -> group-sets ->
combine-groups-with-resolved) preserves that order at every step, so
the picker just rendered get-sets' raw order. Reverse the set seq
once, before it enters the pipeline, so the highest-precedence set
renders first.
group-sets groups sets by parent path via group-by, which risked
restoring definition order within a subgroup independent of the
reversed input order. Added tests covering a flat set list, a
reversed subgroup (to confirm group-by does not silently re-sort
subgroup members), and a mixed flat/subgrouped list.
Closes#10552
Signed-off-by: Andrew Cunliffe <cunliffeandrewc@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
The workspace route can be reached without a `:file-id` query parameter
(e.g. `/#/workspace?team-id=...`). When that happened, `workspace*` was
emitting `dw/initialize-workspace` with a nil file-id, which stored nil
in `:current-file-id`. The `fetch-profiles` event then read nil from
state and called `:get-profiles-for-file-comments` with `{:file-id
nil}`, producing a 400 response.
Move the `use-equal-memo` calls for `file-id` and `page-id` from
`workspace*` up to `workspace-page*`, and guard the render with
`(when (uuid? file-id) ...)` so `workspace*` only mounts when `file-id`
is a valid uuid. Since `workspace*` never mounts with a nil `file-id`,
`initialize-workspace` is never emitted with nil, and the 400 is
prevented at the source.
AI-assisted-by: minimax-m3
* 🎉 Add page multi-selection in the workspace sitemap
* ♻️ Simplify page selection state updates with single assoc
* 🐛 Fix SCSS issue
* ♻️ Update some components to new syntax
* ♻️ Adapt SCSS to the new guidelines
* ♻️ SCSS cleanup
---------
Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
* ✨ Add list view toggle for dashboard files
* ✨ Add drop files visual feedback
* ♻️ Use radio buttons component from DS
* ♻️ Use hook to keep layout status
* ♻️ Refactor code and SCSS
---------
Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
The webhook creation form used a hardcoded AWS instance metadata
endpoint (http://169.254.169.254/...) as the default :uri value for
new webhooks. This leaked an internal cloud credential endpoint into
the UI defaults and could expose it to users.
Remove the default :uri so new webhooks start with an empty URI
instead of a sensitive hardcoded value.
The dashboard profile menu submenus (Help & Learning, Community &
Contributions, About Penpot) opened on pointer enter but nothing closed
them when the pointer left the option, leaving a stale submenu visible
until the whole menu closed.
Close the open submenu when the pointer leaves an expandable option or
its submenu, with a 200ms grace period (same approach as the workspace
context menu) so the submenu survives the pointer crossing the gap
between the parent menu and the floating submenu. Keyboard navigation
is unchanged and now covered by tests.
Closes#10549
AI-assisted-by: claude-fable-5
Signed-off-by: Akshit Nassa <nassaakshit@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
Guard remaining text pipeline locations that accessed :selrect directly
against nil/zero-dimension selrects by using safe-size-rect, which
provides a 4-level fallback chain (selrect -> points -> shape fields ->
empty 0.01x0.01 rect).
Fixes:
- fix-position in viewport_texts_html.cljs: replaced dm/get-prop
:selrect with ctm/safe-size-rect for both old and new shape
- assoc-position-data in modifiers.cljs: replaced (:selrect ...)
with ctm/safe-size-rect for delta computation
- change-orientation-modifiers in modifiers.cljc: replaced raw
:selrect access with safe-size-rect for scale and origin computation
Closes#10617
AI-assisted-by: mimo-v2.5-pro
Use `get` instead of `nth` to avoid index-out-of-bounds when a selected
component copy has fewer variant properties than the first component in
the selection.
Closes#10615
AI-assisted-by: deepseek-v4-pro