* test: add visual regression suite for the admin UI
Adds a Playwright visual-regression suite covering key admin screens in
LTR (English) and RTL (Arabic), gated behind EMDASH_VISUAL=1. Adds the CI
trio: an untrusted measure job, a trusted report job that comments diffs,
and a reaction-gated apply job that commits accepted Linux baselines on a
maintainer 👍. Adds inert data-testid hooks to mask drift-prone timestamps
and version strings.
* fix: address review on visual regression suite
- Widen visual.yml path filter to the whole e2e tree + playwright.config
so fixture/seed/setup changes that alter screenshots trigger a measure run
- Await document.fonts.ready without returning the non-serializable
FontFaceSet, which would throw in Playwright
- Import the canonical ServerInfo type from the fixtures instead of
redeclaring a narrowed copy
- Report workflow: on a bootstrap run with no prior baselines, show the
proposed candidate baselines instead of an empty "0 screens changed" table
- Add an empty changeset (only published-package change is inert testids)
* ci: run visual regression on lockfile/manifest changes
A Playwright or Chromium version bump alters pixel output, so a
pnpm-lock.yaml or package.json change can drift committed baselines
without touching any source path in the existing filter.
The admin shell opens the welcome modal whenever its currentUser query
resolves with isFirstLogin — on a cold workerd start that can be
seconds after the sidebar renders, past dismissOnboardingModal()'s 2s
visibility window, so the modal survives and swallows the test's
clicks (actions run 28678460523, E2E Cloudflare shard 3/8).
devBypassAuth now lands the bypass on the shell-free auth/me endpoint,
clears the welcomeDismissed flag over HTTP, and only then loads the
admin shell, so the first currentUser fetch already reports
isFirstLogin: false and the modal can never mount on the standard auth
path. The UI-side dismissal stays as a fallback for flows that
authenticate outside the bypass.
* fix(admin): remove sticky editor header
The sticky save header had multiple compounding problems:
- Backdrop-blur transparency let scrolled content show through, looking
visually broken when columns scrolled at different rates
- Fragile negative-margin trick (-mx-6 -mt-6) had to perfectly cancel
parent padding; broke when ancestors changed
- z-index conflicts with the app bar header above
- ~85px of permanent vertical chrome on every editor page
The save action is already reachable via the form's bottom Save button
and the standard Cmd+S keyboard shortcut, so the sticky variant wasn't
earning its cost. Drop it across ContentEditor, ContentTypeEditor,
SectionEditor, and the settings sub-pages. Preserve the
distraction-free mode hover-revealed overlay in ContentEditor (different
use case: keep chrome out of the way while writing).
Drops the EditorHeader `sticky` prop entirely. Updates the test to
remove the now-irrelevant sticky-class assertions.
* fix(admin): add bottom-of-form Save buttons; correct doc claims
Address PR review feedback (#955): the previous commit claimed save was
'always reachable via in-form Save button + Cmd+S' but neither was true
for ContentEditor or SectionEditor (header was the only Save button, no
Cmd+S handler exists).
- ContentEditor: add a bottom-of-form SaveButton (mirrors the header
state, last interactive control in DOM order)
- SectionEditor: add a bottom-of-form SaveButton (after the content
editor, before the sidebar)
- Update doc comments and changeset to describe what's actually true
(bottom save button per editor; no Cmd+S claim)
- Update ContentEditor.test.tsx queries to use .first() since there are
now two Save buttons matching the same role/name (both submit the
same form, so either works for the assertion)
* fix(admin): move bottom Save button into main editor column
The previous commit put the Save button at the absolute bottom of the
form -- after both the main editing column AND the entire sidebar
(Publish/Status/Taxonomies/SEO/Outline/Revisions). Visual testing
showed users had to scroll past the whole sidebar to reach it; way too
hidden to be discoverable.
Move the bottom SaveButton inside the main column (lg:col-span-2 in
ContentEditor, col-span-8 in SectionEditor) so it appears right after
the body field, where users naturally land when they finish editing.
SectionEditor was already correct -- just the ContentEditor placement
was wrong.
* test(e2e): scope Save button queries to .first() now that editors have two
Editor pages now render two SaveButtons (one in the header, one at the
bottom of the main column). Both submit the same form, so playwright's
strict-mode click can target either, but the unscoped queries
('button:has-text("Save")', 'getByRole("button", { name: "Save" })')
match both elements and fail.
- e2e/fixtures/admin.ts: clickSave() and waitForSaveComplete() now use
.first() on the Save / Saved button locators.
- e2e/tests/form-data-loss.spec.ts: 3 inline Save queries updated to
.first().
Other E2E save references are either dialog-scoped (redirects), text
'Save Changes' instead of 'Save' (content-types), or on bylines panel
which has only one save button -- those are unaffected.
* feat(i18n): add support for menus and taxonomies
* fix(migration 036): block rollback on multi-locale installs
* test(migration 036): cover data remap and rollback
* fix(menus): surface name in NOT_FOUND, detect CONFLICT without locale
* fix(seed): set translation_group on menu, item, and taxonomy_def inserts
* fixup! fix(seed): set translation_group on menu, item, and taxonomy_def inserts
* style: format
* ci: update query-count snapshots
* feat(seed): i18n round-trip for menus and taxonomies
* fix: update migration 036 to resolve locale via i18n config and add regression tests for non-default locales
* style: format
* chore(menus,taxonomies): tidy module conventions
* feat: implement i18n menu cloning and migration refinements for locale-aware references
* fix: update migration references to 036 in menus and add documentation for pending tasks
* feat: extend featured_image schema with metadata and update e2e test fixtures
* ci: update query-count snapshots
* test: mock useSearch hook in MenuEditor tests
* fix(menus): use defaultLocale in CONFLICT guard and document seed item caveat
---------
Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
* fix: passkeys behind TLS reverse proxy
Add passkeyPublicOrigin and wire it through passkey routes so origin/rpId match
the browser when dev runs behind nginx. Expose dev-only /_emdash/api/dev/passkey-url,
add admin messaging for insecure WebAuthn contexts, nginx repro under demos/simple,
and direct kysely dependency for the simple demo Node adapter bundle.
Made-with: Cursor
* docs: add passkeyPublicOrigin to configuration reference
Adds the new passkeyPublicOrigin option and reverse proxy guidance
to the public-facing configuration docs as requested in PR review.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* update tests and more docs
* fix: add missing refresh-server-pat fixture and restore docs heading
---------
Co-authored-by: Joseph Eftekhari <jdeftekhari@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>