diff --git a/playwright/e2e/edit-submission.spec.ts b/playwright/e2e/edit-submission.spec.ts new file mode 100644 index 000000000..2fa21ad1e --- /dev/null +++ b/playwright/e2e/edit-submission.spec.ts @@ -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() + }) +}) diff --git a/playwright/support/sections/FormSection.ts b/playwright/support/sections/FormSection.ts index 6dea1c2eb..f934ca7ed 100644 --- a/playwright/support/sections/FormSection.ts +++ b/playwright/support/sections/FormSection.ts @@ -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' @@ -40,10 +40,16 @@ export class FormSection { await update } - public async addQuestion(type: QuestionType): Promise { + public async addQuestion( + type: QuestionType, + subtype?: GridSubtype, + ): Promise { 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 } diff --git a/playwright/support/sections/QuestionSection.ts b/playwright/support/sections/QuestionSection.ts index 39576f77f..ebce8f446 100644 --- a/playwright/support/sections/QuestionSection.ts +++ b/playwright/support/sections/QuestionSection.ts @@ -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( @@ -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, }) @@ -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 { + 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 { + const saved = waitForApiResponse(this.page, 'POST') + await this.newRowInput.fill(text) + await this.newRowInput.press('Enter') + await saved + } + async openActionsMenu(): Promise { await this.section .getByRole('button', { name: 'Actions', exact: true }) diff --git a/playwright/support/sections/QuestionType.ts b/playwright/support/sections/QuestionType.ts index cb9cecad2..1f51c8f70 100644 --- a/playwright/support/sections/QuestionType.ts +++ b/playwright/support/sections/QuestionType.ts @@ -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', +} diff --git a/playwright/support/sections/ResultsSection.ts b/playwright/support/sections/ResultsSection.ts index ac13b5ace..1fa61ec6f 100644 --- a/playwright/support/sections/ResultsSection.ts +++ b/playwright/support/sections/ResultsSection.ts @@ -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 { + 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+$/) + } } diff --git a/playwright/support/sections/SubmitSection.ts b/playwright/support/sections/SubmitSection.ts index 80ea540bd..01518cd15 100644 --- a/playwright/support/sections/SubmitSection.ts +++ b/playwright/support/sections/SubmitSection.ts @@ -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 { + 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 { + const cellInput = await this.getGridCellInput( + questionName, + rowName, + columnName, + ) + await cellInput.check({ force: true }) + } + /** * Select a dropdown option. * QuestionDropdown renders NcSelect which uses role="combobox". diff --git a/src/types/Entities.d.ts b/src/types/Entities.d.ts index b72b37dcf..7f531ce13 100644 --- a/src/types/Entities.d.ts +++ b/src/types/Entities.d.ts @@ -136,7 +136,10 @@ export interface MaxStringLengths { [key: string]: number } -export type GridQuestionValues = Record +export type GridQuestionValues = Record< + number, + number | string | (number | string)[] | Record +> /** Statistics for a single option in a countable question (e.g., multiple choice, checkboxes) */ export interface OptionStats extends FormsOption { diff --git a/src/views/Submit.vue b/src/views/Submit.vue index 9439c8142..78179469d 100644 --- a/src/views/Submit.vue +++ b/src/views/Submit.vue @@ -219,7 +219,11 @@