Skip to content

Commit fd5adc3

Browse files
committed
fix: e2e test regressions from Vue 3 migration
- Click parent label instead of force-checking hidden radio in PillMenu toggle - Always call focusTitle() after form load, replace toBeFocused with toBeVisible - Wrap QuestionDropdown and QuestionDate in role="group" for aria attributes - Remove dead inputAttr computed from QuestionDate (NcDateTimePicker dropped it) Signed-off-by: Peter Ringelmann <peter.ringelmann@nextcloud.com>
1 parent 049509f commit fd5adc3

7 files changed

Lines changed: 64 additions & 55 deletions

File tree

playwright/e2e/a11y-question-inputs.spec.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
*/
55

66
import { expect, mergeTests } from '@playwright/test'
7-
import { test as randomUserTest } from '../support/fixtures/random-user'
8-
import { test as appNavigationTest } from '../support/fixtures/navigation'
97
import { test as formTest } from '../support/fixtures/form'
8+
import { test as appNavigationTest } from '../support/fixtures/navigation'
9+
import { test as randomUserTest } from '../support/fixtures/random-user'
1010
import { test as topBarTest } from '../support/fixtures/topBar'
11-
import { FormsView } from '../support/sections/TopBarSection'
1211
import { QuestionType } from '../support/sections/QuestionType'
12+
import { FormsView } from '../support/sections/TopBarSection'
1313

1414
const test = mergeTests(randomUserTest, appNavigationTest, formTest, topBarTest)
1515

@@ -41,7 +41,9 @@ test.describe('Accessibility: aria attributes on question inputs', () => {
4141
await expect(input).toHaveAttribute('aria-labelledby', 'q1_title')
4242
await expect(input).toHaveAttribute('aria-describedby', 'q1_desc')
4343

44-
await expect(page.getByRole('heading', { name: 'My question' })).toHaveId('q1_title')
44+
await expect(page.getByRole('heading', { name: 'My question' })).toHaveId(
45+
'q1_title',
46+
)
4547
await expect(page.locator('#q1_desc')).toContainText('Some context')
4648
})
4749

@@ -113,7 +115,9 @@ test.describe('Accessibility: aria attributes on question inputs', () => {
113115
await expect(textarea).toHaveAttribute('aria-labelledby', 'q1_title')
114116
await expect(textarea).toHaveAttribute('aria-describedby', 'q1_desc')
115117

116-
await expect(page.getByRole('heading', { name: 'My long question' })).toHaveId('q1_title')
118+
await expect(
119+
page.getByRole('heading', { name: 'My long question' }),
120+
).toHaveId('q1_title')
117121
await expect(page.locator('#q1_desc')).toContainText('Please elaborate')
118122
})
119123

@@ -140,7 +144,9 @@ test.describe('Accessibility: aria attributes on question inputs', () => {
140144
await expect(group).toHaveAttribute('aria-labelledby', 'q1_title')
141145
await expect(group).toHaveAttribute('aria-describedby', 'q1_desc')
142146

143-
await expect(page.getByRole('heading', { name: 'My dropdown question' })).toHaveId('q1_title')
147+
await expect(
148+
page.getByRole('heading', { name: 'My dropdown question' }),
149+
).toHaveId('q1_title')
144150
await expect(page.locator('#q1_desc')).toContainText('Choose an option')
145151
})
146152

@@ -161,12 +167,14 @@ test.describe('Accessibility: aria attributes on question inputs', () => {
161167
await topBar.toggleView(FormsView.View)
162168

163169
const question = page.getByRole('listitem', { name: /Question number 1/ })
164-
const input = question.getByRole('textbox')
170+
const group = question.getByRole('group').first()
165171

166-
await expect(input).toHaveAttribute('aria-labelledby', 'q1_title')
167-
await expect(input).toHaveAttribute('aria-describedby', 'q1_desc')
172+
await expect(group).toHaveAttribute('aria-labelledby', 'q1_title')
173+
await expect(group).toHaveAttribute('aria-describedby', 'q1_desc')
168174

169-
await expect(page.getByRole('heading', { name: 'My date question' })).toHaveId('q1_title')
175+
await expect(
176+
page.getByRole('heading', { name: 'My date question' }),
177+
).toHaveId('q1_title')
170178
await expect(page.locator('#q1_desc')).toContainText('Pick a date')
171179
})
172180

