Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
221 commits
Select commit Hold shift + click to select a range
3cd4801
fix(web): bugs found by the e2e suite, accessible names for icon buttons
alexcibotari Oct 15, 2026
7454657
test(e2e): Playwright end-to-end suite, test plan and CI job
alexcibotari Oct 15, 2026
5bfa829
test(e2e): Admin → Spaces and Users with pre-check, action, post-check
alexcibotari Oct 15, 2026
a82e90b
test(e2e): Me → profile with pre-check, action, post-check
alexcibotari Oct 15, 2026
026aa29
fix(web): translation labels checked per label; tree view shows the s…
alexcibotari Oct 15, 2026
0ea5594
test(e2e): Translations in English, German and Romanian
alexcibotari Oct 15, 2026
507f788
test(e2e): Assets upload, folders, rename, delete; asset-name rules t…
alexcibotari Oct 16, 2026
cf5d95a
fix(web): schema editors keep unsaved changes; schema pickers, labels…
alexcibotari Oct 16, 2026
9bc4241
test(e2e): Schemas create, fields, settings, references, enum values,…
alexcibotari Oct 16, 2026
da8f847
fix(web): content editor and list bugs found by the content e2e tests
alexcibotari Oct 16, 2026
8cb895f
test(e2e): Contents folders, lifecycle, every field kind, blocks, val…
alexcibotari Oct 16, 2026
9d88f52
test(e2e): Space settings and Webhooks with pre-check, action, post-c…
alexcibotari Oct 16, 2026
9920154
test(e2e): Assets move, grid and preview; Visual Editor block toolbar
alexcibotari Oct 16, 2026
7adf9b6
test(e2e): Management API schemas, permission matrix, user managers
alexcibotari Oct 16, 2026
afdc1e0
test(e2e): templates, accessibility scan, Google/Microsoft sign-in, e…
alexcibotari Oct 16, 2026
2a04b18
ci(e2e): run the suite on an external Postgres and the smoke suite on…
alexcibotari Oct 16, 2026
450837d
feat(e2e): product videos for the website, recorded by Playwright
alexcibotari Oct 16, 2026
d5983c1
feat(e2e): skills and Playwright Test Agents for writing e2e tests
alexcibotari Oct 16, 2026
edeab6f
fix(auth): link no provider identity to a disabled account
alexcibotari Oct 16, 2026
335ec9c
test(e2e): remove fixed waits and false greens; stricter fixtures
alexcibotari Oct 16, 2026
531936f
test(e2e): shared helpers for refusals, sessions, enums and exact text
alexcibotari Oct 16, 2026
2b184d0
ci(e2e): build once, run the suite in two shards, merge the reports
alexcibotari Oct 16, 2026
d569c94
fix(a11y): name the role select, locale table icons and the environme…
alexcibotari Oct 16, 2026
c679b01
test(e2e): admin, auth, space, me, developers, API: page objects, rel…
alexcibotari Oct 16, 2026
d9fe4d8
fix(web): name the folder and block breadcrumbs, block and link field…
alexcibotari Oct 16, 2026
6f775ee
test(e2e): contents and schemas specs by role and label, with reloads…
alexcibotari Oct 16, 2026
5cd9aeb
test(e2e): role and label locators, refusals proven via the API, relo…
alexcibotari Oct 16, 2026
116405d
test(e2e): close the editor's locale menu after a pick
alexcibotari Oct 16, 2026
c90d6ba
test(e2e): the CDN API's caching contract
alexcibotari Oct 16, 2026
1f7507d
test(e2e): cancelling a confirmation or a form dialog changes nothing
alexcibotari Oct 16, 2026
b9b5ffa
fix(a11y): name the image preview dialog, the asset card picture and …
alexcibotari Oct 16, 2026
b915972
test(e2e): open asset cards by their name; scan waits for finite anim…
alexcibotari Oct 16, 2026
d461e4b
fix(a11y): name choice-card radios and chip remove buttons; give the …
alexcibotari Oct 16, 2026
7418181
test(e2e): accessibility scan of dialogs, the Visual Editor and more …
alexcibotari Oct 16, 2026
4246d2f
ci: allow running Build and Lint by hand (workflow_dispatch)
alexcibotari Oct 16, 2026
b3e884b
fix(contents): the Asset field's file type filters the picker again
alexcibotari Oct 16, 2026
5e0e37d
test(e2e): an Asset field's file type narrows the picker
alexcibotari Oct 16, 2026
df31a3f
fix(auth): say "too many sign-in attempts" on a rate-limited sign-in
alexcibotari Oct 16, 2026
4685b67
fix(schemas): a stale live answer no longer undoes a Save in the sche…
alexcibotari Oct 16, 2026
de181ba
test(e2e): a second server with low limits: too-large upload, sign-in…
alexcibotari Oct 16, 2026
fd9f2e2
docs(roadmap): the Reference field's Path setting (to do)
alexcibotari Oct 17, 2026
4d19fcd
docs(e2e): merge the coverage skill into writing-localess-e2e-tests
alexcibotari Oct 17, 2026
d5aee94
test(e2e): retry the space switcher when a live update closes its menu
alexcibotari Oct 17, 2026
0e7ba70
feat(spaces): space templates on the server, and a large test template
alexcibotari Oct 17, 2026
a78757e
test(server): ignore connection errors of the test pool at teardown
alexcibotari Oct 17, 2026
9f395ef
test: nested deletes of contents and assets leave nothing behind
alexcibotari Oct 17, 2026
9fe1514
feat(spaces): test template "Contents (all fields)"
alexcibotari Oct 17, 2026
286f979
feat(spaces): test template "Assets (large)"
alexcibotari Oct 17, 2026
1b878d9
feat(spaces): test template "Contents (large)"; fix saving empty refe…
alexcibotari Oct 17, 2026
c81cb8a
test(e2e): retry the Visual Editor's cross-frame hover until it lands
alexcibotari Oct 17, 2026
c97e390
test: finish Contents (large) checks; roadmap for documentation recor…
alexcibotari Oct 17, 2026
1cff598
fix(e2e): never empty a database named by the shell's DATABASE_URL
alexcibotari Oct 18, 2026
c67fe6b
refactor(spaces): template ids as a shared union; titles and icons in…
alexcibotari Oct 18, 2026
f2c6e78
fix(spaces): a template needs only what creating a space needs; refus…
alexcibotari Oct 18, 2026
1e1120c
test(e2e): a user with only Space Management is offered the templates…
alexcibotari Oct 18, 2026
0cc44a2
fix(schemas): keep edits made while a schema is saving
alexcibotari Oct 18, 2026
5a6ab3a
refactor(contents): the Link field's ids take the schema-field- prefi…
alexcibotari Oct 18, 2026
213624e
refactor(web): one permission rule, the server's (canAny over canPerf…
alexcibotari Oct 18, 2026
828c580
fix(e2e): report a server that stops unexpectedly instead of exiting …
alexcibotari Oct 18, 2026
91f7043
refactor(web): accessibility fixes from the app, packages/ui back as …
alexcibotari Oct 18, 2026
030a77a
fix(contents): the server works out what a document points to, not th…
alexcibotari Oct 18, 2026
6fc3e19
test(server): check that template rows are written in chunks, not how…
alexcibotari Oct 18, 2026
b01f43f
test(spaces): template image metadata equals what an upload of the sa…
alexcibotari Oct 18, 2026
345572e
test(e2e): name what the lookup holds; roadmap for contents and asset…
alexcibotari Oct 18, 2026
64d2f46
fix(spaces): increase width of ll-column-id for better layout
alexcibotari Oct 18, 2026
c0c67a8
fix(spaces): increase width of ll-column-id for improved layout
alexcibotari Oct 18, 2026
c677caa
fix(web): single-choice menus close on a pick
alexcibotari Oct 18, 2026
782f296
docs: server-side pagination design and plan (Page<T>, Pageable; User…
alexcibotari Oct 18, 2026
50e567a
feat(shared): Page<T> and Pageable, one paging contract
alexcibotari Oct 18, 2026
1c2f473
feat(server): paging helper (offsetOf, toPage, containsPattern)
alexcibotari Oct 18, 2026
6f4e977
feat(users): GET /api/app/users returns a page (sort, search, active …
alexcibotari Oct 18, 2026
8c6650c
feat(spaces): GET /api/app/spaces returns a page (sort, search, impor…
alexcibotari Oct 18, 2026
08c460e
feat(admin): Users and Spaces page, sort and search on the server
alexcibotari Oct 18, 2026
034b492
feat(web): the space store loads every page; the switcher searches pa…
alexcibotari Oct 18, 2026
ba8e156
test(e2e): paging, sorting and search on Users and Spaces, 101 spaces…
alexcibotari Oct 18, 2026
de05b60
fix(pagination): review fixes — lists survive a failed load, spaces s…
alexcibotari Oct 18, 2026
f3b4a08
docs(pagination): round 2 design — Tasks, webhook logs, Assets, Conte…
alexcibotari Oct 19, 2026
15c7b95
feat(tasks): GET /api/app/spaces/:spaceId/tasks returns a page (newes…
alexcibotari Oct 19, 2026
831c53b
refactor(server): request schemas move from controllers to each modul…
alexcibotari Oct 19, 2026
8d01523
refactor(server): validate with NestJS StandardSchemaValidationPipe (…
alexcibotari Oct 19, 2026
1231a5b
refactor(server): NestJS method names in controllers and services (fi…
alexcibotari Oct 19, 2026
48341c6
refactor(server): singular file and class names (space.controller.ts,…
alexcibotari Oct 19, 2026
da4d13d
refactor(server): one controller, service and repository per entity a…
alexcibotari Oct 19, 2026
c69123a
docs(server): server rules in docs/server/ (architecture, organisatio…
alexcibotari Oct 19, 2026
2b5d888
refactor: App API request models defined once in @localess/shared (zX…
alexcibotari Oct 19, 2026
5476de8
refactor(server): database entities in shared/entities with relations…
alexcibotari Oct 19, 2026
66fa394
refactor: zBoolean for booleans sent as text (exactly true / false)
alexcibotari Oct 19, 2026
e9120d3
refactor(server): injected services and repositories named after thei…
alexcibotari Oct 19, 2026
f3428af
refactor(spaces): repositories query, services shape; models mirror t…
alexcibotari Oct 19, 2026
841bfb9
test(e2e): spaces helpers read spaceLocales and the environments endp…
alexcibotari Oct 19, 2026
12291ce
docs: Spaces module readability design and plan; repository read shap…
alexcibotari Oct 19, 2026
ae43d75
refactor: optimize existence checks in repositories using $count and …
alexcibotari Oct 19, 2026
28ed582
refactor: introduce IdName type for space names and simplify findAllN…
alexcibotari Oct 19, 2026
81aa61e
refactor: update space switcher search box visibility threshold from …
alexcibotari Oct 19, 2026
5e3e24e
refactor: standardize space template IDs to lowercase format
alexcibotari Oct 19, 2026
468a38e
refactor: enhance space locale management with pagination and sorting…
alexcibotari Oct 19, 2026
207a9ba
refactor: implement setDefaultLocale method for managing space defaul…
alexcibotari Oct 19, 2026
2b5bc2f
refactor: update space service methods to use findAll and findAllName…
alexcibotari Oct 19, 2026
8e092c0
fix(spaces): environments sort by name and URL, not always by position
alexcibotari Oct 19, 2026
5b60da7
refactor(spaces): SpaceService.update for PATCH /spaces/:spaceId (was…
alexcibotari Oct 19, 2026
5738bd8
refactor(spaces): services take the request model, not its fields
alexcibotari Oct 19, 2026
7c9ca19
fix(spaces): UuidParamPipe on :spaceId; removing a locale the space d…
alexcibotari Oct 19, 2026
49bfc63
refactor(spaces): SpaceLocaleController mounted at /spaces/:spaceId/l…
alexcibotari Oct 19, 2026
3deabac
refactor(spaces): locale requests send localeId / defaultLocaleId, th…
alexcibotari Oct 19, 2026
af9822d
docs(server): IdNameUrl in the response-model rule, for the …/names l…
alexcibotari Oct 19, 2026
a8e4983
fix(server): narrow reads typed as Pick of their entity; id-name.enti…
alexcibotari Oct 19, 2026
5d7888c
fix(spaces): updating an environment is PUT (it replaces name and URL…
alexcibotari Oct 19, 2026
6713fd6
fix(spaces): request bodies refuse unknown fields (z.strictObject); q…
alexcibotari Oct 19, 2026
d601942
refactor: zSearch, the ?search= of every paged list, defined once in …
alexcibotari Oct 19, 2026
2184b6f
perf(spaces): existence checks count the space instead of loading it …
alexcibotari Oct 20, 2026
f2dec73
refactor(spaces): locales and environments move one item to a positio…
alexcibotari Oct 20, 2026
f4eae42
feat(spaces): a move to position -1 takes the locale or environment t…
alexcibotari Oct 20, 2026
64cd9b3
fix(spaces): changes of a space run under its row lock; the default l…
alexcibotari Oct 20, 2026
09f7e19
refactor(spaces): counts and existence with $count, reads as relation…
alexcibotari Oct 20, 2026
8266c0a
style: format the repository with Prettier (no code change)
alexcibotari Oct 20, 2026
d3dda59
chore: Prettier for the whole repository; checked in CI
alexcibotari Oct 20, 2026
4b68a46
refactor(server): drop type assertions the compiler does not need (no…
alexcibotari Oct 20, 2026
1f675f3
fix(server): a caught value is logged as its message (errorMessage), …
alexcibotari Oct 20, 2026
1af229a
refactor(server): enum columns compared with their literal; typed arr…
alexcibotari Oct 20, 2026
e77d815
chore(server): ESLint, type-aware, in `pnpm lint` and CI
alexcibotari Oct 20, 2026
4681a19
refactor(e2e): drop two unused imports and three assertions the compi…
alexcibotari Oct 20, 2026
b48e3ad
style: sort imports and exports (simple-import-sort) in server, share…
alexcibotari Oct 20, 2026
f820fb2
chore: ESLint for packages/shared and apps/e2e; simple-import-sort in…
alexcibotari Oct 20, 2026
b0ac030
refactor(spaces): API tokens are a space sub-entity (SpaceToken contr…
alexcibotari Oct 20, 2026
89df966
fix(spaces): tokens check their space (UuidParamPipe, 404) and page o…
alexcibotari Oct 20, 2026
d7c6647
refactor(spaces): the token repository only queries (findOne, writes …
alexcibotari Oct 20, 2026
9d65f1a
feat(server): one role per user, @RequireRole(role | list); permissio…
alexcibotari Oct 20, 2026
6489dc9
feat(web): roles in the UI: roleGuard, the hasRole pipe, a role selec…
alexcibotari Oct 20, 2026
198c3c1
docs: user roles replace permissions (frontend-permissions.md rewritt…
alexcibotari Oct 20, 2026
fcdaa3d
docs: role leftovers (SPACE_MANAGEMENT comments, test names, the e2e …
alexcibotari Oct 21, 2026
7a00ce1
fix(webhooks): check the space first (UuidParamPipe, 404), :webhookId…
alexcibotari Oct 21, 2026
60855e8
feat(webhooks): the webhook list is paged, sorted, filtered and searc…
alexcibotari Oct 21, 2026
aa90065
feat(webhooks): delivery logs are paged, filtered and searched on the…
alexcibotari Oct 21, 2026
a5dc975
refactor(webhooks): the webhook repository only queries (findOne, exi…
alexcibotari Oct 21, 2026
b520a46
feat(webhooks): typed transformers and declared return types; the sec…
alexcibotari Oct 21, 2026
a829a0a
feat(webhooks): signing is optional, the secret is generated by the s…
alexcibotari Oct 21, 2026
75488e4
feat(webhooks): custom headers in the webhook form, their values writ…
alexcibotari Oct 21, 2026
4129bc4
feat(webhooks): the URL's query values are never answered, logged nor…
alexcibotari Oct 21, 2026
4bd4d9d
refactor: WebHook → Webhook in every name (shared models, zod schemas…
alexcibotari Oct 21, 2026
0e37e1e
chore: git blame skips the WebHook → Webhook rename
alexcibotari Oct 21, 2026
f253327
feat(web): a Signing column in the webhook list
alexcibotari Oct 21, 2026
fa6a6e8
feat(web): the user role as choice cards, each with what the role may…
alexcibotari Oct 21, 2026
923395e
refactor(users): one User response model in @localess/shared, the row…
alexcibotari Oct 21, 2026
dc53eaa
fix(users): strict request bodies for users, Me and sign-in; zod 4 em…
alexcibotari Oct 21, 2026
98b0195
refactor(users): one name per action in every layer; services take th…
alexcibotari Oct 21, 2026
edb7899
refactor(users): the user repositories only query (findAll / findAllC…
alexcibotari Oct 21, 2026
ae991da
refactor(users): one way to read the signed-in user (GET /api/auth/me…
alexcibotari Oct 21, 2026
4282069
chore(users): the users module joins REWORKED (return types and $coun…
alexcibotari Oct 21, 2026
57c58db
feat(auth): every route declares who may call it; the guard fails clo…
alexcibotari Oct 21, 2026
19bd899
refactor(webhooks): the delivery payload is defined once in @localess…
alexcibotari Oct 21, 2026
a4a6089
refactor(web): the content editor's templates type-check (typed contr…
alexcibotari Oct 21, 2026
0726f4c
refactor(web): schemas and tokens look descriptions up by a typed key…
alexcibotari Oct 21, 2026
a68042c
feat(web): templates are type-checked strictly (strictTemplates); two…
alexcibotari Oct 21, 2026
5fa50b0
refactor(styles): update column classes for consistency and clarity i…
alexcibotari Oct 21, 2026
89b242f
refactor(users): rename invite functionality to add user for clarity …
alexcibotari Oct 21, 2026
c1e02dd
fix(translations): every route checks its space first (UuidParamPipe,…
alexcibotari Oct 21, 2026
f5ceb5d
fix(translations): strict App API bodies (translations and machine tr…
alexcibotari Oct 21, 2026
e3e0cbd
refactor(translations): one name per action across layers (web servic…
alexcibotari Oct 21, 2026
49ba828
refactor(translations): the repositories only query (relational reads…
alexcibotari Oct 21, 2026
324d2bb
refactor(translations): a typed transformer, shared answer models and…
alexcibotari Oct 21, 2026
3b94e5a
chore(translations): the translations module joins REWORKED; history …
alexcibotari Oct 21, 2026
cfa61c6
feat(translations): paged list and a tree read by level, from the server
alexcibotari Oct 21, 2026
e65749a
feat(web): search on Enter or the Search button, and show when it isn…
alexcibotari Oct 21, 2026
c7e1a8e
fix(web): Translations fits small windows; the sidebar collapses belo…
alexcibotari Oct 21, 2026
3463d0d
feat(translations): the list loads more as it scrolls; the tree's "Lo…
alexcibotari Oct 21, 2026
9532c91
feat(translations): the details open in a sheet from the right when t…
alexcibotari Oct 21, 2026
199c0cd
fix(web): narrow tables keep their Actions column; declared sheets ge…
alexcibotari Oct 21, 2026
2629d7f
fix(translations): the tree selects the first key it shows; a key out…
alexcibotari Oct 22, 2026
515112f
fix(schemas): every route checks its space first (UuidParamPipe, 404)…
alexcibotari Oct 22, 2026
302b90f
refactor(schemas): the schema repository only queries (findOne, findU…
alexcibotari Oct 22, 2026
00805e0
refactor(schemas): the Schemas page reads SpaceStore's schemas (one l…
alexcibotari Oct 22, 2026
1464a41
refactor(schemas): the web SchemaService carries the controller's nam…
alexcibotari Oct 22, 2026
ba005d8
docs(e2e): the schema editors' Save shows the schema the server answe…
alexcibotari Oct 22, 2026
9bfb012
fix(contents): every route checks its space first (UuidParamPipe, 404…
alexcibotari Oct 22, 2026
3b5769b
refactor(contents): the content repository only queries (findOne, fin…
alexcibotari Oct 22, 2026
a4689e1
docs(roadmap): contents in SpaceStore patched from change events (TOD…
alexcibotari Oct 22, 2026
be87c97
feat(contents): the content list is paged on the server (ContentPagea…
alexcibotari Oct 22, 2026
8b9d01e
feat(contents): the Contents table and the References picker page, so…
alexcibotari Oct 22, 2026
0d49555
feat(contents): SpaceStore holds a light list of every folder and doc…
alexcibotari Oct 22, 2026
2ba1216
feat(contents): the Move dialog is a folder tree; ll-tree can select …
alexcibotari Oct 22, 2026
cc14fbe
refactor(contents): the web ContentService carries the controller's n…
alexcibotari Oct 22, 2026
da53423
fix(contents): a narrow window keeps the Actions menu and the editor'…
alexcibotari Oct 22, 2026
b5d1cd0
fix(assets): every route checks its space first, then the asset; ids …
alexcibotari Oct 22, 2026
656bd07
refactor(assets): the repository only queries; writes answer nothing …
alexcibotari Oct 22, 2026
cc9878c
feat(assets): the asset list is paged on the server (AssetPageable): …
alexcibotari Oct 22, 2026
50f6318
feat(assets): the Assets page and the asset picker page, sort and sea…
alexcibotari Oct 22, 2026
8e99da4
feat(store): SpaceStore holds light lists of content folders and docu…
alexcibotari Oct 22, 2026
c54b243
refactor(assets): the web AssetService carries the controller's names…
alexcibotari Oct 22, 2026
bca6e19
feat(assets): the Move dialog is a folder tree, as for contents; the …
alexcibotari Oct 22, 2026
23d8625
fix(web): opening a folder sends one request; pages load with pagedRe…
alexcibotari Oct 23, 2026
069ca80
refactor(services): rename methods for consistency; update findPage t…
alexcibotari Oct 23, 2026
abf5acb
refactor(plugins): the Unsplash module joins REWORKED; response model…
alexcibotari Oct 23, 2026
4dd8465
refactor(tasks): the Tasks module joins REWORKED; findAll + findAllCo…
alexcibotari Oct 23, 2026
79e8744
refactor(firebase-import): update method signatures for consistency; …
alexcibotari Oct 23, 2026
aa0b08b
feat(docs): add section on space export and backups to roadmap
alexcibotari Oct 23, 2026
1db82dd
refactor(dialogs): remove unused import and export dialogs; add task …
alexcibotari Oct 23, 2026
4d43b7c
feat(assets): store files under their original name; platform thumbna…
alexcibotari Oct 24, 2026
1b061b7
fix(assets): video thumbnails with an ffmpeg built without WebP
alexcibotari Oct 24, 2026
ffe3043
feat(assets): the platform reads assets through the App API only
alexcibotari Oct 24, 2026
76e81cb
feat(translate-locale): the dialog says whether it saves or fills the…
alexcibotari Oct 25, 2026
c9165ec
refactor(server): the SSE change stream is its own module
alexcibotari Oct 25, 2026
d4c88a2
feat(events): implement space-specific event streams and refactor eve…
alexcibotari Oct 25, 2026
94be640
refactor(events): a space's stream checks the space in a pipe
alexcibotari Oct 25, 2026
54b5b49
feat(tasks): active tasks in the action bar replace the progress dialog
alexcibotari Oct 25, 2026
47a309f
test(e2e): Translate Locale waits for the task's toasts, not the remo…
alexcibotari Oct 25, 2026
ac3c0af
test(e2e): import / export of translations and schemas, the Tasks pag…
alexcibotari Oct 25, 2026
78cc72f
test(e2e): queued tasks and progress in the action bar, Tasks page se…
alexcibotari Oct 25, 2026
204c198
test(server): released migrations stay as they shipped
alexcibotari Oct 25, 2026
2a8f0dc
fix: buttons stop spinning after a failed request; Translate Locale r…
alexcibotari Oct 25, 2026
4547f4a
feat: enhance asset import handling for Firebase server errors; retry…
alexcibotari Oct 26, 2026
9bd0692
fix(firebase-import): keep option fields with their own values, drop …
alexcibotari Oct 26, 2026
3d99a6d
feat(admin): redesign the Import from Firebase dialog in three steps,…
alexcibotari Oct 26, 2026
9efe3ea
fix(firebase-import): keep an asset Firebase refuses as too large (41…
alexcibotari Oct 26, 2026
1fa7a21
feat(assets): upload the file of an asset imported from Firebase with…
alexcibotari Oct 26, 2026
b21e235
feat(admin): the Imports from Firebase list as Spartan Items, with wa…
alexcibotari Oct 26, 2026
683b216
feat(tasks): list only imports and exports on the Tasks page
alexcibotari Oct 26, 2026
e0968ea
feat(assets): upload three files at a time, with per-file progress in…
alexcibotari Oct 26, 2026
1287f5d
fix(assets): retry the cleanup of a failed upload, log why an upload …
alexcibotari Oct 26, 2026
eb5fb62
fix(assets): WebP uploads on Windows, thumbnails generated once on th…
alexcibotari Oct 26, 2026
48fc18a
perf(assets): exiftool and ffmpeg read the stored file in place, no t…
alexcibotari Oct 26, 2026
25d42e6
feat(integrations): connect DeepL, Google and Unsplash from Admin → S…
alexcibotari Oct 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 7 additions & 0 deletions .agents/skills/playwright-best-practices/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright © 2026 Currents Software Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
303 changes: 303 additions & 0 deletions .agents/skills/playwright-best-practices/SKILL.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions .agents/skills/playwright-best-practices/SOURCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Source

Vendored from https://github.com/currents-dev/playwright-best-practices-skill (MIT, see LICENSE.md),
commit `283d5cbc5d11aac1abda058b16ad22c317d54dc0` (2026-07-21), folder `playwright-best-practices/`. Unchanged.

To update: copy that folder again from a newer commit and change this note. Localess-specific rules live in
`writing-localess-e2e-tests`, which takes precedence where the two disagree.
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
# Complex Authentication Flow Patterns

## Table of Contents

1. [Email Verification Flows](#email-verification-flows)
2. [Password Reset](#password-reset)
3. [Session Timeout](#session-timeout)
4. [Remember Me Persistence](#remember-me-persistence)
5. [Logout Patterns](#logout-patterns)
6. [Tips](#tips)
7. [Related](#related)

> **When to use**: Testing email verification, password reset, session timeout/expiration, or remember-me functionality. For basic auth setup (storage state, OAuth mocking, MFA, role-based access), see [authentication.md](authentication.md).
---

## Email Verification Flows

### Capturing Verification Tokens

Intercept API responses to capture verification tokens for testing:

```typescript
test('completes registration with email verification', async ({ page }) => {
let capturedToken = '';

await page.route('**/api/auth/register', async (route) => {
const response = await route.fetch();
const body = await response.json();
capturedToken = body.verificationToken;
await route.fulfill({ response });
});

await page.goto('/register');
await page.getByLabel('Name').fill('New User');
await page.getByLabel('Email').fill('newuser@test.com');
await page.getByLabel('Password', { exact: true }).fill('SecurePass!');
await page.getByLabel('Confirm password').fill('SecurePass!');
await page.getByRole('button', { name: 'Create account' }).click();

await expect(page.getByText('Check your inbox')).toBeVisible();

expect(capturedToken).toBeTruthy();
await page.goto(`/verify?token=${capturedToken}`);

await expect(page.getByText('Email confirmed')).toBeVisible();
});
```

### Fully Mocked Verification

```typescript
test('verifies email with mocked endpoints', async ({ page }) => {
const mockToken = 'test-verification-abc123';

await page.route('**/api/auth/register', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ message: 'Verification sent', verificationToken: mockToken }),
});
});

await page.route(`**/api/auth/verify?token=${mockToken}`, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ verified: true }),
});
});

await page.goto('/register');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password', { exact: true }).fill('Password123!');
await page.getByRole('button', { name: 'Sign up' }).click();

await expect(page.getByText('Check your inbox')).toBeVisible();

await page.goto(`/verify?token=${mockToken}`);
await expect(page.getByText('Email confirmed')).toBeVisible();
});
```

---

## Password Reset

### Complete Reset Flow

```typescript
test('resets password through email link', async ({ page }) => {
let resetToken = '';

await page.route('**/api/auth/forgot-password', async (route) => {
const response = await route.fetch();
const body = await response.json();
resetToken = body.resetToken;
await route.fulfill({ response });
});

await page.goto('/forgot-password');
await page.getByLabel('Email').fill('user@test.com');
await page.getByRole('button', { name: 'Send link' }).click();

await expect(page.getByText('Reset email sent')).toBeVisible();

expect(resetToken).toBeTruthy();
await page.goto(`/reset-password?token=${resetToken}`);

await page.getByLabel('New password', { exact: true }).fill('NewPassword456!');
await page.getByLabel('Confirm password').fill('NewPassword456!');
await page.getByRole('button', { name: 'Update password' }).click();

await expect(page.getByText('Password updated')).toBeVisible();
});
```

### Expired Token Handling

```typescript
test('shows error for expired reset token', async ({ page }) => {
await page.goto('/reset-password?token=expired-token');

await page.getByLabel('New password', { exact: true }).fill('NewPass!');
await page.getByLabel('Confirm password').fill('NewPass!');
await page.getByRole('button', { name: 'Update password' }).click();

await expect(page.getByRole('alert')).toContainText(/expired|invalid/i);
});
```

### Password Strength Validation

```typescript
test('enforces password requirements on reset', async ({ page }) => {
await page.goto('/reset-password?token=valid-token');

await page.getByLabel('New password', { exact: true }).fill('weak');
await page.getByLabel('Confirm password').fill('weak');
await page.getByRole('button', { name: 'Update password' }).click();

await expect(page.getByText(/at least 8 characters/i)).toBeVisible();
});
```

---

## Session Timeout

### Detecting Expired Sessions

```typescript
test('redirects to signin after session expires', async ({ page, context }) => {
await page.goto('/signin');
await page.getByLabel('Email').fill('user@test.com');
await page.getByLabel('Password').fill('Password!');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL('/home');

const cookies = await context.cookies();
const sessionCookie = cookies.find((c) => c.name.includes('session'));

if (sessionCookie) {
await context.clearCookies({ name: sessionCookie.name });
}

await page.goto('/profile');
await expect(page).toHaveURL(/\/signin/);
await expect(page.getByText(/session.*expired|sign in again/i)).toBeVisible();
});
```

### Session Extension Warning

```typescript
test('shows warning before session expires', async ({ page }) => {
await page.route('**/api/auth/session', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ valid: true, expiresIn: 60 }),
});
});

await page.goto('/home');

await expect(page.getByText(/session.*expir/i)).toBeVisible({ timeout: 10000 });
await expect(page.getByRole('button', { name: /extend|stay signed in/i })).toBeVisible();
});
```

### Session Extension Action

```typescript
test('extends session when user clicks extend', async ({ page }) => {
let sessionExtended = false;

await page.route('**/api/auth/session', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ valid: true, expiresIn: 60 }),
});
});

await page.route('**/api/auth/refresh', async (route) => {
sessionExtended = true;
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ valid: true, expiresIn: 3600 }),
});
});

await page.goto('/home');

await expect(page.getByRole('button', { name: /extend|stay signed in/i })).toBeVisible({
timeout: 10000,
});
await page.getByRole('button', { name: /extend|stay signed in/i }).click();

expect(sessionExtended).toBe(true);
await expect(page.getByText(/session.*expir/i)).not.toBeVisible();
});
```

---

## Remember Me Persistence

### Persistent Session

```typescript
test('persists session with remember me enabled', async ({ browser }) => {
const ctx1 = await browser.newContext();
const page1 = await ctx1.newPage();

await page1.goto('/signin');
await page1.getByLabel('Email').fill('user@test.com');
await page1.getByLabel('Password').fill('Password!');
await page1.getByLabel('Keep me signed in').check();
await page1.getByRole('button', { name: 'Sign in' }).click();

await expect(page1).toHaveURL('/home');

const state = await ctx1.storageState();
await ctx1.close();

const ctx2 = await browser.newContext({ storageState: state });
const page2 = await ctx2.newPage();

await page2.goto('/home');
await expect(page2).toHaveURL('/home');
await expect(page2.getByText('Welcome')).toBeVisible();

await ctx2.close();
});
```

### Session-Only Login

```typescript
test('session-only login does not persist across browser restarts', async ({ browser }) => {
const ctx1 = await browser.newContext();
const page1 = await ctx1.newPage();

await page1.goto('/signin');
await page1.getByLabel('Email').fill('user@test.com');
await page1.getByLabel('Password').fill('Password!');
// Leave "Remember me" unchecked
await expect(page1.getByLabel('Keep me signed in')).not.toBeChecked();
await page1.getByRole('button', { name: 'Sign in' }).click();

await expect(page1).toHaveURL('/home');

// Only keep persistent cookies (filter out session cookies)
const cookies = await ctx1.cookies();
await ctx1.close();

const persistentCookies = cookies.filter((c) => c.expires > 0);
const ctx2 = await browser.newContext();
await ctx2.addCookies(persistentCookies);
const page2 = await ctx2.newPage();

await page2.goto('/home');

// Should redirect to login since session was not persisted
await expect(page2).toHaveURL(/\/signin/);

await ctx2.close();
});
```

---

## Logout Patterns

### Standard Logout with Session Cleanup

```typescript
test.use({ storageState: '.auth/user.json' });

test('logs out and clears session', async ({ page, context }) => {
await page.goto('/home');

await page.getByRole('button', { name: /account|menu/i }).click();
await page.getByRole('menuitem', { name: 'Sign out' }).click();

await expect(page).toHaveURL('/signin');

const cookies = await context.cookies();
const sessionCookies = cookies.filter((c) => c.name.includes('session') || c.name.includes('token'));
expect(sessionCookies).toHaveLength(0);

await page.goto('/home');
await expect(page).toHaveURL(/\/signin/);
});
```

### Logout from All Devices

```typescript
test('logs out from all devices', async ({ page }) => {
let logoutAllCalled = false;

await page.route('**/api/auth/logout-all', async (route) => {
logoutAllCalled = true;
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ message: 'Logged out everywhere' }),
});
});

await page.goto('/settings/security');

await page.getByRole('button', { name: 'Sign out everywhere' }).click();
await page.getByRole('dialog').getByRole('button', { name: 'Confirm' }).click();

expect(logoutAllCalled).toBe(true);
await expect(page).toHaveURL(/\/signin/);
});
```

---

## Tips

1. **Configure shorter session timeouts in test environments** — Enables testing timeout behavior without slow tests
2. **Test token expiration edge cases** — Expired tokens, invalid tokens, already-used tokens
3. **Verify cleanup on logout** — Check both cookies and localStorage are cleared
4. **Test the full flow end-to-end** — Password reset should verify login with new password works

---

## Related

- [authentication.md](authentication.md) — Storage state, OAuth mocking, MFA, role-based access, API login
- [fixtures-hooks.md](../core/fixtures-hooks.md) — Creating auth fixtures
- [third-party.md](./third-party.md) — Mocking external auth providers
Loading