Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions playwright/e2e/edit-submission.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { expect, mergeTests } from '@playwright/test'
import { test as formTest } from '../support/fixtures/form.ts'
import { test as appNavigationTest } from '../support/fixtures/navigation.ts'
import { test as randomUserTest } from '../support/fixtures/random-user.ts'
import { test as resultsTest } from '../support/fixtures/results.ts'
import { test as submitTest } from '../support/fixtures/submit.ts'
import { test as topBarTest } from '../support/fixtures/topBar.ts'
import { GridSubtype, QuestionType } from '../support/sections/QuestionType.ts'
import { FormsView } from '../support/sections/TopBarSection.ts'

const test = mergeTests(
randomUserTest,
appNavigationTest,
formTest,
topBarTest,
submitTest,
resultsTest,
)

test.describe('Edit submission', () => {
// Setup: create a form with grid questions and submit a response
test.beforeEach(async ({ page, appNavigation, form, topBar, submitView }) => {
await page.goto('apps/forms')
await page.waitForURL(/apps\/forms\/?$/)
await appNavigation.clickNewForm()
await form.fillTitle('Edit submission test form')

// Add a checkbox grid question
await form.addQuestion(QuestionType.Grid, GridSubtype.Checkboxes)
const questions1 = await form.getQuestions()
await questions1[0].fillTitle('Skills matrix')
await questions1[0].addColumn('Python')
await questions1[0].addColumn('PHP')
await questions1[0].addRow('Beginner')
await questions1[0].addRow('Expert')

// Add a radio grid question
await form.addQuestion(QuestionType.Grid, GridSubtype.RadioButtons)
const questions2 = await form.getQuestions()
await questions2[1].fillTitle('Satisfaction')
await questions2[1].addColumn('Good')
await questions2[1].addColumn('Bad')
await questions2[1].addRow('Service')
await questions2[1].addRow('Price')

// Switch to View mode and submit a response
await topBar.toggleView(FormsView.View)
await submitView.checkGridCell('Skills matrix', 'Beginner', 'Python')
await submitView.checkGridCell('Skills matrix', 'Expert', 'PHP')
await submitView.checkGridCell('Satisfaction', 'Service', 'Good')
await submitView.submit()
await expect(submitView.successMessage).toBeVisible()
})

test('Grid answers are restored when editing a submission', async ({
page,
resultsView,
submitView,
}) => {
// Open the submission for editing from the responses tab
await page.goto(page.url().replace(/\/submit.*$/, '/results'))
await resultsView.switchToResponses()
await resultsView.editSubmission()

// Previously submitted grid answers are restored
await expect(
await submitView.getGridCellInput('Skills matrix', 'Beginner', 'Python'),
).toBeChecked()
await expect(
await submitView.getGridCellInput('Skills matrix', 'Expert', 'PHP'),
).toBeChecked()
await expect(
await submitView.getGridCellInput('Skills matrix', 'Beginner', 'PHP'),
).not.toBeChecked()
await expect(
await submitView.getGridCellInput('Satisfaction', 'Service', 'Good'),
).toBeChecked()

// The restored submission can be edited and submitted again
await submitView.checkGridCell('Skills matrix', 'Beginner', 'PHP')
await submitView.submit()
await expect(submitView.successMessage).toBeVisible()
})
})
10 changes: 8 additions & 2 deletions playwright/support/sections/FormSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import type { Locator, Page } from '@playwright/test'
import type { QuestionType } from './QuestionType.ts'
import type { GridSubtype, QuestionType } from './QuestionType.ts'

import { waitForApiResponse } from '../helpers.ts'
import { QuestionSection } from './QuestionSection.ts'
Expand Down Expand Up @@ -40,10 +40,16 @@ export class FormSection {
await update
}