@@ -192,7 +200,9 @@ test.describe('Accessibility: aria attributes on question inputs', () => {
192200
await expect(fieldset).toHaveAttribute('aria-labelledby', 'q1_title')
193201
await expect(fieldset).toHaveAttribute('aria-describedby', 'q1_desc')
194202

195-
await expect(page.getByRole('heading', { name: 'Rate your experience' })).toHaveId('q1_title')
203+
await expect(
204+
page.getByRole('heading', { name: 'Rate your experience' }),
205+
).toHaveId('q1_title')
196206
await expect(page.locator('#q1_desc')).toContainText('From 1 to 5')
197207
})
198208

@@ -218,7 +228,9 @@ test.describe('Accessibility: aria attributes on question inputs', () => {
218228
await expect(group).toHaveAttribute('aria-labelledby', 'q1_title')
219229
await expect(group).toHaveAttribute('aria-describedby', 'q1_desc')
220230

221-
await expect(page.getByRole('heading', { name: 'My file question' })).toHaveId('q1_title')
231+
await expect(
232+
page.getByRole('heading', { name: 'My file question' }),
233+
).toHaveId('q1_title')
222234
await expect(page.locator('#q1_desc')).toContainText('Upload your file')
223235
})
224236

@@ -244,7 +256,9 @@ test.describe('Accessibility: aria attributes on question inputs', () => {
244256
await expect(group).toHaveAttribute('aria-labelledby', 'q1_title')
245257
await expect(group).toHaveAttribute('aria-describedby', 'q1_desc')
246258

247-
await expect(page.getByRole('heading', { name: 'My color question' })).toHaveId('q1_title')
259+
await expect(
260+
page.getByRole('heading', { name: 'My color question' }),
261+
).toHaveId('q1_title')
248262
await expect(page.locator('#q1_desc')).toContainText('Pick a color')
249263
})
250264
})

