[#69564] Fix A11y in Workflows, Permissions Report

- Provides labels for matrix checkboxes that clearly describe the
  purpose of each control.

- Improves "toggle all" button labeling and "Check all"/"Uncheck all"
  link tooltips to clearly indicate consequences. Also adds these
  tooltips to Role edit page for consistency.
- Improves truncation of text in row headers by displaying an "ellipsis"
  and an expander control that allows a user to click to see the full
  text. It introduces a `TruncationController` Stimulus Controller to
  add behavior to Primer `Truncation` and `HiddenTextExpander`
  components.

This commit also:

- Primerizes the checkboxes.
- Harmonizes the positioning of the column "toggle all" button (moved to
  left-hand side).
- Makes numerous small spacing fixes to improve alignment of headers and
  checkboxes.
This commit is contained in:
Alexander Brandon Coles
2025-12-14 01:12:01 -03:00
parent 9dcdf4176d
commit 3c9eb66eec
13 changed files with 521 additions and 120 deletions
+2 -2
View File
@@ -362,8 +362,8 @@ module ApplicationHelper
back_url
end
def check_all_links(form_id = nil)
render(OpenProject::Common::CheckAllComponent.new(checkable_id: form_id))
def check_all_links(form_id = nil, &)
render(OpenProject::Common::CheckAllComponent.new(checkable_id: form_id), &)
end
def current_layout
+11 -10
View File
@@ -27,17 +27,13 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<% permissions.each do |mod, mod_permissions| %>
<% global_prefix = show_global_role ? "fieldset--global--" : "fieldset--" %>
<% module_name = mod.blank? ? "fieldset--global--#{Project.model_name.human.downcase.tr(' ', '_')}" : "#{global_prefix}#{l_or_humanize(mod, prefix: 'project_module_').downcase.tr(' ', '_')}" %>
<% module_id = module_name.parameterize %>
<% module_name = mod.blank? ? Project.model_name.param_key : l_or_humanize(mod, prefix: "project_module_").downcase.tr(" ", "_") %>
<% module_title = show_global_role && mod.blank? ? t(:label_global) : permission_header_for_project_module(mod) %>
<% section_id = "#{'global-' if show_global_role}#{module_name.parameterize}-section" %>
<%= render Primer::OpenProject::CollapsibleSection.new(id: module_id, display: :block, mb: 3) do |section| %>
<%= render Primer::OpenProject::CollapsibleSection.new(id: section_id, display: :block, mb: 3) do |section| %>
<% section.with_title(tag: :h3) do %>
<% if show_global_role && mod.blank? %>
<%= t(:label_global) %>
<% else %>
<%= permission_header_for_project_module(mod) %>
<% end %>
<%= module_title %>
<% end %>
<% if I18n.exists?("permission_header_explanation", scope: mod) %>
@@ -47,7 +43,12 @@ See COPYRIGHT and LICENSE files for more details.
<% end %>
<% section.with_additional_information do %>
<%= check_all_links module_id %>
<%=
check_all_links section_id do |links|
links.with_check_all_button.with_tooltip(text: t(".section_check_all_label", module: module_title))
links.with_uncheck_all_button.with_tooltip(text: t(".section_uncheck_all_label", module: module_title))
end
%>
<% end %>
<% section.with_collapsible_content(classes: "-columns-2", data: { controller: "checkable" }) do %>
+89 -33
View File
@@ -48,16 +48,22 @@ See COPYRIGHT and LICENSE files for more details.
<%= hidden_field_tag "permissions[0]", "", id: nil %>
<% group_permissions_by_module(@permissions).each do |mod, mod_permissions| %>
<% module_name = mod.blank? ? "form--#{I18n.t('attributes.project')}" : "form--#{l_or_humanize(mod, prefix: 'project_module_').tr(' ', '_')}" %>
<% escaped_name = module_name.parameterize %>
<% module_name = mod.blank? ? Project.model_name.param_key : l_or_humanize(mod, prefix: "project_module_").tr(" ", "_") %>
<% module_title = permission_header_for_project_module(mod) %>
<% section_id = "#{module_name.parameterize}-section" %>
<%= render Primer::OpenProject::CollapsibleSection.new(id: escaped_name, display: :block, mb: 3) do |section| %>
<%= render Primer::OpenProject::CollapsibleSection.new(id: section_id, display: :block, mb: 3) do |section| %>
<% section.with_title do %>
<%= permission_header_for_project_module(mod) %>
<%= module_title %>
<% end %>
<% section.with_additional_information do %>
<%= check_all_links escaped_name %>
<%=
check_all_links section_id do |links|
links.with_check_all_button.with_tooltip(text: t(".matrix_check_all_label", module: module_title))
links.with_uncheck_all_button.with_tooltip(text: t(".matrix_uncheck_all_label", module: module_title))
end
%>
<% end %>
<% section.with_collapsible_content do %>
@@ -66,8 +72,11 @@ See COPYRIGHT and LICENSE files for more details.
<table
class="generic-table"
data-controller="checkable table-highlighting">
<caption class="sr-only">
<%= t(".matrix_caption", module: module_title) %>
</caption>
<colgroup>
<col>
<col data-highlight="false">
<col>
<col>
<col>
@@ -87,22 +96,29 @@ See COPYRIGHT and LICENSE files for more details.
</th>
<% @roles.each do |role| %>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header-outer p-0">
<div class="generic-table--sort-header">
<%= render(Primer::BaseComponent.new(tag: :div, display: :inline_flex, align_items: :center)) do %>
<%= render(Primer::Beta::Text.new(mr: 1)) do %>
<%= role.name %>
<%= render(Primer::Beta::Label.new(scheme: :secondary)) { t(:label_builtin) } if role.builtin? %>
<% end %>
<%=
render(
Primer::BaseComponent.new(
tag: :div,
display: :flex,
align_items: :center,
classes: "gap-1",
ml: 1,
mr: 2
)
) do
%>
<%=
render(
Primer::Beta::IconButton.new(
scheme: :invisible,
size: :small,
icon: :check,
tooltip_direction: :se,
tooltip_direction: :sw,
aria: {
label: t(:label_check_uncheck_all_in_column)
label: t(".matrix_check_uncheck_all_in_col_label_html", module: module_title, role: role.name)
},
data: {
action: "checkable#toggleSelection",
@@ -112,6 +128,10 @@ See COPYRIGHT and LICENSE files for more details.
)
)
%>
<%= render(Primer::Beta::Truncate.new(flex: 1)) do %>
<%= role.name %>
<%= render(Primer::Beta::Label.new(scheme: :secondary)) { t(:label_builtin) } if role.builtin? %>
<% end %>
<% end %>
</div>
</div>
@@ -121,9 +141,22 @@ See COPYRIGHT and LICENSE files for more details.
</thead>
<tbody>
<% mod_permissions.each do |permission| %>
<% humanized_permission_name = l_or_humanize(permission.name, prefix: "permission_") %>
<tr>
<td>
<%= render(Primer::BaseComponent.new(tag: :div, display: :inline_flex, align_items: :center)) do %>
<th scope="row" class="-min-300">
<%=
render(
Primer::BaseComponent.new(
tag: :div,
display: :flex,
align_items: :center,
data: { controller: "truncation" },
classes: "gap-1",
ml: 1,
mr: 3
)
) do
%>
<%=
render(
Primer::Beta::IconButton.new(
@@ -132,7 +165,7 @@ See COPYRIGHT and LICENSE files for more details.
icon: :check,
tooltip_direction: :sw,
aria: {
label: t(:label_check_uncheck_all_in_row)
label: t(".matrix_check_uncheck_all_in_row_label_html", permission: humanized_permission_name)
},
data: {
action: "checkable#toggleSelection",
@@ -142,26 +175,49 @@ See COPYRIGHT and LICENSE files for more details.
)
)
%>
<%= render(Primer::Beta::Text.new(ml: 1)) do %>
<%= l_or_humanize(permission.name, prefix: "permission_") %>
<% end %>
<%=
render(
Primer::Beta::Truncate.new(
data: { truncation_target: "truncate" },
flex: 1
)
) do
humanized_permission_name
end
%>
<%=
render(
Primer::Alpha::HiddenTextExpander.new(
hidden: true,
aria: { label: t(:"js.label_expand_text") },
data: { truncation_target: "expander" }
)
)
%>
<% end %>
</td>
<% @roles.each do |role| %>
</th>
<% @roles.each.with_index do |role, i| %>
<td>
<% if setable_permissions(role).include?(permission) %>
<%=
check_box_tag(
"permissions[#{role.id}][]",
permission.name,
role.permissions.include?(permission.name),
id: nil,
data: {
checkable_target: "checkbox",
role: role.id,
permission: permission.name
}
)
render(Primer::BaseComponent.new(tag: :div, display: :flex, align_items: :center, mx: 1)) do
render(
Primer::Alpha::CheckBox.new(
scheme: :array,
name: "permissions[#{role.id}]",
id: "permissions_#{role.id}_#{permission.name}", # See BUG https://github.com/primer/view_components/issues/3811
value: permission.name,
checked: role.permissions.include?(permission.name),
label: t(".matrix_checkbox_label", permission: humanized_permission_name, role: role.name),
visually_hide_label: true,
data: {
checkable_target: "checkbox",
role: role.id,
permission: permission.name
}
)
)
end
%>
<% end %>
</td>
+104 -47
View File
@@ -43,6 +43,9 @@ See COPYRIGHT and LICENSE files for more details.
<table
class="generic-table workflow-table transitions-<%= name %>"
data-controller="checkable">
<caption class="sr-only">
<%= t(".matrix_caption_#{name}", default: t(".matrix_caption")) %>
</caption>
<thead class="-sticky">
<tr>
<th></th>
@@ -53,7 +56,12 @@ See COPYRIGHT and LICENSE files for more details.
<%= t(:label_new_statuses_allowed) %>
</span>
<span class="workflow-table--check-all">
<%= check_all_links "workflow_form_#{name}" %>
<%=
check_all_links "workflow_form_#{name}" do |links|
links.with_check_all_button.with_tooltip(text: t(".matrix_check_all_label"))
links.with_uncheck_all_button.with_tooltip(text: t(".matrix_uncheck_all_label"))
end
%>
</span>
</div>
</div>
@@ -61,38 +69,20 @@ See COPYRIGHT and LICENSE files for more details.
</tr>
<tr>
<th class="-table-border-bottom -table-border-right"></th>
<% for new_status in @statuses %>
<th class="workflow-table--current-status -table-border-top -table-border-bottom">
<%= render(Primer::BaseComponent.new(tag: :div, display: :inline_flex, align_items: :center)) do %>
<%=
render(
Primer::Beta::IconButton.new(
scheme: :invisible,
size: :small,
icon: :check,
tooltip_direction: :sw,
aria: {
label: t(:label_check_uncheck_all_in_column)
},
data: {
action: "checkable#toggleSelection",
checkable_key_param: "newStatus",
checkable_value_param: new_status.id
}
)
)
%>
<%= render(Primer::Beta::Text.new(ml: 1)) { new_status.name } %>
<% end %>
</th>
<% end %>
</tr>
</thead>
<tbody>
<% for old_status in @statuses %>
<tr class="-table-border-left">
<td class="workflow-table--current-status -table-border-right">
<%= render(Primer::BaseComponent.new(tag: :div, display: :inline_flex, align_items: :center)) do %>
<% @statuses.each do |new_status| %>
<th class="workflow-table--current-status -table-border-top -table-border-bottom px-0">
<%=
render(
Primer::BaseComponent.new(
tag: :div,
display: :flex,
align_items: :center,
classes: "gap-1",
ml: 1,
mr: 2
)
) do
%>
<%=
render(
Primer::Beta::IconButton.new(
@@ -101,7 +91,48 @@ See COPYRIGHT and LICENSE files for more details.
icon: :check,
tooltip_direction: :sw,
aria: {
label: t(:label_check_uncheck_all_in_row)
label: t(".matrix_check_uncheck_all_in_col_label_html", new_status: new_status.name)
},
data: {
action: "checkable#toggleSelection",
checkable_key_param: "newStatus",
checkable_value_param: new_status.id
}
)
)
%>
<%= render(Primer::Beta::Truncate.new(flex: 1)) { new_status.name } %>
<% end %>
</th>
<% end %>
</tr>
</thead>
<tbody>
<% @statuses.each do |old_status| %>
<tr class="-table-border-left">
<th scope="row" class="workflow-table--current-status -table-border-right">
<%=
render(
Primer::BaseComponent.new(
tag: :div,
display: :flex,
align_items: :center,
data: { controller: "truncation" },
classes: "gap-1",
ml: 1,
mr: 3
)
) do
%>
<%=
render(
Primer::Beta::IconButton.new(
scheme: :invisible,
size: :small,
icon: :check,
tooltip_direction: :sw,
aria: {
label: t(".matrix_check_uncheck_all_in_row_label_html", old_status: old_status.name)
},
data: {
action: "checkable#toggleSelection",
@@ -111,22 +142,48 @@ See COPYRIGHT and LICENSE files for more details.
)
)
%>
<%= render(Primer::Beta::Text.new(ml: 1)) { old_status.name } %>
<%=
render(
Primer::Beta::Truncate.new(
data: { truncation_target: "truncate" },
flex: 1
)
) do
old_status.name
end
%>
<%=
render(
Primer::Alpha::HiddenTextExpander.new(
hidden: true,
aria: { label: t(:"js.label_expand_text") },
data: { truncation_target: "expander" }
)
)
%>
<% end %>
</td>
<% for new_status in @statuses -%>
</th>
<% @statuses.each do |new_status| -%>
<td>
<%=
check_box_tag(
"status[#{old_status.id}][#{new_status.id}][]",
name,
workflows.detect { |w| w.old_status_id == old_status.id && w.new_status_id == new_status.id },
data: {
checkable_target: "checkbox",
old_status: old_status.id,
new_status: new_status.id
}
)
render(Primer::BaseComponent.new(tag: :div, display: :flex, align_items: :center, mx: 1)) do
render(
Primer::Alpha::CheckBox.new(
scheme: :array,
name: "status[#{old_status.id}][#{new_status.id}]",
id: "status_#{old_status.id}_#{new_status.id}", # See BUG https://github.com/primer/view_components/issues/3811
value: name,
checked: workflows.any? { it.old_status_id == old_status.id && it.new_status_id == new_status.id },
label: t(".matrix_checkbox_label", old_status: old_status.name, new_status: new_status.name),
visually_hide_label: true,
data: {
checkable_target: "checkbox",
old_status: old_status.id,
new_status: new_status.id
}
)
)
end
%>
</td>
<% end -%>
+23
View File
@@ -771,6 +771,18 @@ en:
no_results_title_text: There is currently no news to report.
no_results_content_text: Add a news item
roles:
permissions:
section_check_all_label: "Assign all %{module} permissions"
section_uncheck_all_label: "Unassign all %{module} permissions"
report:
matrix_caption: "Permissions matrix for %{module} module"
matrix_checkbox_label: "Assign %{permission} permission to %{role} role"
matrix_check_all_label: "Assign all %{module} permissions to all roles"
matrix_uncheck_all_label: "Unassign all %{module} permissions from all roles"
matrix_check_uncheck_all_in_row_label_html: "Toggle <em>%{permission}</em> permission for all roles"
matrix_check_uncheck_all_in_col_label_html: "Toggle all %{module} permissions for <em>%{role}</em> role"
users:
autologins:
prompt: "Stay logged in for %{num_days}"
@@ -985,6 +997,17 @@ en:
index:
no_results_content_text: Add a new wiki page
workflows:
form:
matrix_caption: "Workflow matrix"
matrix_caption_assignee: "Workflow matrix for assignee"
matrix_caption_author: "Workflow matrix for author"
matrix_checkbox_label: "Allow transition from %{old_status} to %{new_status}"
matrix_check_all_label: "Allow all transitions"
matrix_uncheck_all_label: "Disallow all transitions"
matrix_check_uncheck_all_in_row_label_html: "Toggle transitions from <em>%{old_status}</em> to all new statuses"
matrix_check_uncheck_all_in_col_label_html: "Toggle transitions from all old statuses to <em>%{new_status}</em>"
work_flows:
index:
no_results_title_text: There are currently no workflows.
+2
View File
@@ -385,6 +385,7 @@ en:
label_collapse: "Collapse"
label_collapsed: "collapsed"
label_collapse_all: "Collapse all"
label_collapse_text: "Collapse text"
label_comment: "Comment"
label_committed_at: "%{committed_revision_link} at %{date}"
label_committed_link: "committed revision %{revision_identifier}"
@@ -397,6 +398,7 @@ en:
label_expand: "Expand"
label_expanded: "expanded"
label_expand_all: "Expand all"
label_expand_text: "Show full text"
label_expand_project_menu: "Expand project menu"
label_export: "Export"
label_export_preparing: "The export is being prepared and will be downloaded shortly."
+18 -4
View File
@@ -150,6 +150,7 @@ table.generic-table
border-bottom: 1px solid var(--borderColor-default)
@media screen
th[scope="row"]:not(.-no-ellipsis),
td:not(.-no-ellipsis)
@include text-shortener
@@ -162,6 +163,10 @@ table.generic-table
position: relative
top: 1px
th[scope="row"]
font-weight: var(--base-text-weight-bold)
th[scope="row"],
td
max-width: 300px
text-align: left
@@ -172,6 +177,12 @@ table.generic-table
&.form--td
vertical-align: middle
&.-centered
text-align: center
&.-right
text-align: right
// Center input fields and select boxes vertically in tables
.form--field
margin: 0px
@@ -180,7 +191,9 @@ table.generic-table
#{$inputElement}~.form-label
vertical-align: middle
input[type="checkbox"], input[type="radio"]
// N.B. we only adjust margin on non-Primerized controls
input[type="checkbox"]:not(.FormControl-checkbox),
input[type="radio"]:not(.FormControl-radio)
margin-top: -0.25rem
// In the interactive table the behaviour is like this:
@@ -200,9 +213,10 @@ table.generic-table
width: 100%
max-width: 500px
&.-min-200
@media screen
min-width: 200px
@each $size in (200, 300)
&.-min-#{$size}
@media screen
min-width: #{$size}px
// The avatar image is not taken into the width calculation of the table cell by the browser.
// That is why we add the space manually.
@@ -136,3 +136,15 @@ ul.SegmentedControl,
// we override ordering to display help text after the asterisk
> .op-attribute-help-text
order: 1
.Truncate
&.Truncate--expanded
max-width: none
min-width: 0
overflow: visible
white-space: normal
.Truncate-text
white-space: normal
overflow: visible
text-overflow: clip
@@ -0,0 +1,88 @@
/*
* -- 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 { Controller } from '@hotwired/stimulus';
import { useResize } from 'stimulus-use';
export default class TruncationController extends Controller<HTMLElement> {
static targets = ['truncate', 'expander'];
static values = { expanded: Boolean };
declare readonly truncateTarget:HTMLElement;
declare readonly expanderTarget:HTMLElement;
declare expandedValue:boolean;
declare readonly expandLabelValue:string;
declare readonly collapseLabelValue:string;
private abortController:AbortController|null = null;
connect() {
useResize(this, { element: this.truncateTarget });
this.update();
}
resize() {
this.update();
}
expanderTargetConnected(_target:HTMLElement) {
this.abortController = new AbortController();
const { signal } = this.abortController;
this.expanderButton.addEventListener('click', () => this.expanderClicked(), { signal });
}
expanderTargetDisconnected(_target:HTMLElement) {
this.abortController?.abort();
}
expandedValueChanged(value:boolean) {
this.expanderButton.setAttribute('aria-label', value ? I18n.t('js.label_collapse_text') : I18n.t('js.label_expand_text'));
this.expanderButton.setAttribute('aria-expanded', String(value));
this.truncateTarget.classList.toggle('Truncate--expanded', value);
this.update(); // Redundant call to ensure state consistency; the resize observer will likely trigger this anyway.
}
get truncateText():HTMLElement {
return this.truncateTarget.querySelector<HTMLElement>('.Truncate-text')!;
}
get expanderButton():HTMLButtonElement {
return this.expanderTarget.querySelector<HTMLButtonElement>('button')!;
}
private update() {
const truncated = this.truncateText.scrollWidth > this.truncateText.clientWidth;
this.expanderTarget.hidden = !truncated && !this.expandedValue;
}
private expanderClicked() {
this.expandedValue = !this.expandedValue;
}
}
+2
View File
@@ -39,6 +39,7 @@ import SelectAutosizeController from 'core-stimulus/controllers/select-autosize.
import OpZenModeController from 'core-stimulus/controllers/zen-mode.controller';
import CheckAllController from 'core-stimulus/controllers/check-all.controller';
import CheckableController from 'core-stimulus/controllers/checkable.controller';
import TruncationController from 'core-stimulus/controllers/truncation.controller';
declare global {
interface Window {
@@ -83,6 +84,7 @@ OpenProjectStimulusApplication.preregister('select-autosize', SelectAutosizeCont
OpenProjectStimulusApplication.preregister('editable-page-header-title', EditablePageHeaderTitleController);
OpenProjectStimulusApplication.preregister('check-all', CheckAllController);
OpenProjectStimulusApplication.preregister('checkable', CheckableController);
OpenProjectStimulusApplication.preregister('truncation', TruncationController);
const instance = OpenProjectStimulusApplication.start();
window.Stimulus = instance;
+3 -3
View File
@@ -112,9 +112,9 @@ RSpec.describe "Role creation", :js do
select type.name, from: "Type"
click_button "Edit"
from_id = existing_workflow.old_status_id
to_id = existing_workflow.new_status_id
old_status = existing_workflow.old_status.name
new_status = existing_workflow.new_status.name
expect(page).to have_field("status_#{from_id}_#{to_id}_", checked: true)
expect(page).to have_checked_field("Allow transition from #{old_status} to #{new_status}")
end
end
+146
View File
@@ -0,0 +1,146 @@
# frozen_string_literal: true
#-- 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.
#++
require "rails_helper"
RSpec.describe "Roles report", :js, :selenium do
shared_let(:admin) { create(:admin) }
let(:project) { create(:project, name: "Project 1", identifier: "project1") }
let(:permissions) { %i[view_project permission1 permission2] }
let!(:role1) { create(:global_role, permissions:, name: "Global IT MGMT") }
let!(:role2) { create(:global_role, permissions:, name: "Unsure Off-Shore") }
current_user { admin }
before do
visit report_roles_path
end
it "allows checking and unchecking by row" do
expect(page).to have_heading "Permissions report"
expect(page).to be_axe_clean
.within("#content")
.skipping("nested-interactive") # TODO: fix Collapsible Sections
expect(page).to have_region "Project"
within_region "Project" do
expect(page).to have_selector :table, "Permissions matrix for Project module"
expect(page).to have_unchecked_field "Assign Create projects permission to Global IT MGMT role"
check "Assign Create projects permission to Global IT MGMT role"
# mixed state
expect(page).to have_checked_field "Assign Create projects permission to Global IT MGMT role"
row = find(:row, "Create projects")
row.click_on accessible_name: "Toggle Create projects permission for all roles"
# stays checked
expect(page).to have_checked_field "Assign Create projects permission to Global IT MGMT role"
# mixed -> all checked
expect(row.all(:checkbox, minimum: 1)).to all(match_selector(:checkbox, checked: true))
row.click_on accessible_name: "Toggle Create projects permission for all roles"
# all checked -> all unchecked
expect(page).to have_unchecked_field "Assign Create projects permission to Global IT MGMT role"
expect(row.all(:checkbox, minimum: 1)).to all(match_selector(:checkbox, unchecked: true))
end
click_on "Save"
expect_and_dismiss_flash type: :success, message: "Successful update."
end
it "allows checking and unchecking by column" do
expect(page).to have_heading "Permissions report"
expect(page).to be_axe_clean
.within("#content")
.skipping("nested-interactive") # TODO: fix Collapsible Sections
expect(page).to have_region "Project"
within_region "Project" do
expect(page).to have_selector :table, "Permissions matrix for Project module"
expect(page).to have_unchecked_field "Assign Create projects permission to Global IT MGMT role"
check "Assign Create projects permission to Global IT MGMT role"
# mixed state
expect(page).to have_checked_field "Assign Create projects permission to Global IT MGMT role"
col_header = find("th", text: "GLOBAL IT MGMT")
col_header.click_on accessible_name: "Toggle all Project permissions for Global IT MGMT role"
# stays checked
expect(page).to have_checked_field "Assign Create projects permission to Global IT MGMT role"
# mixed -> all checked
col_index = col_header.all(:xpath, "preceding-sibling::th").size + 1
all_checkboxes = all("tbody tr td:nth-child(#{col_index})").flat_map { it.all(:checkbox, wait: 0) }
expect(all_checkboxes).to all(match_selector(:checkbox, checked: true))
col_header.click_on accessible_name: "Toggle all Project permissions for Global IT MGMT role"
# all checked -> all unchecked
expect(page).to have_unchecked_field "Assign Create projects permission to Global IT MGMT role"
all_checkboxes = all("tbody tr td:nth-child(#{col_index})").flat_map { it.all(:checkbox, wait: 0) }
expect(all_checkboxes).to all(match_selector(:checkbox, unchecked: true))
end
click_on "Save"
expect_and_dismiss_flash type: :success, message: "Successful update."
end
it "allows checking and unchecking all" do
expect(page).to have_heading "Permissions report"
expect(page).to be_axe_clean
.within("#content")
.skipping("nested-interactive") # TODO: fix Collapsible Sections
within("#project-section") do # FIXME: collapsible section semantics
expect(page).to have_unchecked_field "Create projects"
click_on "Check all"
expect(page).to have_checked_field "Create projects"
expect(all(:checkbox)).to all(match_selector(:checkbox, checked: true))
click_on "Uncheck all"
expect(page).to have_unchecked_field "Create projects"
expect(all(:checkbox)).to all(match_selector(:checkbox, unchecked: true))
end
click_on "Save"
expect_and_dismiss_flash type: :success, message: "Successful update."
end
end
+21 -21
View File
@@ -56,7 +56,7 @@ RSpec.describe "Workflow edit" do
click_button "Edit"
within "#workflow_form_always" do
check "status_#{statuses[1].id}_#{statuses[2].id}_"
check "status_#{statuses[1].id}_#{statuses[2].id}"
end
click_button "Save"
@@ -65,18 +65,18 @@ RSpec.describe "Workflow edit" do
within "#workflow_form_always" do
expect(page)
.to have_field "status_#{statuses[0].id}_#{statuses[1].id}_", checked: true
.to have_field "status_#{statuses[0].id}_#{statuses[1].id}", checked: true
expect(page)
.to have_field "status_#{statuses[1].id}_#{statuses[2].id}_", checked: true
.to have_field "status_#{statuses[1].id}_#{statuses[2].id}", checked: true
expect(page)
.to have_field "status_#{statuses[0].id}_#{statuses[2].id}_", checked: false
.to have_field "status_#{statuses[0].id}_#{statuses[2].id}", checked: false
expect(page)
.to have_field "status_#{statuses[1].id}_#{statuses[0].id}_", checked: false
.to have_field "status_#{statuses[1].id}_#{statuses[0].id}", checked: false
expect(page)
.to have_field "status_#{statuses[2].id}_#{statuses[0].id}_", checked: false
.to have_field "status_#{statuses[2].id}_#{statuses[0].id}", checked: false
expect(page)
.to have_field "status_#{statuses[2].id}_#{statuses[1].id}_", checked: false
.to have_field "status_#{statuses[2].id}_#{statuses[1].id}", checked: false
expect(Workflow.where(type_id: type.id, role_id: role.id).count).to be 2
@@ -95,7 +95,7 @@ RSpec.describe "Workflow edit" do
click_button "Edit"
within "#workflow_form_author" do
check "status_#{statuses[2].id}_#{statuses[1].id}_"
check "status_#{statuses[2].id}_#{statuses[1].id}"
end
click_button "Save"
@@ -104,18 +104,18 @@ RSpec.describe "Workflow edit" do
within "#workflow_form_author" do
expect(page)
.to have_field "status_#{statuses[2].id}_#{statuses[1].id}_", checked: true
.to have_field "status_#{statuses[2].id}_#{statuses[1].id}", checked: true
expect(page)
.to have_field "status_#{statuses[0].id}_#{statuses[1].id}_", checked: false
.to have_field "status_#{statuses[0].id}_#{statuses[1].id}", checked: false
expect(page)
.to have_field "status_#{statuses[1].id}_#{statuses[2].id}_", checked: false
.to have_field "status_#{statuses[1].id}_#{statuses[2].id}", checked: false
expect(page)
.to have_field "status_#{statuses[0].id}_#{statuses[2].id}_", checked: false
.to have_field "status_#{statuses[0].id}_#{statuses[2].id}", checked: false
expect(page)
.to have_field "status_#{statuses[1].id}_#{statuses[0].id}_", checked: false
.to have_field "status_#{statuses[1].id}_#{statuses[0].id}", checked: false
expect(page)
.to have_field "status_#{statuses[2].id}_#{statuses[0].id}_", checked: false
.to have_field "status_#{statuses[2].id}_#{statuses[0].id}", checked: false
expect(Workflow.where(type_id: type.id, role_id: role.id).count).to be 2
@@ -136,7 +136,7 @@ RSpec.describe "Workflow edit" do
click_button "Edit"
within "#workflow_form_assignee" do
check "status_#{statuses[2].id}_#{statuses[0].id}_"
check "status_#{statuses[2].id}_#{statuses[0].id}"
end
click_button "Save"
@@ -145,18 +145,18 @@ RSpec.describe "Workflow edit" do
within "#workflow_form_assignee" do
expect(page)
.to have_field "status_#{statuses[2].id}_#{statuses[0].id}_", checked: true
.to have_field "status_#{statuses[2].id}_#{statuses[0].id}", checked: true
expect(page)
.to have_field "status_#{statuses[0].id}_#{statuses[1].id}_", checked: false
.to have_field "status_#{statuses[0].id}_#{statuses[1].id}", checked: false
expect(page)
.to have_field "status_#{statuses[1].id}_#{statuses[2].id}_", checked: false
.to have_field "status_#{statuses[1].id}_#{statuses[2].id}", checked: false
expect(page)
.to have_field "status_#{statuses[0].id}_#{statuses[2].id}_", checked: false
.to have_field "status_#{statuses[0].id}_#{statuses[2].id}", checked: false
expect(page)
.to have_field "status_#{statuses[1].id}_#{statuses[0].id}_", checked: false
.to have_field "status_#{statuses[1].id}_#{statuses[0].id}", checked: false
expect(page)
.to have_field "status_#{statuses[2].id}_#{statuses[1].id}_", checked: false
.to have_field "status_#{statuses[2].id}_#{statuses[1].id}", checked: false
expect(Workflow.where(type_id: type.id, role_id: role.id).count).to be 2