public async addQuestion(type: QuestionType): Promise<void> {
public async addQuestion(
type: QuestionType,
subtype?: GridSubtype,
): Promise<void> {
const created = waitForApiResponse(this.page, 'POST')
await this.newQuestionButton.click()
await this.page.getByRole('menuitem', { name: type }).click()
if (subtype !== undefined) {
await this.page.getByRole('menuitem', { name: subtype }).click()
}
await created
}

Expand Down
32 changes: 32 additions & 0 deletions playwright/support/sections/QuestionSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export class QuestionSection {
public readonly titleInput: Locator
public readonly descriptionInput: Locator
public readonly newAnswerInput: Locator
public readonly newColumnInput: Locator
public readonly newRowInput: Locator
public readonly answerInputs: Locator

constructor(
Expand All @@ -26,6 +28,12 @@ export class QuestionSection {
this.newAnswerInput = this.section.getByRole('textbox', {
name: 'Add a new answer option',
})
this.newColumnInput = this.section.getByRole('textbox', {
name: 'Add a new column',
})
this.newRowInput = this.section.getByRole('textbox', {
name: 'Add a new row',
})
this.answerInputs = this.section.getByRole('textbox', {
name: /The text of option \d+/i,
})
Expand All @@ -50,6 +58,30 @@ export class QuestionSection {
await saved
}

/**
* Add a column option to a grid question.
*
* @param text the text of the column
*/
async addColumn(text: string): Promise<void> {
const saved = waitForApiResponse(this.page, 'POST')
await this.newColumnInput.fill(text)
await this.newColumnInput.press('Enter')
await saved
}

/**
* Add a row option to a grid question.
*
* @param text the text of the row
*/
async addRow(text: string): Promise<void> {
const saved = waitForApiResponse(this.page, 'POST')
await this.newRowInput.fill(text)
await this.newRowInput.press('Enter')
await saved
}

async openActionsMenu(): Promise<void> {
await this.section
.getByRole('button', { name: 'Actions', exact: true })
Expand Down
11 changes: 11 additions & 0 deletions playwright/support/sections/QuestionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,20 @@ export enum QuestionType {
Date = 'Date',
Dropdown = 'Dropdown',
File = 'File',
Grid = 'Grid',
LinearScale = 'Linear scale',
LongAnswer = 'Long text',
Ranking = 'Ranking',
RadioButtons = 'Radio buttons',
ShortAnswer = 'Short answer',
}

/**
* Cell type subtypes available for grid questions.
* Labels of the entries in the grid subtype menu.
*/
export enum GridSubtype {
Checkboxes = 'Checkboxes',
Number = 'Number',
RadioButtons = 'Radio buttons',
}
13 changes: 13 additions & 0 deletions playwright/support/sections/ResultsSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,17 @@ export class ResultsSection {
.getByRole('heading', { name })
.locator('..')
}

/**
* Open the "Edit this response" action of a submission on the
* responses tab and wait for the submission edit page.
*
* @param index index of the submission block, defaults to the first
*/
public async editSubmission(index = 0): Promise<void> {
const submission = this.page.locator('.submission').nth(index)
await submission.getByRole('button', { name: 'Actions' }).click()
await this.page.getByRole('menuitem', { name: 'Edit this response' }).click()
await this.page.waitForURL(/\/submit\/\d+$/)
}
}
46 changes: 46 additions & 0 deletions playwright/support/sections/SubmitSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,52 @@ export class SubmitSection {
.check({ force: true })
}

/**
* Get the input element of a grid question cell.
* QuestionGrid renders a table.answer-grid where the first column
* contains the row labels and each following cell one input
* (checkbox, radio or number input depending on the cell type).
*
* @param questionName the title of the question
* @param rowName the label of the grid row
* @param columnName the label of the grid column
*/
public async getGridCellInput(
questionName: string | RegExp,
rowName: string | RegExp,
columnName: string | RegExp,
): Promise<Locator> {
const grid = this.getQuestion(questionName).locator('table.answer-grid')
const columnIndex = await grid
.getByRole('columnheader', { name: columnName })
.evaluate((element) => (element as HTMLTableCellElement).cellIndex)
return grid
.getByRole('row', { name: rowName })
.getByRole('cell')
.nth(columnIndex)
.locator('input')
}

/**
* Check a checkbox or radio cell of a grid question.
*
* @param questionName the title of the question
* @param rowName the label of the grid row
* @param columnName the label of the grid column
*/
public async checkGridCell(
questionName: string | RegExp,
rowName: string | RegExp,
columnName: string | RegExp,
): Promise<void> {
const cellInput = await this.getGridCellInput(
questionName,
rowName,
columnName,
)
await cellInput.check({ force: true })
}

/**
* Select a dropdown option.
* QuestionDropdown renders NcSelect which uses role="combobox".
Expand Down
5 changes: 4 additions & 1 deletion src/types/Entities.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ export interface MaxStringLengths {
[key: string]: number
}

export type GridQuestionValues = Record<number, number | number[]>
export type GridQuestionValues = Record<
number,
number | string | (number | string)[] | Record<number, number | string>
>

/** Statistics for a single option in a countable question (e.g., multiple choice, checkboxes) */
export interface OptionStats extends FormsOption {
Expand Down
Loading
Loading