Repository navigation
E2E test suite (Playwright) and the bugs it found - #74
Open
alexcibotari wants to merge 221 commits into
Open
alexcibotari wants to merge 221 commits into
alexcibotari wants to merge 221 commits into
Conversation
- Translation detail: a live refresh reset the target locale to the default, so a value typed for another locale was saved over the default one, and unsaved text was discarded. Inputs are now followed by value, not object identity. - SpaceStore: the live space list reset the open content/asset folder to the root whenever any space changed; it now keeps them while the same space stays selected. It also only listens to documents/schemas the user may read, once permissions are known (no endless 403 retries and "Lost connection" toasts for translators). - General settings: live updates overwrote a space name being typed. - Edit document: a never-saved document opened by deep link showed no fields (its _schema came from the not-yet-loaded schema list). - authGuard waits for the session check instead of a flag cached in localStorage: deep links in a fresh browser landed on the welcome page. - The GitHub release check handles its error (uncaught on offline installs). - Webhook detail: optional paginator query (NG0951, logs never paginated). - aria-labels on icon-only buttons (from their tooltip), labels for the translation locale selects and the space name field.
apps/e2e (@localess/e2e) runs the production SPA against the built server on a fresh embedded Postgres: 63 tests across auth, users and permissions, profile, spaces and settings (locales, environments, tokens), translations, schemas, contents, assets, dashboard, webhooks, and the CDN / Management APIs (browser-less `api` project). - Tests own their data (space / createUser fixtures) and run in parallel. - Every UI test fails on uncaught errors and anything reported to the ErrorHandler; all requests outside the server under test are aborted (offline install). - pnpm e2e (build + run, used in CI), e2e:run, e2e:smoke (@smoke, ~30 s), e2e:ui. - TEST-PLAN.md: what can be tested and how, status per scenario, findings. - CI: E2E Tests job uploads the HTML report and traces of failures.
alexcibotari
force-pushed
the
feat/e2e-playwright
branch
from
October 15, 2026 18:48
5bf0cc2 to
7454657
Compare
Every admin test checks the state before (UI and API), acts in the UI, then checks the table (also after a reload), the API, and what the user can do with their own session. - Spaces: create (opens, in the switcher), cancel, name too short; rename (same id), cancel; delete with content, cancel. - Users: invite as Admin / Custom / None, taken email refused; role Custom → Admin → None → Custom; permissions; lock; disable / enable, disabling ends an open session; the admin can't disable themself; reset link via the clipboard; delete, cancel. - cleanup fixture for spaces and users created through the UI; API helpers to list and find spaces and users. - Users table: the Active and Locked icons get an accessible name (ng-icon hides icons from assistive technology unless aria-hidden is set explicitly).
Each test uses its own user and checks the page (also after a reload), the API and whether the account can sign in. - View: opens from the user menu; email, verification, role, a custom user's permissions; an admin has no permission list. - Profile: display name (page, user menu, API), avatar URL (an invalid one refused), cancel keeps everything. - Email: sign-in with the new email only, unverified, same account; refused with a wrong current password or another user's email. - Password: the old one stops working, other sessions end, this one stays; refused with a wrong current password; under 6 characters can't be submitted. - Locked account: message, no buttons, the API refuses changes.
…elected key - Labels (Add and Edit Translation): the label rules were applied to the list, not to each label. A single label left Save disabled with no message, while duplicates, labels with spaces and 1-character labels were accepted (the Edit dialog even used the description's rule). Each label is now checked when entered, the reason shown and the text kept; the list only keeps the server's limit of 100. - Tree view: the tree was given the selected translation's id while its nodes are keyed by translation key, so the selection (and the saving spinner) never showed. - Accessible names for the translation filter's locale select and the Translate Locale dialog's selects (their labels pointed at the host, not the trigger).
33 tests in tests/translations, pre-check → action → post-check (list, detail panel, after a reload, API, CDN where publishing matters), replacing content/translations: - add (with machine translation of the other locales), duplicate keys in any letter case, invalid keys, cancel - edit values per locale, Save only on change, rename (duplicates refused), description and labels, delivery only after Publish - delete (all locales, CDN after Publish), cancel - labels: one label, add / remove, duplicates and invalid labels refused, filter - language and search: values per locale, key and value search per language, status filters, combined filters - machine translation: one value into German and Romanian, another source, not into itself, Translate Locale with and without overwrite - list view (virtual scrolling, live updates) and tree view (groups, expand, selection, search, layout kept)
…o-do 19 tests in tests/assets, pre-check → action → post-check (list, breadcrumb, after a reload, App API, CDN), replacing content/assets: - upload: an image (type, size, bytes on the CDN), several types at once, drag and drop, from a link, into the open folder, a name that exists (both kept) - create folder: nested, breadcrumb back; a name the folder has refused, another folder may have it; too short, spaces around; cancel - rename: a file (extension and id kept, download name), alt text, a folder (contents stay), taken names refused, cancel - delete: a file (CDN 404), a folder with everything in it, a file a content uses (warning, content stays), cancel docs/roadmap/asset-names.md (TODO): short file names block Edit (N1), duplicate names differ between upload, dialogs and server (N2), Edit Folder stops at 100 characters (N3); "roadmap check" tests pin today's behaviour. Test fixes: delete checks wait for the confirmation to close (a modal hides the page from the accessibility tree), and select helpers wait for the previous list to close (its options briefly doubled the next one's).
… and locales - Schema editors (Root/Node and Enum): any save of another schema of the space re-emitted the live schema list and reset the form, wiping unsaved changes. The form now reloads only without unsaved changes; Save marks it saved and reloads it once, not over edits made since. - Content editor: the chosen locale jumped back to the default whenever any space changed (it followed the live space list by identity). It is kept while the space has it. - Edit Field: a Schema / Schemas field could allow no schema at all, the picker selected nothing: the allowed schemas started as undefined instead of []. - Enum editor: an existing value name was not refused (the validator's taken-names array was replaced on load), and deleting a value freed another value's name. - Schema labels had no rules: the same per-label check as translation labels, now in a shared LabelValidator; labels set as a new list and the form marked changed. - Labels: Edit Field's Display Name and Description pointed at the Settings tab's inputs (duplicate ids, Settings ids renamed); the content editor's Option / Options labels pointed at nothing (for="kind").
… edit ID, delete 26 tests in tests/schemas, pre-check → action → post-check (list, editor after a reload, App API, and the content editor where a schema's effect shows), replacing content/schemas: - create: Root / Node / Enum open their editors, ID from the display name and Normalize, an existing ID, reserved and invalid IDs, cancel - fields: added as Text in order on Save; kind, display name, required, translatable, description seen in the content editor; invalid names; delete; drag reorder; leaving with unsaved changes; unsaved changes survive another schema's save - settings: display name, description, preview field, labels and their rules - references: Option from an Enum and Schemas allowing a Node in the content editor; wrong types refused by the server; only Enums offered as a source - enum values: add, edit, delete, reorder; an existing name and a value with spaces - edit ID and delete: unused, existing ID, in use by a schema (disabled, 409), in use by contents only (explained), cancel - list: search, labels, Reset
- Visual Editor: clicking a field of the document itself (not inside a block) on the preview did nothing; the field to focus was only set inside the walk through blocks. - Number fields ignored a limit of 0 (truthiness check): a "minimum 0" price took -5. - Blocks in the editor showed `#<schema UUID>`, and the add-block menus fell back to the UUID, instead of the schema's ID (name). - Publishing or unpublishing from the content list reported a failure as a success. - Accessible names: the document status icon (Published / Draft / Not published, was a tooltip only), the field's translate button, the editor's root breadcrumb.
…idation, locales, Visual Editor 34 tests in tests/contents, pre-check → action → post-check (list or form, after a reload, App API, and the CDN API), replacing content/contents: - tree: nested folders and breadcrumb, documents in the open folder (Root schemas only), slug rules, renamed slugs move CDN paths, move, clone, delete with cascade, copy slug, folder publish / unpublish - lifecycle: Not published → Published → Draft with public vs preview tokens, unpublish, Ctrl+S and the unsaved-changes guard - form: Text, Text Area, Number, Color, Date, Date and Time, Boolean, Rich Text, Markdown, Option(s), Link (URL and document, resolveLink), Reference(s) (resolveReference), Asset(s) (resolveAsset), Schema and Schemas blocks (add above, clone, delete, drag) - validation: required of several kinds and the Errors list, length / value / count limits (0 included), another locale's error; the API storing invalid content (pinned) - locales (en / de / ro): per-locale vs shared fields, CDN with fallback, translate a field and a whole document (blocks included) - delivery: /links, content.published webhook, live updates, Draft / Published JSON - Visual Editor: the environment URL per locale; a local site with sync-v1.js follows typing live and a click focuses the field Also: the offline rule lets 127.0.0.1 through (sites a test serves); waits after repeated identical toasts confirm the result (row or API) instead of the toast.
…heck - Locales: add, remove (CDN falls back to the default), set another default (fallback follows, old default deletable), Move Up / Move Down - Environments: add, edit, remove, drag to reorder ((default) follows) - Tokens: edit name and Cache TTL; CDN redirect Cache-Control follows - Danger Zone: delete all translations (published stay until next publish) - Webhooks: create, edit (new URL only), disable / enable, delete, failed delivery logged with status code, error type and URL - Shared dragAbove helper for schemas, blocks and environments - TEST-PLAN: new rows, stale planned rows marked covered
- Assets move: file into a folder and back (same id, URL works), folder with its subtree, dialog leaves out the current folder and the subtree, API refuses a cycle; roadmap check for duplicate names on move (N2) - Grid layout (cards, actions, remembered, folder opens) and image preview - Visual Editor: block toolbar on the page (move, duplicate, delete with a second click), hover highlight both ways, saved order fix(web): image preview kept its caption and Close below a small screen A fixed min-h-[800px] beat the max height, so on a window under ~980px high the dialog's footer was unreachable. The minimum is now capped by the window (unit test).
- Management API: schema export; push dry run, upsert, sync (refuses deleting a schema contents use); invalid pushes store nothing; OpenAPI - Permission matrix: every App API route (read from the server's access decorators) x each permission, no role, admin and no session - User managers: grant only what they hold, manage only users they outrank (API), and the invite dialog offers only that (UI) - TEST-PLAN: open finding on DEV_WEBHOOK vs SPACE_MANAGEMENT fix(web): Admin → Users offered "Regenerate Metadata" as a permission ASSET_REGEN_METADATA is an admin-only task kind, not a permission; the server refused the whole invite or save with it ticked. Not offered any more; a unit test checks the dialog offers exactly the server's permissions.
…mail reset, CLI - Space templates: each template's schemas, complete; none offered without Schema Create - Accessibility: axe-core scan (WCAG 2 A/AA, serious/critical) of the sign-in page, a space's pages, admin and profile - Google / Microsoft sign-in against oauth2-mock-server started next to the e2e server; refusals show their reason - Password reset by email through a mail catcher next to the server - Server CLI: check, admin:create, usage - oauth2-mock-server in the pnpm catalog; @axe-core/playwright for e2e fix(web): OAuth sign-in errors were never shown The login page's session check answers 401; the API interceptor then navigated to /auth/login and dropped the ?error= the callback came back with. No redirect while already on a sign-in page. fix(web): accessibility issues found by the scan Sidebar collapsible between ul and li; item groups and the translation list are lists (as shadcn's ItemGroup); names for the page-size select, help link, progress bars and provider icons; a hidden focusable button; Settings tabs point at the routed page as their panel.
… the Docker image - E2E_DATABASE_URL: the server uses that database (DATABASE_URL); the start script empties it first. CI job e2e-postgres: Postgres 18 service, the whole suite. - E2E_BASE_URL: no server is started; the tests run against that one. CI job e2e-docker: builds the image, runs it (embedded Postgres, host network), then the smoke suite. The CLI tests skip in that mode. - pg and @types/pg in the pnpm catalog (server and e2e)
- `pnpm demos` records apps/e2e/demos/*.demo.ts into apps/e2e/demo-videos (1600x900 WebM; with ffmpeg also an H.264 MP4 and a poster from the title card): Translations, Assets, Schemas - A director for watchable recordings: title card, captions, a visible pointer with click ripples kept in front of dialogs (top layer), the pointer gliding to controls, readable typing - Their own server, admin and ports (playwright.demos.config.ts); each demo works in a fresh "Acme Website" space; sample pictures drawn in code chore: prettier ignores Markdown and packages/ui; Docker context and docs - Root .prettierignore (Markdown keeps its line breaks, Spartan sources keep tabs), *.md in apps/web/.prettierignore - .dockerignore: the demo server's data and the videos - README: Playwright options pass through without `--` (with it, every option became a test filter and a full run started); CLAUDE.md lists `pnpm demos`
- Project skills (.agents/skills, linked in .claude/skills): writing-localess-e2e-tests (shape, rules, done-means, other tools) and covering-a-localess-feature (understand, confirm findings, prioritised proposal, rounds); both tested with and without the skill on real tasks - playwright-best-practices: Currents.dev's generic skill, vendored unchanged (MIT, commit 283d5cb, see SOURCE.md); ours win where they differ - Playwright Test Agents (planner, generator, healer) in .claude/agents with a Localess section each (fixtures, drafts only, never skip or loosen a test; the healer's test.fixme() step replaced by "stop and report"), .mcp.json for their MCP server (apps/e2e, headless), tests/seed.spec.ts on the fixtures, plans in apps/e2e/specs - Token Form name validation test (space/token-validation), from the skill test run
Google / Microsoft sign-in of a disabled account was refused, but the provider identity was linked to the account before the check. Disabled is now checked before linking; the server test asserts no identity row. Found by the e2e test "a disabled account is refused" (apps/e2e/tests/auth/oauth.spec.ts).
- No sleeps: webhooks use a witness webhook as a barrier, password reset waits for the answer (the mail is sent before it), the schema editor waits for the live refetch. - Repeated toasts are no proof: translate and fields-relations poll the API. - Checks that could pass before the page loaded: translations delete/search wait for the list, the dashboard a11y scan waits for its cards, dashboard counts are exact. - CDN specs run without the admin session; the transformed image must be image/webp. - Tighter assertions: a management token is never listed with its value nor shown after a reload; the added block is stored; opening a schema waits for the editor. - Fixtures: the error guard also fails on /api/ 5xx and watches popups; signedOutPage() replaces raw browser.newContext() (oauth, a11y, users) and login.spec uses the fixtures; teardown fails when it can't delete test data and attempts every delete. - CI: failOnFlakyTests, so a retry no longer hides a flaky test. - TEST-PLAN: the OAuth fix, and an open finding (an admin can delete their own account).
- dialogs: expectRefused (message + Save disabled), cancel, answerConfirm (native confirm()). - `probe` fixture (support/sessions.ts) replaces the copies of accessOf / canSignIn in the users and profile specs. - createEnum (support/schemas.ts); escapeRegExp and exactly (support/text.ts). - The seeded admin's email comes from env.ts everywhere.
- e2e-build builds the production SPA and the server once; e2e (two shards) and e2e-postgres download
them instead of building again.
- The shards write blob reports (E2E_BLOB=1); e2e-report merges them into one HTML report.
- e2e-docker caches the image layers (buildx, GHA cache) and uploads traces of failed tests.
- E2E_WORKERS accepts a number ("2" was refused: Playwright wants a number, not a string).
…nts list - Admin → Users dialogs: the Role label points at the select's trigger (buttonId), so the select has a name. - Settings → Locales: the Translate From / Translate To / Default icons have names (Yes, No, Default locale). - Settings → Visual Editor: the environments list is named "Environments".
…oads, refusals - New page objects: TokensPage (tokens and token-validation specs), ResetPasswordPage (5 copies of the reset form); pages/table.ts finds a cell by its column header (Role, ID) instead of a fixed index. - Selectors by role and label: the role select, the Lock User switch, locale icons, the environments list, webhook rows, the Danger Zone button, template radios (by their label's title). - Post-checks reload and read the API: token create/regenerate/delete (the id is kept on regenerate), webhook create/edit/toggle/delete, avatar update; refusals prove nothing changed (short space name, an invalid avatar URL, a used reset link). - Stronger checks: deleting a space empties its content and the CDN no longer finds it; the Blog template's document is stored; a space created without templates has no schemas; the Dashboard is checked by its cards; a disabled user's sign-in answers 401. - `newClient` fixture: signed-out HTTP clients disposed after the test (no leaks in the API specs); spaces made by the API specs have unique names and are cleaned up. - The webhook receiver and preview site close their keep-alive connections on teardown. - TEST-PLAN: open finding: deleting a locale keeps its stored values.
…sets, rich text field and preview iframe; point select labels at their triggers
…, API proofs of refusals and shared page-object helpers
…ads in translations and assets - aria-labels in the translations list/detail and the assets page (folder path, drop area, grid cards, Move folder field) - page objects: no CSS selectors (except the asset card picture), choose() for selects, openDelete/editFile/confirmDelete helpers - refusals: exact messages via expectRefused + cancel, API state compared with the pre-check - post-checks after a reload; size, move options, alt-only edit asserted - support: one FilePayload, assetNamed by listed name (unambiguous), published() fails on non-200 and caches its token
A radio menu stays open after its item is clicked; the helper now checks the item, closes the menu with Escape and accepts the default locale's "(Default)" suffix.
api/cdn-caching: a publish moves the cache version (translations, contents by id and slug) and an edit alone doesn't; the old version is redirected, never served; the versioned response is cached a week without an ETag; drafts get their own version per edit; a TTL-0 token's redirects are no-cache; 404 caching; assets cached a year with ETags and 304s, a bad transform parameter 400 for an hour.
…s in @localess/shared, the rate limit as numbers; the web's UnsplashService named after its controller The architecture test matches posix paths on every platform (on Windows the module checks matched nothing).
…unt, relational reads, writes answer nothing, 404s in the service; the Tasks page loads with pagedResource An unknown or malformed space answers 404 on the task list, exports and logs.
…enhance type definitions and return types in controller and service
…progress dialog components and models
…il; video playback in the preview
- assets.original_name (folded into 0000_init): each file is stored as
spaces/{s}/assets/{id}/{original_name}, the upload's sanitised file name, so
storage shows what it is; the API, URLs and downloads keep name + extension.
The Firebase import always fills it.
- Renditions get readable names with their format as extension
(w400.jpg, w800-h600-q60-fit-cover.avif, w200-frame.webp, full.jpg).
- GET /api/app/spaces/:s/assets/:id/thumbnail: one 512 px WebP (q75, fit
inside, never enlarged) per image or video, made on first request and warmed
after upload, stored as renditions/thumbnail.webp even with the rendition
cache off; used by every thumbnail of the platform. The v1 CDN API is unchanged.
- Asset/Assets content fields show video thumbnails too.
- The preview dialog plays videos: the thumbnail with a Play button, the
<video> mounted only on Play and streamed with Range requests; a video the
browser can't decode falls back to its picture and a download link.
ffmpeg wrote the extracted frame as frame.webp, which needs libwebp; some builds lack it (Homebrew on macOS: "Encoder not found"), so no video got a thumbnail. The frame is now a PNG, an encoder every ffmpeg has, and sharp encodes it to its final format. A video frame is now encoded to WebP explicitly when no ?format= is asked for: sharp keeps the input's format otherwise, and the public ?thumbnail route would have sent the PNG under image/webp. The v1 video test checks the bytes.
The preview dialog loaded images from the public CDN API
(/api/v1/…/assets/{id}?w=2000&thumbnail=true), and played videos and
downloaded from /api/v1/…/original and /download. New App API routes
(session cookie, any role):
- GET …/assets/:id/preview: an image as WebP, the longest side at most
2048 px, never enlarged, quality 85; generated the first time it is
previewed (not on upload) and stored as renditions/preview.webp, even with
the rendition cache off. An SVG or an animation is served as stored.
- GET …/assets/:id/original: the stored file with Range requests, inline
only where it can't script (same policy as the CDN API).
- GET …/assets/:id/download: an attachment named name + extension,
revalidated on every use since the name can change.
The thumbnail and the preview share one generation path; the Range
streaming moved to utils/send-stored-object.ts for both controllers. The
dialog shows /preview (a video that can't play: its thumbnail), plays from
/original and links /download; the Assets list's Download uses /download.
… form Translations and Contents share the Translate Locale dialog but not its consequence: one writes every key of the space at once as a task, the other fills the open document for review. The dialog now takes a mode and says so: - save (Translations): "…saved at once, as a task", button "Translate and Save", and a "cannot be undone" warning when Overwrite is ticked - review (Contents): "…Nothing is saved: review them, then press Save" The unused `description` option is gone. Translations now preselects the space default locale as the source and the locale the list shows as the target; with no target given the dialog starts on a locale other than the source instead of a pair it refuses.
EventController and its access rules move from shared/http to modules/events (EventStreamModule): shared/ holds what modules use, and every endpoint lives in a module. infra/events still publishes the events; the new module only serves them. No behaviour change.
SpaceExistsPipe answers 404 before the handler runs, so the SSE handler returns a plain Observable: once it returns, the stream's 200 is sent.
- GET /api/app/spaces/:spaceId/tasks/active: queued and running tasks for every role, without the file, with the latest progress kept in memory on every instance (TaskProgressService) - task row and progress events reach every role; logs, the task and its file stay with the roles of its kind - ll-active-tasks on Translations, Schemas and Assets: the running task's progress, the queued ones, what each may overwrite - the progress dialog and the export's automatic download are gone: starting a task says so, the end is a toast with Download or View
…ved progress dialog - expectTaskDone waits for "Translate Locale started" and "finished", then the API's count of finished Translate Locale tasks (a previous run's toast may still show) - TEST-PLAN: the Translate Locale and Import / Export rows describe the action bar and the toasts
…e and active tasks - translations/import-export: all translations exported, downloaded from the toast and imported into another space; one locale as key-value JSON; a key-value JSON into one locale; an invalid key fails and writes nothing; the dialog's file and locale checks; roles - schemas/import-export: export and import into another space (references by name), an import updating in place, roles - tasks/tasks: history, download again, logs, delete; the kinds each role sees; another user's running task in the action bar until it ends - fix(web): the Tasks page status icons have the status as their name; the Translations Export / Import labels point at their selects - the e2e server runs 4 tasks at once, so parallel tests don't queue behind each other; task waits up to 60 s
…arch and filters
- tasks/tasks: a second task of the same space waits ("+1 queued"), the running one shows its progress, the popover lists both; the second starts after the first finished
- tasks/tasks: search by file name and id, the Kind and Status filters, Reset
- AppApi.importTranslationsJson: a key-value JSON import as a task (multipart)
- contents/visual-editor: the hover retry moves the pointer away first (the page reports mouseenter, which a pointer already on the block never fires again); was flaky, 2 in 10
- TEST-PLAN: both rows done; open finding: Translate Locale reports no step while it saves
- drizzle/released.json: the migrations a release shipped, with the SHA-256 of their SQL (empty until the first release) - pnpm --filter @localess/server db:release records them when tagging a release; refuses a recorded migration that changed - released-migrations.test.ts: an edited, removed or reordered released migration fails; line endings alone don't count - db:regenerate refuses once a migration is released (a schema change is then a new migration)
…eports Writing while it saves - web: the schema editors' Save, Translations' Publish and value save, the detail panel's Translate reset their loading flag on error too (an error never reaches complete), as the document editor does; a unit test each - server: Translate Locale reports a Writing step around its save, as the imports do (it showed "Translating n / n" for the whole save) - e2e: translations/edit makes a Publish fail once and publishes again; the queued-task test waits for Writing - server tests: the Translate Locale helpers wait until the task ended (the worker takes it when its created event arrives, so whenIdle alone could return first; failed under load) - e2e: the Tasks roles test waits for the exact kinds; the "No tasks yet." row is not a task row - TEST-PLAN: both open findings fixed
… logic added for 500 responses
…legacy schema keys, warn about cut files Found by importing the four spaces of localess-prod.web.app and comparing every row, file and CDN answer. - Option fields that held their own values (`source: 'self'` with `options`, or no source) were dropped: this install has no such field, so the values (TrueSeducer: color and size of 54 product variants; Lessify.IO: imagePosition of 12 sections) stayed in the data but were no longer delivered. Each now gets an ENUM schema made of its values, `<schema><Field>` (a free name), and takes its options from it, with a warning. - Blocks kept the legacy `schema` key next to `_schema`. It is now removed: `_schema` is kept when present, else taken from `schema`. A `schema` unlike the block's `_schema` is a field of that name and stays. - Firebase serves `/original` from a Cloud Function, whose responses stop at 32 MB: a 104 MB video arrived as 32 MB without a word. A file whose bytes differ from its Firebase size is now a warning naming it.
… with a stage track and a run summary
- Steps on top: Connect → Choose space → Import (firebase-import-steps). Spaces are radio cards with their creation
date; one imported before is disabled with "Imported as …"; Back returns to the connection.
- The run view (firebase-import-run-header + firebase-import-progress): source space, host and a status badge, one
segment per stage; while running, the stage at work with its own bar ("n of total", or a moving bar without a total)
and what is done and next; once finished, a tile per count and Open space; once failed, the failing stage with its
error, what finished before it and what to do next. Warnings per stage in an accordion.
- The Imports history opens a past run in the same view, with a status badge per run.
…3) without its file, and go on Firebase Hosting carries at most 32 MiB of a function's answer; the Firebase side now refuses a larger file with 413 and a plain-text reason instead of a cut 200. The import failed the assets stage on it, so Localess.ORG (a 104 MB video) could not be imported at all. Now the asset is kept without its file, like one Firebase has no file for, with a warning carrying Firebase's reason and "upload it again in Assets". Any other refusal still fails the stage, its message now with Firebase's plain-text reason. Checked against localess-prod.web.app: all four spaces finish, 316 of 317 files byte-identical, the video kept without its file.
…out it; drop the tokens of the migration API An asset Firebase could not serve (too large, 413) or had no file for is imported as a row without its file. Its preview showed a broken image and nothing could fix it: a new upload made a new asset, leaving the contents on the old id. - The App API marks such a file `missingFile: true` (its md5 is empty: every other way of creating an asset stores the file with its md5). Its URLs keep answering 404 with no-cache. - PUT /api/app/spaces/:spaceId/assets/:assetId/file (CONTENT_WRITERS, multipart `file`) stores a file under the same id, so every content using it keeps working: name, folder and alt stay; type, extension, size, md5 and metadata come from the upload. Only such an asset takes a file (409 otherwise, a folder too: an asset's bytes never change); over the size limit 413, the asset still without its file and nothing left in storage. - Assets: "File missing" instead of the preview (list and cards), a warning instead of the preview dialog, and "Upload file" in its row menu and card. The import's warning points to it. The Firebase migration API no longer answers tokens (they are generated anew on this install): the fake Firebase and the test fixtures no longer carry them, and the design doc no longer lists the endpoint (D8 reworded). Checked against localess-prod.web.app: Localess.ORG imports with the 104 MB video kept without its file, shown as "File missing"; Upload file put a video into that same asset, served at once.
…rnings, duration and Open space The list of past runs was plain text lines: the full Firebase URL on every row, a status word, nothing to act on. Each run is now an Item: a status icon (spinner, check, cross), the source space with its status and warning badges, the Firebase host and who started it, when and how long it took; Open space for a finished run, Cancel for a running one, a row (or its chevron) opens the run view. A running run is followed while the list is open, so it turns Imported or Failed by itself. Host, warning count and duration are shared helpers (firebase-import.utils), also used by the run header.
Translate Locale and Regenerate Asset Metadata are told while they run in the active tasks of Translations and Assets; the Kind filter offers only the imports and exports the role may start, and no kind chosen asks for all of them. The Tasks sidebar entry is for CONTENT_WRITERS (the route stays open to TRANSLATORS for View task).
… an upload queue popover UploadQueue (core/utils) runs the uploads three at a time, each its own request, and keeps each file's state: waiting, its progress from the request's upload events, done, or why it failed with Retry and Dismiss. ll-upload-queue shows it in the action bar of the Assets page and the asset picker, replacing the "Uploading … (n)" badge. The target folder is now taken when a file is queued: a file still waiting when the user opened another folder went there instead.
…failed, drop a stray console.log On Windows a file just written is often still held and rm fails with EBUSY or EPERM: deletePrefix now retries, so a failed upload's file need not wait an hour for the sweeper. A failed upload is logged with its file, space, asset and error, and whether its file was removed: a cut request left nothing to tell why. onPaste no longer logs every event.
…e first request sharp keeps a WebP it opened by path open, so on Windows the metadata's temporary copy could not be removed (EBUSY) and every WebP upload answered 500. The metadata reads the bytes instead, and removing its temporary folder can no longer fail an upload. The thumbnail is no longer generated in the background after an upload, where it raced the first request and was written twice: it is made on the first request, and requests for the same platform image at once share one generation and one write. The assets test read a stored thumbnail by path with sharp, holding it open so its delete failed on Windows: it reads the bytes. The folder delete test sweeps until the files are gone, retrying a failed delete.
…emporary copy The temporary copies were there because the storage interface hands out no paths, to keep an S3-compatible driver possible. With the filesystem driver every image and video upload was written twice (once to storage, once for exiftool) and every video thumbnail copied the whole video for ffmpeg. StorageDriver.localPath(key) gives the stored file where the driver keeps one; withLocalFile runs a tool on it, or on a temporary copy (always removed) for a driver without files. Metadata and video frames use it. Both tools let the file go, checked on Windows with names holding spaces and accents.
…ettings Admins connect integrations by pasting a key, with no server access and no restart: "Test & enable" makes one real call before saving (a wrong key or an incomplete setup is never saved; "Save without testing" when the provider can't be reached). Environment variables still win: an env-locked integration takes all its values from env and its card is read-only. - Keys saved in the UI are sealed with AES-256-GCM (integration_settings, migration 0002) by LOCALESS_SECRET_KEY or a generated $LOCALESS_DATA_DIR/secret.key, and never sent back: only a hint. An unreadable row shows "Key unreadable" and counts as off. - IntegrationService resolves env → UI → off, cached per instance and dropped on `integrations` change events, so a change applies on every instance and, through /api/config, in every open browser without a reload. - Machine translation: a primary provider (LOCALESS_TRANSLATE_PRIMARY or the admin's choice), the other one for unsupported pairs and when the primary fails on its key, quota or network. /api/config lists the translatable locales. - Real calls keep each key's status (rejected, quota) current; the integration stays on. - localess:check reports each integration and the secret key to back up.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A Playwright end-to-end suite (
apps/e2e) that runs the production build on the built server, in CI on every push and PR, plus fixes for the bugs it found. 289 tests, about 8 minutes locally, split over two CI shards.Every test follows pre-check → action in the UI → post-check in the UI, again after a reload, and through the API (and the CDN when the action changes what it delivers). Tests own their data and run in parallel. A test fails on any app error (uncaught, reported to
ErrorHandler, or an/api5xx), and the browser can reach nothing but the server under test.flowchart LR B[e2e-build<br/>SPA + server once] --> S1[e2e shard 1/2] B --> S2[e2e shard 2/2] B --> P[e2e-postgres<br/>Postgres 18 service] S1 --> R[e2e-report<br/>merged HTML] S2 --> R D[e2e-docker<br/>image + @smoke, layer cache]A test that passes only on its retry fails the run (
failOnFlakyTests).Bugs fixed (each with a unit or server test; the e2e test stays as the regression test)
Data loss and wrong state
Wrong behaviour
Accessibility (found by the axe scans): unnamed icon buttons, selects, the image preview dialog, chip remove buttons, the asset card picture (and it opened on any key), choice-card radios, a Link switch id shared by every Link field, the Visual Editor's resize handle without a value, sidebar list structure, Settings tabs without a panel.
Open findings (recorded in TEST-PLAN.md, pinned by tests where they matter)
Evidence
auth/oauth"a disabled account is refused": after the refused sign-in,providerswas['password', 'google.com'].After:
CI=1 pnpm e2e:run→ 289 passed (8.3 min), no retries, no flakes; every changed spec stress-run 3–5× (--repeat-each).pnpm test(web unit): 1717/1717; the server OAuth suite green;pnpm build:prod,pnpm server:build,pnpm lintclean.main/developonly): every job green. E2E shards 145 + 145 passed (3.0 / 3.1 min, the setup project runs in each), merged report uploaded; external Postgres 289 passed (6.7 min); Docker image smoke 35 passed.Merge Danger
Door: two-way
App changes are small and local: accessibility attributes, a few guards and state fixes (each with a test), one server change (OAuth checks
disabledbefore linking). The rest is test code, docs and CI.Blast Radius: CI
The workflow now builds once and shares the build as an artifact across the e2e jobs; if that wiring breaks, the e2e jobs fail, not the app. App-side, the riskiest are the live-update fixes in the translation, schema and content editors (covered by unit and e2e tests).
🤖 Generated with Claude Code