From 7964957c37dd4d6755f26a7d03dcb68b87ce8e06 Mon Sep 17 00:00:00 2001 From: Lars Wassermann Date: Wed, 30 Sep 2026 09:27:37 +0200 Subject: [PATCH 1/2] Fix rubocop complaints --- ..._display_representations_on_mobile_spec.rb | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/features/work_packages/display_representations/switch_display_representations_on_mobile_spec.rb b/spec/features/work_packages/display_representations/switch_display_representations_on_mobile_spec.rb index b12fbf1c0451..0b039e6217ea 100644 --- a/spec/features/work_packages/display_representations/switch_display_representations_on_mobile_spec.rb +++ b/spec/features/work_packages/display_representations/switch_display_representations_on_mobile_spec.rb @@ -36,33 +36,33 @@ let(:wp_table) { Pages::WorkPackagesTable.new(project) } let(:cards) { Pages::WorkPackageCards.new(project) } - let(:wp_1) do + let(:wp1) do create(:work_package, project:) end - let(:wp_2) do + let(:wp2) do create(:work_package, project:) end before do - wp_1 - wp_2 + wp1 + wp2 login_as(user) wp_table.visit! - wp_table.expect_work_package_listed wp_1, wp_2 + wp_table.expect_work_package_listed wp1, wp2 end - context "switching to mobile card view" do + context "in mobile card view" do include_context "with mobile screen size" it "can switch the representation automatically on mobile after a refresh" do # It shows the elements as cards - cards.expect_work_package_listed wp_1, wp_2 + cards.expect_work_package_listed wp1, wp2 # A single click leads to the full view - cards.select_work_package(wp_1) + cards.select_work_package(wp1) expect(page).to have_css(".work-packages--details--subject", text: wp_1.subject) page.go_back @@ -74,8 +74,8 @@ # Since the query is unchanged, the WPs will be displayed as list on larger screens again page.driver.browser.manage.window.resize_to(700, 1080) page.driver.browser.navigate.refresh - wp_table.expect_work_package_listed wp_1, wp_2 - wp_table.expect_work_package_order wp_1, wp_2 + wp_table.expect_work_package_listed wp1, wp2 + wp_table.expect_work_package_order wp1, wp2 end end end From 546d7b0c45609c3966ca94d40f2851ead3f33a8b Mon Sep 17 00:00:00 2001 From: Lars Wassermann Date: Mon, 21 Sep 2026 19:16:34 +0200 Subject: [PATCH 2/2] [DREAM-833] Add Work Package heading level 2 So that it appears in screenreader heading menues. Add it as a separate, sr-only element because the wp-subject would otherwise either get shadowed, or the editing-button translation would become part of the heading. Use the opportunity to add some tests for wp-subject. https://community.openproject.org/wp/DREAM-833 Co-authored-by: Alexander Brandon Coles --- .../wp-subject/wp-subject.component.spec.ts | 113 ++++++++++++++++++ .../components/wp-subject/wp-subject.html | 3 + .../features/card_view/select_card_spec.rb | 3 +- .../work_packages/cancel_editing_spec.rb | 2 +- ..._display_representations_on_mobile_spec.rb | 3 +- .../select/select_work_package_row_spec.rb | 3 +- 6 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 frontend/src/app/features/work-packages/components/wp-subject/wp-subject.component.spec.ts diff --git a/frontend/src/app/features/work-packages/components/wp-subject/wp-subject.component.spec.ts b/frontend/src/app/features/work-packages/components/wp-subject/wp-subject.component.spec.ts new file mode 100644 index 000000000000..5bb22b2c4d41 --- /dev/null +++ b/frontend/src/app/features/work-packages/components/wp-subject/wp-subject.component.spec.ts @@ -0,0 +1,113 @@ +//-- copyright +// OpenProject is an open source project management software. +// Copyright (C) the OpenProject GmbH +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See COPYRIGHT and LICENSE files for more details. +//++ + +import { CommonModule } from '@angular/common'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { fireEvent, screen } from '@testing-library/dom'; +import { WorkPackageSubjectComponent } from './wp-subject.component'; +import { EditableAttributeFieldComponent } from 'core-app/shared/components/fields/edit/field/editable-attribute-field.component'; +import { EditFormComponent } from 'core-app/shared/components/fields/edit/edit-form/edit-form.component'; +import { + HalResourceEditingService, +} from 'core-app/shared/components/fields/edit/services/hal-resource-editing.service'; +import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; +import { DisplayFieldService } from 'core-app/shared/components/fields/display/display-field.service'; +import { initializeCoreDisplayFields } from 'core-app/shared/components/fields/display/display-field.initializer'; +import { OPContextMenuService } from 'core-app/shared/components/op-context-menu/op-context-menu.service'; +import { States } from 'core-app/core/states/states.service'; +import { I18nService } from 'core-app/core/i18n/i18n.service'; +import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; +import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; +import { HalResource } from 'core-app/features/hal/resources/hal-resource'; + +describe('WorkPackageSubjectComponent', () => { + beforeEach(async () => { + const schemaStub = { + ofProperty: () => ({ type: 'String' }), + isAttributeEditable: () => true, + }; + + const registeredFields:Record = {}; + + await TestBed.configureTestingModule({ + imports: [CommonModule], + declarations: [WorkPackageSubjectComponent, EditableAttributeFieldComponent], + providers: [ + { provide: ApiV3Service, useValue: {} }, + { provide: OPContextMenuService, useValue: { close: () => undefined } }, + { provide: States, useValue: {} }, + { provide: I18nService, useValue: { t: (key:string) => key } }, + { provide: SchemaCacheService, useValue: { of: () => schemaStub } }, + { + provide: EditFormComponent, + useValue: { + register: (field:EditableAttributeFieldComponent) => { registeredFields[field.fieldName] = field; }, + activate: (fieldName:string) => { + registeredFields[fieldName].editContainer.nativeElement.innerHTML = ''; + return Promise.resolve(); + }, + }, + }, + { + provide: HalResourceEditingService, + useValue: { + temporaryEditResource: (resource:HalResource) => ({ values$: () => of(resource) }), + typedState: () => ({ hasValue: () => false }), + }, + }, + ], + }).compileComponents(); + + initializeCoreDisplayFields(TestBed.inject(DisplayFieldService))(); + }); + + function render(wpOverrides:Partial = {}):ComponentFixture { + const fixture = TestBed.createComponent(WorkPackageSubjectComponent); + fixture.componentInstance.workPackage = { subject: 'Some subject', ...wpOverrides } as WorkPackageResource; + fixture.detectChanges(); + return fixture; + } + + it('renders the work package subject as a level 2 heading', () => { + const subject = 'Add some angular tests for wp-subject'; + render({ subject }); + + expect(screen.getByRole('heading', { level: 2, name: subject })).toBeInTheDocument(); + }); + + it('keeps the accessible heading while editing', async () => { + const subject = 'Add some angular tests for wp-subject'; + render({ subject }); + + fireEvent.click(screen.getByRole('button', { name: `subject ${subject}`})); + await screen.findByRole('textbox'); + + expect(screen.getByRole('heading', { level: 2, name: subject })).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/app/features/work-packages/components/wp-subject/wp-subject.html b/frontend/src/app/features/work-packages/components/wp-subject/wp-subject.html index 7b7ce11acdf5..682c2d17c4b7 100644 --- a/frontend/src/app/features/work-packages/components/wp-subject/wp-subject.html +++ b/frontend/src/app/features/work-packages/components/wp-subject/wp-subject.html @@ -8,6 +8,9 @@ [wrapperClasses]="'work-packages--type-selector work-packages--subject-element -no-label'" [fieldName]="'type'" /> + +

{{ workPackage.subject }}

+