From e8d5210015a522f22f2a9f55028f2aac5389290d Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Sat, 5 Sep 2026 14:59:19 +0100 Subject: [PATCH 1/7] [DREAM-821] Correct plugin frontend linking docs Plugins symlink into `features/plugins/linked/` and the generated module is `linked-plugins.module.ts`; both paths still named the `modules/` tree they were moved out of. Costs is a bundled module loaded from `Gemfile.modules` rather than an external plugin to clone, and the repository it linked has been archived since 2018. The example reached for lodash, declared a `params` argument the hook never passes, set a `resource` key nothing reads, and named its types without importing them, so it could not be copied into a plugin entry point. It now compiles: extracted from the markdown and run through `tsc -p tsconfig.app.json`. https://community.openproject.org/wp/DREAM-821 --- frontend/doc/PLUGINS.md | 76 +++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/frontend/doc/PLUGINS.md b/frontend/doc/PLUGINS.md index 466d79830eaa..074ba4d81315 100644 --- a/frontend/doc/PLUGINS.md +++ b/frontend/doc/PLUGINS.md @@ -1,8 +1,7 @@ Rails plugins with Frontends ==================== -OpenProject Community edition has some plugins that contain a frontend, -e.g., [Costs](https://github.com/finnlabs/openproject-costs/) or [My Project Page](https://github.com/finnlabs/openproject-my_project_page/). +Some OpenProject modules ship a frontend of their own. Costs is the worked example below; it lives in this repository at `modules/costs` and is loaded from `Gemfile.modules`, not cloned separately. For developing these plugins, they need to be linked so either the Legacy or Angular frontend can see and process them. @@ -12,17 +11,21 @@ For developing these plugins, they need to be linked so either the Legacy or Ang -To install a plugin, you clone it locally and place it into your `Gemfile.plugins` like so: +Modules bundled with OpenProject are already listed in `Gemfile.modules` and need no installation step: +```ruby +gem 'costs', path: 'modules/costs' ``` + +A plugin developed outside this repository is added to `Gemfile.plugins` instead, pointing at wherever you cloned it: + +```ruby group :opf_plugins do - gem 'openproject-costs', path: '../plugins/openproject-costs' + gem 'openproject-my-plugin', path: '../plugins/openproject-my-plugin' end ``` - - -After that you first need to bundle the application with `bundle install`. +After changing either file, run `bundle install`. The plugin is now known in the OpenProject application, but their frontends are not linked. For development, before you run any frontend build or `CLI` commands, execute this rake task: @@ -34,13 +37,13 @@ The plugin is now known in the OpenProject application, but their frontends are -This will ensure those plugins with a frontend are symlinked to `frontend/src/app/modules/plugins/linked/` for plugins with an exported Angular module under `frontend/module/main.ts`. +This will ensure those plugins with a frontend are symlinked to `frontend/src/app/features/plugins/linked/` for plugins with an exported Angular module under `frontend/module/main.ts`. -### Example: OpenProject Costs plugin +### Example: the Costs module -The [Costs](https://github.com/finnlabs/openproject-costs/) plugin has both legacy components that are still used by Rails templates as well as an entry module file to register to the Angular frontend. +The Costs module (`modules/costs`) has both legacy components that are still used by Rails templates as well as an entry module file to register to the Angular frontend. Let's take a look at the file structure of the costs folder `frontend/`: @@ -55,32 +58,31 @@ module The Angular frontend entry point is `frontend/module/main.ts` and should export a `PluginModule` ngModule that looks like the following: ```typescript +import { NgModule } from '@angular/core'; +import { OpenProjectPluginContext } from 'core-app/features/plugins/plugin-context'; +import { WorkPackageAction } from 'core-app/features/work-packages/components/wp-table/context-menu-helper/wp-context-menu-helper.service'; + export function initializeCostsPlugin() { - window.OpenProject.getPluginContext() - .then((pluginContext:OpenProjectPluginContext) => { - // Register a field type to the core EditField functionality - pluginContext.services.editField.extendFieldType('select', ['Budget']); - - // Register a hook callback for a specific core hook - pluginContext.hooks.workPackageSingleContextMenu(function(params:any) { - return { - key: 'log_costs', - icon: 'icon-projects', - indexBy: function(actions:any) { - var index = _.findIndex(actions, {key: 'log_time'}); - return index !== -1 ? index + 1 : actions.length; - }, - resource: 'workPackage', - link: 'logCosts' - }; - }); - }); + window.OpenProject.getPluginContext() + .then((pluginContext:OpenProjectPluginContext) => { + // Register a field type to the core EditField functionality + pluginContext.services.editField.extendFieldType('select', ['Budget']); + + // Register a hook callback for a specific core hook + pluginContext.hooks.workPackageSingleContextMenu(():WorkPackageAction => ({ + key: 'log_costs', + icon: 'icon-projects', + indexBy: (actions:WorkPackageAction[]) => { + const index = actions.findIndex((action) => action.key === 'log_time'); + return index !== -1 ? index + 1 : actions.length; + }, + link: 'logCosts', + })); + }); } - @NgModule({ - providers: [ - ], + providers: [], }) export class PluginModule { // The name PluginModule is important! constructor() { @@ -89,6 +91,14 @@ export class PluginModule { // The name PluginModule is important! } ``` +`workPackageSingleContextMenu` is invoked without arguments, so its callback takes none. `indexBy` +receives the actions collected so far and returns the position the new entry should take. + +`hooks` is typed as `Record unknown) => void>`, so a callback +returning anything at all compiles. Annotating the return as `WorkPackageAction` is what makes a +wrong key an error rather than a silent no-op — the consuming code casts the collected results to +`WorkPackageAction[]` regardless. +A working implementation of this pattern lives at `modules/costs/frontend/module/main.ts`. -The rake task will generate a Module under `frontend/src/app/modules/plugins/linked-plugin-module.ts` that will import all these plugin modules. This happens by filling an ERB template by the rake task and is performed in `lib/open_project/plugins/frontend_linking/*` +The rake task will generate a module under `frontend/src/app/features/plugins/linked-plugins.module.ts` that will import all these plugin modules. This happens by filling an ERB template by the rake task and is performed in `lib/open_project/plugins/frontend_linking/*` From 5739546b446acb8c7d7664ef7dcb79cbb297f772 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Sat, 5 Sep 2026 14:59:20 +0100 Subject: [PATCH 2/7] [DREAM-821] Refresh the frontend README The Living Styleguide is gone: no route serves it, `styleguide.html` is absent and no `.lsg` partials remain, so `STYLING.md` documented nothing that still exists. Lookbook covers components and styling now. Neither build command was valid, tests no longer run on karma-jasmine, the styles pointer named a directory that does not exist, and the lockfile claim contradicted the lockfile. The dev server is described as what it is: a proxy for two paths, not a catch-all, so contributors know to reach application pages through Rails. https://community.openproject.org/wp/DREAM-821 --- frontend/doc/README.md | 57 ++++++++++++++++++++++------------------- frontend/doc/STYLING.md | 29 --------------------- 2 files changed, 30 insertions(+), 56 deletions(-) delete mode 100644 frontend/doc/STYLING.md diff --git a/frontend/doc/README.md b/frontend/doc/README.md index 73e18d56b2c0..be29d8d0aad9 100644 --- a/frontend/doc/README.md +++ b/frontend/doc/README.md @@ -2,58 +2,57 @@ The OpenProject frontend is located at `frontend/src` and uses the Angular CLI (which itself uses Vite in dev and esbuild in production) to compile and serve locally. +## Directory structure + +- `src/app/` — the legacy Angular application. New work does not go here. +- `src/common/` — framework-agnostic modules, reachable through the `core-common` alias and importable from both Angular and Stimulus. Code belongs here when it depends on neither framework and both sides need it; a helper only Stimulus controllers use belongs in `src/stimulus/helpers/` instead. +- `src/stimulus/` — Stimulus controllers, the current approach for new behaviour. +- `src/turbo/` — Turbo integration: stream actions, custom elements and helpers. + ## Angular frontend -When developing, `npm run serve` will open the Angular CLI dev server (Vite) as a proxy that serves assets from memory. -That server is running on `http://localhost:4200` by default and will forward all requests it cannot handle to the Rails server -which it expects to run at `http://localhost:3000`. +When developing, `npm run serve` starts the Angular CLI dev server (Vite) on `http://localhost:4200`, serving frontend assets from memory. It is not a general-purpose proxy: it forwards only `/api` and `/assets/frontend/media` to Rails, as configured in `frontend/src/proxy.conf.mjs`. -You can always access the Rails server with `http://localhost:3000` -and it will forward the requests to the CLI Proxy unless an empty ENV variable `OPENPROJECT_CLI_PROXY='''` is passed to it. +Application pages are reached through Rails at `http://localhost:3000`, which forwards frontend asset requests to the dev server unless `OPENPROJECT_CLI_PROXY=''` is passed to it. Then, Rails will try to locate the asset on disk (e.g., as output from the `rake assets:precompile` task). This is also what happens in production mode. To learn more about how this behavior works in detail, see the asset helper at `app/helpers/frontend_asset_helper.rb`. -### Ahead-of-Time compilation (AOT) - -In development, by default AOT is disabled. You can force it in by running `npm run serve --aot`. -For production builds with `ng build --prod`, `--aot` is enabled by default as per the `frontend/angular.json` configuration. - ### Production builds -Production builds can be triggered either through the `rake assets:precompile` rake task (which will compile legacy and angular frontend) -or by running `npm build --prod` manually. +Production builds are triggered either through the `rake assets:precompile` rake task, which compiles +the frontend along with the rest of the assets, or by running `npm run build` manually. That script +builds with `--configuration production`, which enables ahead-of-time compilation. ## Tests -Tests are run with karma-jasmine through the Angular CLI `ng test`. To watch the test output, use `ng test --watch`. -Only files ending with `.spec.ts` will be matched and compiled. +Tests run with `npm run test`, which uses the Angular CLI's `ng test` to build the specs and +[Vitest](https://vitest.dev/) to execute them in a real browser. Use `npm run test:watch` to watch. +Only files ending with `.spec.ts` are matched. For more information, see [TESTING](./TESTING.md). ## Plugins OpenProject Community edition has some plugins that contain a frontend, -e.g., [Costs](https://github.com/finnlabs/openproject-costs/) -or [My Project Page](https://github.com/finnlabs/openproject-my_project_page/). +e.g., Costs, which lives in this repository at `modules/costs`. For developing these plugins, they need to be linked so either the Legacy or Angular frontend can see and process them. For more information on that part, see [PLUGINS](./PLUGINS.md) -## Living Style Guide +## Components and styling -The style guide is available as part of the Rails development server at: . +Component documentation and previews live in Lookbook, served by the Rails development server at when `lookbook_enabled` is set. See [Design system](https://www.openproject.org/docs/development/design-system/) for Primer and the component library. -For more information on styling the application, see [STYLING](./STYLING.md). +Styles live under `frontend/src`: shared rules in `global_styles`, component styles beside their components, with `styles.scss` and `spot.scss` as the build entry points declared in `frontend/angular.json`. ## Changing or updating Dependencies -We use a `package-lock` to lock down runtime (but not development) -dependencies. When adding or removing dependencies, please use `npm install` to also update the lockfile. +We use a `package-lock` to lock down dependencies, development ones included. When adding or removing dependencies, please use `npm install` to also update the lockfile. Please commit `package-lock.json` along with any changes to `package.json`. If you want to install the packages from the lockfile without updating it, please use the following command: @@ -64,10 +63,14 @@ npm ci ## Topics -The individual topics for the documentation for the frontend are +Frontend-specific documentation: + +1. [TESTING.md](./TESTING.md) — writing frontend specs, the Stimulus helpers, coverage +2. [PLUGINS.md](./PLUGINS.md) — linking plugin frontends during development + +Frontend topics documented alongside the rest of the application: -1. `TESTING.md` - documentation of our approach to integration and unit testing -2. `STYLING.md` - notes on styling and the Sass-Pipeline -3. `API.md` - notes on dealing with the several APIs provided by OpenProject -4. `LEGACY.md` - contains additional information on how to use the legacy bundle -5. `PLUGINS.md` - contains additional information on how to link plugins with a frontend during development. +- [Frontend style guide](https://www.openproject.org/docs/development/style-guide/frontend/) — code format and development patterns +- [Design system](https://www.openproject.org/docs/development/design-system/) — Primer and the component Lookbook +- [Running tests locally](https://www.openproject.org/docs/development/testing/running-tests-locally/#frontend-tests) — how to invoke the suite +- [Testing architecture](https://www.openproject.org/docs/development/testing/) — where frontend specs sit in the wider strategy diff --git a/frontend/doc/STYLING.md b/frontend/doc/STYLING.md deleted file mode 100644 index de431a17cbbe..000000000000 --- a/frontend/doc/STYLING.md +++ /dev/null @@ -1,29 +0,0 @@ -Styling the frontend -==================== - -Frontend styling is coupled to the Living Styleguide. The guide implements the same CSS that the application does. It takes the very same Sass files used in the Asset pipeline to build a second css file that is then used for displaying the guide. - -All styles for OpenProject are found in `./app/assets/stylesheets`. The frontend folder contains no styling, besides rendered files and some styling for the styleguide itself. - -## Using the styleguide - -The styleguide itself is just a long html page demonstrating the components. It can be modified by altering its base file `styleguide.html` (see `./app/assets/styleguide.html`). - -The general approach here is that for every partial of sass there is a Markdown file (`*.lsg`) describing it: - -```shell -$ cd app/assets/stylesheets/content -$ ls -la _accounts* -_accounts.lsg -_accounts.sass -``` - -The `lsg` is simple markdown containing information on how to use the component described. - -Ideally, this should be only one component per Sass partial, but this is not always possible, as seen in the case of `./app/assets/stylesheets/content/_work_packages.sass` which describes an area of the application instead of a single component. - -## A note on the css style used - -Originally introduced by `@myabc`, Sass-Code should ideally follow a convention as described in [Simple naming for CSS class names](http://www.hagenburger.net/BLOG/Modular-CSS-Class-Names.html). - -So far, mostly Sass partials have been used, grouped by their component. There is still a lot of legacy code in there, especially in the plugins. The legacy code for the core can be found within `./app/assets/stylesheets/_misc_legacy.sass` From 63c07e242dbc725deace27b07e08592e21ad00cc Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Sat, 5 Sep 2026 14:59:20 +0100 Subject: [PATCH 3/7] [DREAM-821] Rewrite the frontend testing guide The guide framed Angular as the current approach and cited a spec path that moved long ago. Rails with Hotwire is current; Angular is legacy, though it still bootstraps every run. Documents what was previously only discoverable by reading the config: specs run in real browsers, the two Stimulus helpers serve different purposes, and the Node tooling suite exists. How to invoke the runner is left to the guide that already covers it. Both examples compile, checked by extracting the fenced blocks and running them through `tsc`. https://community.openproject.org/wp/DREAM-821 --- frontend/doc/TESTING.md | 97 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 6 deletions(-) diff --git a/frontend/doc/TESTING.md b/frontend/doc/TESTING.md index 932c4458d637..3ce51fe0148e 100644 --- a/frontend/doc/TESTING.md +++ b/frontend/doc/TESTING.md @@ -1,17 +1,102 @@ -Running tests -==================== +# Testing the frontend +**How to run the suite** — including the browser matrix and watch mode — is documented once, in +[Running tests locally](https://www.openproject.org/docs/development/testing/running-tests-locally/#frontend-tests). +This page covers what is specific to writing frontend specs. +## What runs, and what bootstraps it -OpenProject is a hybrid application with most parts being Rails, along with some directives being used within Rails templates (the legacy application), and an Angular frontend for isolated SPA-like Modules such as the Work Packages module. +OpenProject is a hybrid application. Server-rendered Rails views with [Hotwire](https://hotwired.dev/) +(Turbo and Stimulus) are the current approach; the Angular frontend is legacy and is being migrated +away from. Both are covered by the same command, and only files ending in `.spec.ts` are matched. +`ng test` builds the specs with esbuild through the Angular CLI and executes them with +[Vitest](https://vitest.dev/). **Angular bootstraps the run even for specs that touch no Angular +code**, so a Stimulus or Turbo spec still needs the Angular build to succeed — a compile error in the +Angular tree fails specs that have nothing to do with it. +Specs run in real browsers driven by [Playwright](https://playwright.dev/), not in a DOM shim. That +means layout, focus and event behaviour are real, and an engine-specific bug surfaces in CI rather +than in review. -## Frontend specs +## Writing assertions +Assertions come from Vitest, extended with [jest-dom](https://github.com/testing-library/jest-dom) +matchers (`toBeVisible`, `toHaveAccessibleName`, and so on), and queries from +[DOM Testing Library](https://testing-library.com/docs/dom-testing-library/intro/). Prefer querying +the way a user finds things — by role, label or text — over CSS selectors and test-only attributes. +## Stimulus controllers -The Angular frontend services and components can be tested with frontend specs. +Two helpers in `src/stimulus/test-helpers.ts` cover the two things worth testing. +**Mount real markup and let Stimulus drive it.** `setupStimulusTest` starts an application, registers +the controllers under test, and returns a context with a Testing Library `screen` scoped to the +mounted container: -If you want to test services that have no dependencies, a simple instantiation of that class is sufficient to test the service in isolation. A good example is ` frontend/src/app/components/projects/current-project.service.spec.ts` \ No newline at end of file +```typescript +import { userEvent } from 'vitest/browser'; +import { setupStimulusTest, type StimulusTestContext } from 'core-stimulus/test-helpers'; +import CheckableController from 'core-stimulus/controllers/checkable.controller'; + +let ctx:StimulusTestContext; + +afterEach(() => ctx?.dispose()); + +it('checks every box when the button is pressed', async () => { + ctx = await setupStimulusTest({ controllers: { checkable: CheckableController } }); + await ctx.mount(` +
+ + +
+ `); + + await userEvent.click(ctx.screen.getByRole('button', { name: 'Check all' })); + + expect(ctx.screen.getByRole('checkbox', { name: 'First' })).toBeChecked(); +}); +``` + +Dispose in `afterEach` rather than at the end of the test: an assertion failure would otherwise skip +cleanup, and disposal is also where the helper surfaces errors Stimulus swallowed. + +This is the default. It exercises the controller through its real contract — identifier, targets, +actions — so a rename that breaks consumers fails the spec. + +**Call a method in isolation.** `createControllerInstance` builds an instance without connecting it +to the DOM, for logic that would otherwise need a lot of markup to reach: + +```typescript +const controller = createControllerInstance(CheckableController); +Object.defineProperty(controller, 'checkboxTargets', { value: inputs }); + +controller.toggleAll(new Event('click')); + +expect(inputs.every((input) => input.checked)).toBe(true); +``` + +Target properties are declared `readonly`, so they are supplied with `Object.defineProperty` rather +than assigned. Do not reach for an `any` cast to get around it — that discards exactly the type +checking that makes the isolated test worth writing. Reach for this only when the +behaviour is worth testing apart from its markup — a controller tested solely this way can pass while +being unusable from HTML. + +## Angular components + +Angular's own [testing guide](https://angular.dev/guide/testing) applies. Note that specs run +zoneless: mutating a plain component field and calling `detectChanges()` will not update the view, so +use signals for state the template reads. + +Broader patterns for Angular code are in the +[frontend style guide](https://www.openproject.org/docs/development/style-guide/frontend/). + +## Coverage + +Pass `--coverage` to collect a report through `@vitest/coverage-v8`. There is no enforced threshold; +it is for finding untested paths, not for gating. + +## See also + +- [Testing architecture](https://www.openproject.org/docs/development/testing/) — how frontend specs fit the wider + test strategy, including where feature specs take over. From db82407dbdcff80f91757ffb148fba792af9c0cd Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Sat, 5 Sep 2026 14:59:20 +0100 Subject: [PATCH 4/7] [DREAM-821] Cross-link frontend and repo docs Frontend documentation drifted partly because it sat apart from `docs/` with no route between the two. `AGENTS.md` keeps the commands and constraints an agent needs in context and defers the prose to documents that can be opened on demand, linked within the checkout so they match the branch being worked on. `frontend/doc` links out by published URL instead, so readers land on the rendered documentation rather than its Markdown source. Inbound links use GitHub URLs, since `frontend/doc` is not part of the published site. https://community.openproject.org/wp/DREAM-821 --- .../testing/running-tests-locally/README.md | 2 + frontend/AGENTS.md | 51 +++++++------------ 2 files changed, 21 insertions(+), 32 deletions(-) diff --git a/docs/development/testing/running-tests-locally/README.md b/docs/development/testing/running-tests-locally/README.md index a214ea7ce394..28686eb214dc 100644 --- a/docs/development/testing/running-tests-locally/README.md +++ b/docs/development/testing/running-tests-locally/README.md @@ -69,6 +69,8 @@ CI additionally typechecks the application and spec sources with `tsc`, which re npm run typecheck ``` +For guidance on writing frontend specs — the Stimulus test helpers, Testing Library queries and coverage — see [frontend/doc/TESTING.md](https://github.com/opf/openproject/blob/dev/frontend/doc/TESTING.md). + ## Unit tests After following the prerequisites, use the following command to run individual specs: diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index a14ff7b87f25..33efd0188439 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -2,49 +2,36 @@ ## Directory Structure -- `./src/` - Frontend code - - `./src/app/` - Legacy Angular modules/components - - `./src/common/` - Framework-agnostic modules (the `core-common` alias), importable from both Angular and Stimulus. Code belongs here when it depends on neither framework and both sides need it; a helper only Stimulus controllers use belongs in `./src/stimulus/helpers/` instead. - - `./src/stimulus/` - Stimulus controllers - - `./src/turbo/` - Turbo integration -- [Sortable lists selection contract](src/stimulus/controllers/dynamic/sortable-lists/README.md) - Consumer wiring, item identity, selection gestures, and presentation. +- `./src/app/` — legacy Angular modules and components +- `./src/common/` — framework-agnostic modules (the `core-common` alias) +- `./src/stimulus/` — Stimulus controllers +- `./src/turbo/` — Turbo integration -## Configuration Files - -- `eslint.config.mjs` - JavaScript/TypeScript linting -- `../package.json` / `./frontend/package.json` - Node.js dependencies +Where a given module belongs is explained in [doc/README.md](./doc/README.md#directory-structure). ## Version Requirements -- Node: `^24.15.0` (see `package.json` engines) +- Node: `^24.15.0` (see `engines` in the repository root `../package.json`) -## Setup +## Commands ```bash -npm ci && cd .. # Install Node packages +npm ci # install packages +npx eslint src/ # lint +npm run typecheck # typecheck application and spec sources +npm test # run the specs ``` ## Code Style -### JavaScript/TypeScript -- **New development**: Use Hotwire (Turbo + Stimulus) with server-rendered HTML -- **Legacy code**: Follow ESLint rules -- Prefer TypeScript over JavaScript -- Use [Primer Design System](https://primer.style/product/) via ViewComponent - -## Linting +New development uses Hotwire (Turbo + Stimulus) with server-rendered HTML and [Primer](https://primer.style/product/) via ViewComponent; the Angular tree is legacy. Prefer TypeScript over JavaScript. -```bash -# JavaScript/TypeScript -npx eslint src/ +Patterns and formatting conventions: [frontend style guide](../docs/development/style-guide/frontend/README.md). +Components and design system: [design system](../docs/development/design-system/README.md). -# Typecheck application and spec sources -npm run typecheck -``` +## Further documentation -## Testing - -```bash -# Frontend (Jasmine/Karma) -npm test && cd .. -``` +- [doc/TESTING.md](./doc/TESTING.md) — writing frontend specs, the Stimulus helpers, coverage +- [doc/PLUGINS.md](./doc/PLUGINS.md) — linking plugin frontends during development +- [doc/README.md](./doc/README.md) — dev server, builds, dependencies +- [Sortable lists selection contract](src/stimulus/controllers/dynamic/sortable-lists/README.md) — consumer wiring, item identity, selection gestures, and presentation From c6f02bb30b2ad90f5cb3b03449677b737b6bccca Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Sat, 5 Sep 2026 14:59:21 +0100 Subject: [PATCH 5/7] [DREAM-821] Drop the stale AirBnB style reference No airbnb config is installed and `eslint.config.mjs` never referenced one; format comes from `@stylistic` house rules and `.editorconfig`. Pointing at the enforcement is truthful where naming an external guide was not. Also marks the Angular pattern material as legacy, since the page is now linked from `AGENTS.md` while new development uses Hotwire. https://community.openproject.org/wp/DREAM-821 --- docs/development/style-guide/frontend/README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/development/style-guide/frontend/README.md b/docs/development/style-guide/frontend/README.md index 7224a933f731..cbeca715ca53 100644 --- a/docs/development/style-guide/frontend/README.md +++ b/docs/development/style-guide/frontend/README.md @@ -1,14 +1,22 @@ # OpenProject development style guide - frontend +Working on the frontend day to day — dev server, builds, plugin linking and writing specs — is documented in [frontend/doc](https://github.com/opf/openproject/tree/dev/frontend/doc). + ## Code format -OpenProject follows the [AirBnB's style guide](https://github.com/airbnb/javascript) regarding to the code format. +Code format is enforced by ESLint rather than described here. The house rules live in `frontend/eslint.config.mjs`, largely through [`@stylistic`](https://eslint.style/), and indentation defers to `.editorconfig`. + +```shell +cd frontend && npx eslint src/ +``` ## Development patterns +> The patterns below describe the Angular application, which is now legacy. New development uses Hotwire — Turbo and Stimulus — with server-rendered HTML and Primer via ViewComponent. Reframing this section is tracked in [DREAM-823](https://community.openproject.org/wp/DREAM-823). + ### Angularized -OpenProject follows the [Angular's style guide](https://angular.io/guide/styleguide) patterns. +OpenProject follows the [Angular's style guide](https://angular.dev/style-guide) patterns. ### Declarative From 8f8a2de777987a5caf2b314725ae79f2f81e2451 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Sat, 5 Sep 2026 14:59:21 +0100 Subject: [PATCH 6/7] Remove the unused jsdom environment Specs run in real browsers, so the CSS.escape, ResizeObserver and HTMLDialogElement shims guarding against jsdom's gaps never fired. All three are removed along with the dependency itself, which nothing else pulls in. --- frontend/package-lock.json | 466 ------------------ frontend/package.json | 1 - frontend/src/test-setup.ts | 33 -- .../dispatch-event-stream-action.spec.ts | 2 +- 4 files changed, 1 insertion(+), 501 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e20f1aa77bb0..843d159ea0ef 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -175,7 +175,6 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.1.1", "globals": "^17.12.0", - "jsdom": "^29.1.1", "patch-package": "^8.0.1", "playwright": "^1.61.1", "source-map-explorer": "^2.5.2", @@ -1968,57 +1967,6 @@ "@appsignal/javascript": "=1.6.1" } }, - "node_modules/@asamuzakjp/css-color": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", - "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@csstools/css-calc": "^3.2.0", - "@csstools/css-color-parser": "^4.1.0", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", - "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/generational-cache": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", - "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@atlaskit/pragmatic-drag-and-drop": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-2.0.1.tgz", @@ -2512,19 +2460,6 @@ "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", "license": "MIT" }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" - } - }, "node_modules/@cacheable/memory": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", @@ -2573,146 +2508,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@csstools/color-helpers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", - "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@csstools/css-calc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", - "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", - "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^6.0.2", - "@csstools/css-calc": "^3.2.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", - "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "peerDependencies": { - "css-tree": "^3.2.1" - }, - "peerDependenciesMeta": { - "css-tree": { - "optional": true - } - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, "node_modules/@datorama/akita": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@datorama/akita/-/akita-8.0.1.tgz", @@ -3352,24 +3147,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@exodus/bytes": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", - "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, "node_modules/@floating-ui/core": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", @@ -8605,16 +8382,6 @@ "node": ">=18.0.0" } }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, "node_modules/bignumber.js": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", @@ -9339,20 +9106,6 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, - "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -9422,13 +9175,6 @@ } } }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -11397,19 +11143,6 @@ "node": "20 || >=22" } }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -11975,13 +11708,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -12315,57 +12041,6 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "node_modules/jsdom": { - "version": "29.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", - "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^5.1.11", - "@asamuzakjp/dom-selector": "^7.1.1", - "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.3", - "@exodus/bytes": "^1.15.0", - "css-tree": "^3.2.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.3.5", - "parse5": "^8.0.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.25.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.3.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", - "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -15059,19 +14734,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, "node_modules/scheduler": { "version": "0.28.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", @@ -15796,13 +15458,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", @@ -15925,26 +15580,6 @@ "node": ">=14.0.0" } }, - "node_modules/tldts": { - "version": "7.0.30", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", - "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.0.30" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.0.30", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", - "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", - "dev": true, - "license": "MIT" - }, "node_modules/tmp": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", @@ -15991,32 +15626,6 @@ "node": ">=6" } }, - "node_modules/tough-cookie": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", - "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/ts-action": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/ts-action/-/ts-action-11.0.0.tgz", @@ -16275,16 +15884,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undici": { - "version": "7.30.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", - "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, "node_modules/undici-types": { "version": "8.9.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", @@ -16726,19 +16325,6 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/watchpack": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", @@ -16760,41 +16346,6 @@ "license": "MIT", "optional": true }, - "node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -17006,23 +16557,6 @@ "repeat-string": "^1.5.2" } }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "license": "MIT" - }, "node_modules/y-prosemirror": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/y-prosemirror/-/y-prosemirror-1.3.7.tgz", diff --git a/frontend/package.json b/frontend/package.json index 89873977f8a6..7e9e25f8da8b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -45,7 +45,6 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.1.1", "globals": "^17.12.0", - "jsdom": "^29.1.1", "patch-package": "^8.0.1", "playwright": "^1.61.1", "source-map-explorer": "^2.5.2", diff --git a/frontend/src/test-setup.ts b/frontend/src/test-setup.ts index 824a3b4d93ae..e3250964cc15 100644 --- a/frontend/src/test-setup.ts +++ b/frontend/src/test-setup.ts @@ -47,36 +47,3 @@ afterEach(() => { window.I18n = new I18n(); -// jsdom does not implement CSS.escape; production helpers (e.g. getMetaElement) -// call it unconditionally. -if (typeof CSS === 'undefined' || typeof CSS.escape !== 'function') { - (globalThis as any).CSS = (globalThis as any).CSS || {}; - (globalThis as any).CSS.escape = (value:string) => String(value).replace(/[^a-zA-Z0-9_-]/g, (ch) => `\\${ch}`); -} - -// jsdom does not implement ResizeObserver. The shim declares the native -// constructor signature so static analysis resolving the global to this class -// still accepts the callback every real call site passes. -if (typeof (globalThis as any).ResizeObserver === 'undefined') { - (globalThis as any).ResizeObserver = class { - // eslint-disable-next-line @typescript-eslint/no-empty-function - constructor(_callback:ResizeObserverCallback) {} - observe() {} - unobserve() {} - disconnect() {} - }; -} - -// jsdom does not implement HTMLDialogElement.showModal/close. -if (typeof HTMLDialogElement !== 'undefined') { - const proto = HTMLDialogElement.prototype as any; - if (typeof proto.showModal !== 'function') { - proto.showModal = function showModal() { this.open = true; }; - } - if (typeof proto.show !== 'function') { - proto.show = function show() { this.open = true; }; - } - if (typeof proto.close !== 'function') { - proto.close = function close() { this.open = false; }; - } -} diff --git a/frontend/src/turbo/dispatch-event-stream-action.spec.ts b/frontend/src/turbo/dispatch-event-stream-action.spec.ts index 39b344f5b07c..6ded5bc68962 100644 --- a/frontend/src/turbo/dispatch-event-stream-action.spec.ts +++ b/frontend/src/turbo/dispatch-event-stream-action.spec.ts @@ -45,7 +45,7 @@ describe('dispatchEvent turbo stream action', () => { beforeEach(() => { registerDispatchEventStreamAction(); dispatched = []; - // jsdom does not let us add a listener for an arbitrary name retroactively, + // A listener cannot be added for an arbitrary event name retroactively, // so we spy on dispatchEvent itself to record everything. vi.spyOn(document, 'dispatchEvent').mockImplementation((event:Event) => { dispatched.push(event as CustomEvent); From 605764f5ee87dfa4095b6ecdaf28a38c49eff517 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Fri, 9 Oct 2026 11:48:13 +0200 Subject: [PATCH 7/7] [DREAM-821] Soften wording in frontend docs Applies two review suggestions: the style guide no longer contrasts enforcement with description, and the testing guide presents the Stimulus helpers as covering the most common use cases instead of ruling on what is worth testing. https://community.openproject.org/wp/DREAM-821 --- docs/development/style-guide/frontend/README.md | 2 +- frontend/doc/TESTING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/development/style-guide/frontend/README.md b/docs/development/style-guide/frontend/README.md index cbeca715ca53..485e1d94c71a 100644 --- a/docs/development/style-guide/frontend/README.md +++ b/docs/development/style-guide/frontend/README.md @@ -4,7 +4,7 @@ Working on the frontend day to day — dev server, builds, plugin linking and wr ## Code format -Code format is enforced by ESLint rather than described here. The house rules live in `frontend/eslint.config.mjs`, largely through [`@stylistic`](https://eslint.style/), and indentation defers to `.editorconfig`. +Code format is enforced by ESLint. The house rules live in `frontend/eslint.config.mjs`, largely through [`@stylistic`](https://eslint.style/), and indentation defers to `.editorconfig`. ```shell cd frontend && npx eslint src/ diff --git a/frontend/doc/TESTING.md b/frontend/doc/TESTING.md index 3ce51fe0148e..937ab2666d71 100644 --- a/frontend/doc/TESTING.md +++ b/frontend/doc/TESTING.md @@ -28,7 +28,7 @@ the way a user finds things — by role, label or text — over CSS selectors an ## Stimulus controllers -Two helpers in `src/stimulus/test-helpers.ts` cover the two things worth testing. +Two helpers in `src/stimulus/test-helpers.ts` cover the most common use cases. **Mount real markup and let Stimulus drive it.** `setupStimulusTest` starts an application, registers the controllers under test, and returns a context with a Testing Library `screen` scoped to the