Skip to content

Commit 720e3f3

Browse files
authored
Merge pull request #3250 from nextcloud/enh/e2e-tests
Add more E2E tests
2 parents a6b818f + 218f7b1 commit 720e3f3

15 files changed

Lines changed: 805 additions & 28 deletions
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { expect, mergeTests } from '@playwright/test'
7+
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'
10+
import { test as submitTest } from '../support/fixtures/submit'
11+
import { test as topBarTest } from '../support/fixtures/topBar'
12+
import { waitForApiResponse } from '../support/helpers'
13+
import { QuestionType } from '../support/sections/QuestionType'
14+
import { FormsView } from '../support/sections/TopBarSection'
15+
16+
const test = mergeTests(
17+
randomUserTest,
18+
appNavigationTest,
19+
formTest,
20+
topBarTest,
21+
submitTest,
22+
)
23+
24+
test.describe('Form settings', () => {
25+
// Setup: create a form with one question, open the Settings sidebar tab
26+
test.beforeEach(async ({ page, appNavigation, form }) => {
27+
await page.goto('apps/forms')
28+
await page.waitForURL(/apps\/forms\/?$/)
29+
await appNavigation.clickNewForm()
30+
await form.fillTitle('Settings test form')
31+
32+
await form.addQuestion(QuestionType.ShortAnswer)
33+
const questions = await form.getQuestions()
34+
await questions[0].fillTitle('Your answer')
35+
36+
// Open sidebar and switch to Settings tab
37+
await page.getByRole('button', { name: /Share/ }).click()
38+
const settingsTab = page.getByRole('tab', { name: /Settings/ })
39+
await settingsTab.click()
40+
await expect(
41+
page.getByRole('checkbox', { name: /Close form/ }),
42+
).toBeVisible()
43+
})
44+
45+
test('Closing a form blocks submissions', async ({ page, topBar }) => {
46+
const saved = waitForApiResponse(page, 'PATCH')
47+
await page
48+
.getByRole('checkbox', { name: /Close form/ })
49+
.click({ force: true })
50+
await saved
51+
52+
await topBar.toggleView(FormsView.View)
53+
54+
// NcEmptyContent renders with role="note" — scope to main to avoid
55+
// matching the "Form closed" status text in the sidebar navigation.
56+
const main = page.getByRole('main')
57+
await expect(main.getByText('Form closed')).toBeVisible()
58+
await expect(
59+
main.getByText('This form was closed and is no longer taking responses'),
60+
).toBeVisible()
61+
})
62+
63+
test('Reopening a closed form allows submissions', async ({
64+
page,
65+
topBar,
66+
submitView,
67+
}) => {
68+
// Close the form
69+
const closed = waitForApiResponse(page, 'PATCH')
70+
await page
71+
.getByRole('checkbox', { name: /Close form/ })
72+
.click({ force: true })
73+
await closed
74+
75+
// Reopen the form
76+
const reopened = waitForApiResponse(page, 'PATCH')
77+
await page
78+
.getByRole('checkbox', { name: /Close form/ })
79+
.click({ force: true })
80+
await reopened
81+
82+
await topBar.toggleView(FormsView.View)
83+
84+
// Form should be accessible — questions visible and submit button present
85+
await expect(submitView.submitButton).toBeVisible()
86+
})
87+
88+
test('Anonymous mode shows anonymous message on submit view', async ({
89+
page,
90+
topBar,
91+
}) => {
92+
const saved = waitForApiResponse(page, 'PATCH')
93+
await page
94+
.getByRole('checkbox', { name: /Store responses anonymously/ })
95+
.click({ force: true })
96+
await saved
97+
98+
await topBar.toggleView(FormsView.View)
99+
100+
await expect(page.getByText('Responses are anonymous.')).toBeVisible()
101+
})
102+
103+
test('Non-anonymous mode shows account-connected message on edit view', async ({
104+
page,
105+
}) => {
106+
// The Create (edit) view always shows this message when anonymous
107+
// is off, because the editor is always in a logged-in context.
108+
// The Submit route doesn't receive isLoggedIn from the router, so
109+
// the message only appears on the edit view.
110+
await expect(
111+
page.getByText('Responses are connected to your account.'),
112+
).toBeVisible()
113+
})
114+
})
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
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'
9+
import { test as formTest } from '../support/fixtures/form'
10+
import { QuestionType } from '../support/sections/QuestionType'
11+
import { waitForApiResponse } from '../support/helpers'
12+
13+
const test = mergeTests(
14+
randomUserTest,
15+
appNavigationTest,
16+
formTest,
17+
)
18+
19+
test.describe('Form sharing', () => {
20+
test.beforeEach(async ({ page, appNavigation, form }) => {
21+
await page.goto('apps/forms')
22+
await page.waitForURL(/apps\/forms\/?$/)
23+
await appNavigation.clickNewForm()
24+
await form.fillTitle('Sharing test form')
25+
26+
await form.addQuestion(QuestionType.ShortAnswer)
27+
const questions = await form.getQuestions()
28+
await questions[0].fillTitle('Test question')
29+
30+
// Open the sidebar via the Share button in the TopBar
31+
await page.getByRole('button', { name: /Share/ }).click()
32+
// Sidebar opens on the Sharing tab by default — wait for it
33+
await expect(
34+
page.getByRole('complementary').getByText('Share link'),
35+
).toBeVisible()
36+
})
37+
38+
test('Add a public share link', async ({ page }) => {
39+
// New forms start without a public link share.
40+
// NcActions with a single child renders it as an inline button.
41+
const shareLinkRow = page.locator('.share-div--link')
42+
const addLinkButton = shareLinkRow.getByRole('button', {
43+
name: /Add link/,
44+
})
45+
await expect(addLinkButton).toBeVisible()
46+
47+
const linkCreated = waitForApiResponse(page, 'POST')
48+
await addLinkButton.click()
49+
await linkCreated
50+
51+
// After adding, the share link entry renders NcActions with :inline="1",
52+
// so the first action ("Copy to clipboard") appears as an inline button.
53+
await expect(
54+
shareLinkRow.getByRole('link', { name: /Copy to clipboard/ }),
55+
).toBeVisible()
56+
})
57+
58+
test('Remove a public share link', async ({ page }) => {
59+
// First, add a link
60+
const shareLinkRow = page.locator('.share-div--link')
61+
62+
const linkCreated = waitForApiResponse(page, 'POST')
63+
await shareLinkRow.getByRole('button', { name: /Add link/ }).click()
64+
await linkCreated
65+
66+
// The inline "Copy to clipboard" action should now be visible
67+
await expect(
68+
shareLinkRow.getByRole('link', { name: /Copy to clipboard/ }),
69+
).toBeVisible()
70+
71+
// Open the overflow menu (the "Actions" toggle) to find "Remove link".
72+
// NcActions :inline="1" renders the first action inline and puts the
73+
// rest behind an overflow toggle button.
74+
await shareLinkRow.getByRole('button', { name: /Actions/ }).click()
75+
76+
const linkDeleted = waitForApiResponse(page, 'DELETE')
77+
await page.getByRole('menuitem', { name: /Remove link/ }).click()
78+
await linkDeleted
79+
80+
// After removal, the share link row reverts to the "no link" state.
81+
// NcActions collapses a single action to an inline button.
82+
await expect(
83+
shareLinkRow.getByRole('button', { name: /Add link/ }),
84+
).toBeVisible()
85+
})
86+
})
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
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'
9+
import { test as formTest } from '../support/fixtures/form'
10+
import { QuestionType } from '../support/sections/QuestionType'
11+
12+
const test = mergeTests(randomUserTest, appNavigationTest, formTest)
13+
14+
test.describe('Question editing lifecycle', () => {
15+
test.beforeEach(async ({ page, appNavigation, form }) => {
16+
await page.goto('apps/forms')
17+
await page.waitForURL(/apps\/forms\/?$/)
18+
await appNavigation.clickNewForm()
19+
await form.fillTitle('Editing test form')
20+
})
21+
22+
const questionTypes = [
23+
QuestionType.ShortAnswer,
24+
QuestionType.LongAnswer,
25+
QuestionType.Checkboxes,
26+
QuestionType.RadioButtons,
27+
QuestionType.Dropdown,
28+
QuestionType.Date,
29+
QuestionType.LinearScale,
30+
QuestionType.Color,
31+
]
32+
33+
for (const type of questionTypes) {
34+
test(`Add a ${type} question`, async ({ form }) => {
35+
await form.addQuestion(type)
36+
37+
const questions = await form.getQuestions()
38+
expect(questions).toHaveLength(1)
39+
await expect(questions[0].titleInput).toBeVisible()
40+
})
41+
}
42+
43+
test('Edit question title and description', async ({ form }) => {
44+
await form.addQuestion(QuestionType.ShortAnswer)
45+
46+
const questions = await form.getQuestions()
47+
const question = questions[0]
48+
49+
await question.fillTitle('What is your name?')
50+
await expect(question.titleInput).toHaveValue('What is your name?')
51+
52+
await question.fillDescription('Please enter your full name')
53+
await expect(question.descriptionInput).toHaveValue(
54+
'Please enter your full name',
55+
)
56+
})
57+
58+
test('Add answer options to a checkbox question', async ({ form }) => {
59+
await form.addQuestion(QuestionType.Checkboxes)
60+
61+
const questions = await form.getQuestions()
62+
const question = questions[0]
63+
64+
await question.addAnswer('Option A')
65+
await question.addAnswer('Option B')
66+
await question.addAnswer('Option C')
67+
68+
await expect(question.answerInputs).toHaveCount(3)
69+
})
70+
71+
test('Delete a question', async ({ page, form }) => {
72+
await form.addQuestion(QuestionType.ShortAnswer)
73+
await form.addQuestion(QuestionType.LongAnswer)
74+
75+
let questions = await form.getQuestions()
76+
expect(questions).toHaveLength(2)
77+
await questions[0].fillTitle('First question')
78+
await questions[1].fillTitle('Second question')
79+
80+
await questions[0].delete()
81+
82+
// Wait for Vue to re-render after deletion before taking a snapshot
83+
await expect(
84+
page.getByRole('listitem', { name: /Question number \d+/i }),
85+
).toHaveCount(1)
86+
questions = await form.getQuestions()
87+
await expect(questions[0].titleInput).toHaveValue('Second question')
88+
})
89+
90+
test('Clone a question', async ({ form }) => {
91+
await form.addQuestion(QuestionType.Checkboxes)
92+
93+
const questions = await form.getQuestions()
94+
const question = questions[0]
95+
await question.fillTitle('Favorite colors')
96+
await question.addAnswer('Red')
97+
await question.addAnswer('Blue')
98+
99+
await question.clone()
100+
101+
const updatedQuestions = await form.getQuestions()
102+
expect(updatedQuestions).toHaveLength(2)
103+
104+
// The clone should have the same title and options
105+
const clone = updatedQuestions[1]
106+
await expect(clone.titleInput).toHaveValue('Favorite colors')
107+
await expect(clone.answerInputs).toHaveCount(2)
108+
})
109+
})
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { expect, mergeTests } from '@playwright/test'
7+
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'
10+
import { test as submitTest } from '../support/fixtures/submit'
11+
import { test as topBarTest } from '../support/fixtures/topBar'
12+
import { QuestionType } from '../support/sections/QuestionType'
13+
import { FormsView } from '../support/sections/TopBarSection'
14+
15+
const test = mergeTests(
16+
randomUserTest,
17+
appNavigationTest,
18+
formTest,
19+
topBarTest,
20+
submitTest,
21+
)
22+
23+
test.describe('Required field validation', () => {
24+
// Setup: create form with 2 questions, mark the first as required
25+
test.beforeEach(async ({ page, appNavigation, form }) => {
26+
await page.goto('apps/forms')
27+
await page.waitForURL(/apps\/forms\/?$/)
28+
await appNavigation.clickNewForm()
29+
await form.fillTitle('Required fields test')
30+
31+
// Add a required short answer
32+
await form.addQuestion(QuestionType.ShortAnswer)
33+
const questions = await form.getQuestions()
34+
await questions[0].fillTitle('Required field')
35+
36+
await questions[0].toggleRequired()
37+
38+
// Add a non-required question
39+
await form.addQuestion(QuestionType.ShortAnswer)
40+
const questions2 = await form.getQuestions()
41+
await questions2[1].fillTitle('Optional field')
42+
})
43+
44+
test('Submit with empty required field shows validation error', async ({
45+
topBar,
46+
submitView,
47+
}) => {
48+
await topBar.toggleView(FormsView.View)
49+
50+
// Fill only the optional field
51+
await submitView.fillText('Optional field', 'some text')
52+
53+
// Try to submit — should fail due to required field
54+
await submitView.submitButton.click()
55+
56+
// The form should NOT show success message (submission blocked by HTML5 validation)
57+
await expect(submitView.successMessage).not.toBeVisible()
58+
})
59+
60+
test('Submit succeeds after filling required field', async ({
61+
topBar,
62+
submitView,
63+
}) => {
64+
await topBar.toggleView(FormsView.View)
65+
66+
await submitView.fillText('Required field', 'my answer')
67+
await submitView.submit()
68+
await expect(submitView.successMessage).toBeVisible()
69+
})
70+
})

0 commit comments

Comments
 (0)