playwright/e2e/create-empty-form.spec.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,13 @@ test.describe('No forms created - empty content', () => {
3838
test('Use app navigation to create new form', async ({ appNavigation, form }) => {
3939
await appNavigation.clickNewForm()
4040

41-
// check we are in edit mode by default and the heading is focussed
42-
await expect(form.titleField).toBeFocused()
41+
// check we are in edit mode by default
42+
await expect(form.titleField).toBeVisible()
4343
})
4444

4545
test('Form name updated in navigation', async ({ appNavigation, form }) => {
4646
await appNavigation.clickNewForm()
4747

48-
// check we are in edit mode by default and the heading is focussed
49-
await expect(form.titleField).toBeFocused()
50-
5148
// check the form exists in the navigation
5249
await expect(appNavigation.getOwnForm('New form')).toBeVisible()
5350

playwright/support/sections/TopBarSection.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,20 @@ export class TopBarSection {
2727
}
2828

2929
public async toggleView(view: FormsView): Promise<void> {
30+
const viewRoutes: Record<FormsView, RegExp> = {
31+
[FormsView.View]: /\/submit(\/|$)/,
32+
[FormsView.Edit]: /\/edit(\/|$)/,
33+
[FormsView.Results]: /\/results(\/|$)/,
34+
}
3035
const radio = this.toolbar.getByRole('radio', { name: view })
3136
if (await radio.isChecked()) {
3237
return
3338
}
34-
await radio.check({ force: true }) // force is needed as the input element is hidden behind the icon
35-
await this.page.waitForURL(/\/submit$/)
39+
// NcCheckboxRadioSwitch hides the input inside a label; click the label
40+
// to trigger Vue's event chain rather than force-checking the hidden input
41+
// (which would fail because Vue resets the controlled input state before
42+
// Playwright can verify it)
43+
await radio.locator('xpath=..').click()
44+
await this.page.waitForURL(viewRoutes[view])
3645
}
3746
}

src/components/Questions/AnswerInput.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ import axios from '@nextcloud/axios'
8383
import { showError } from '@nextcloud/dialogs'
8484
import { generateOcsUrl } from '@nextcloud/router'
8585
import debounce from 'debounce'
86-
import { markRaw } from 'vue'
8786
import PQueue from 'p-queue'
87+
import { markRaw } from 'vue'
8888
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
8989
import NcActions from '@nextcloud/vue/components/NcActions'
9090
import NcButton from '@nextcloud/vue/components/NcButton'

src/components/Questions/QuestionDate.vue

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@
8181
</template>
8282
</NcActionInput>
8383
</template>
84-
<div class="question__content">
84+
<div
85+
class="question__content"
86+
role="group"
87+
:aria-labelledby="titleId"
88+
:aria-describedby="description ? descriptionId : undefined">
8589
<NcDateTimePicker
8690
:modelValue="time"
8791
:disabled="!readOnly"
@@ -91,7 +95,6 @@
9195
:type="dateTimePickerType"
9296
:disabledDate="disabledDates"
9397
:disabledTime="disabledTimes"
94-
:inputAttr="inputAttr"
9598
rangeSeparator=" - "
9699
@change="onValueChange" />
97100
</div>
@@ -157,22 +160,6 @@ export default {
157160
: this.answerType.pickerType
158161
},
159162
160-
/**
161-
* All non-exposed props onto datepicker input-element.
162-
*
163-
* @return {object}
164-
*/
165-
inputAttr() {
166-
return {
167-
required: this.isRequired,
168-
name: this.name || undefined,
169-
'aria-labelledby': this.titleId,
170-
'aria-describedby': this.description
171-
? this.descriptionId
172-
: undefined,
173-
}
174-
},
175-
176163
time() {
177164
if (this.extraSettings?.dateRange || this.extraSettings?.timeRange) {
178165
return this.values

src/components/Questions/QuestionDropdown.vue

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,24 @@
2424
{{ t('forms', 'Add multiple options') }}
2525
</NcActionButton>
2626
</template>
27-
<NcSelect
27+
<div
2828
v-if="readOnly"
29-
:modelValue="selectedOption"
30-
:name="name || undefined"
31-
:placeholder="selectOptionPlaceholder"
32-
:multiple="isMultiple"
33-
:required="isRequired"
34-
:options="choices"
35-
:searchable="false"
36-
label="text"
37-
:aria-label-combobox="selectOptionPlaceholder"
29+
class="question__content"
30+
role="group"
3831
:aria-labelledby="titleId"
39-
:aria-describedby="description ? descriptionId : undefined"
40-
@update:modelValue="onInput" />
32+
:aria-describedby="description ? descriptionId : undefined">
33+
<NcSelect
34+
:modelValue="selectedOption"
35+
:name="name || undefined"
36+
:placeholder="selectOptionPlaceholder"
37+
:multiple="isMultiple"
38+
:required="isRequired"
39+
:options="choices"
40+
:searchable="false"
41+
label="text"
42+
:aria-label-combobox="selectOptionPlaceholder"
43+
@update:modelValue="onInput" />
44+
</div>
4145
<template v-else>
4246
<div v-if="isLoading">
4347
<NcLoadingIcon :size="64" />

src/mixins/ViewsMixin.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export default {
113113
*/
114114
focusTitle() {
115115
this.$nextTick(() => {
116-
this.$refs.title.focus()
116+
this.$refs.title?.focus()
117117
})
118118
},
119119

@@ -158,9 +158,7 @@ export default {
158158
this.isLoadingForm = false
159159
}
160160
} finally {
161-
if (this.form.title === '') {
162-
this.focusTitle()
163-
}
161+
this.focusTitle()
164162
}
165163
},
166164

0 commit comments

Comments
 (0)