Skip to content
Merged
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
10 changes: 10 additions & 0 deletions lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ public function views(string $hash): TemplateResponse {
return $this->index($hash);
}

/**
* @return TemplateResponse
*/
#[NoAdminRequired()]
#[NoCSRFRequired()]
#[FrontpageRoute(verb: 'GET', url: '/{hash}/results/{view}', requirements: ['hash' => '[a-zA-Z0-9]{16,}'])]
public function resultsViews(string $hash): TemplateResponse {
return $this->index($hash);
}

/**
* @return TemplateResponse
*/
Expand Down
70 changes: 15 additions & 55 deletions playwright/e2e/results-view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,11 @@
await submitView.checkOption('Pick colors', 'Red')
await submitView.checkOption('Pick colors', 'Blue')
await submitView.submit()
await expect(submitView.successMessage).toBeVisible()

Check failure on line 52 in playwright/e2e/results-view.spec.ts

View workflow job for this annotation

GitHub Actions / test

[chromium] › playwright/e2e/results-view.spec.ts:75:2 › Results view › Responses tab shows individual submission

3) [chromium] › playwright/e2e/results-view.spec.ts:75:2 › Results view › Responses tab shows individual submission Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toBeVisible() failed Locator: getByText('Thank you for completing the form!') Expected: visible Error: strict mode violation: getByText('Thank you for completing the form!') resolved to 2 elements: 1) <div data-v-2f8d97d7="" aria-live="polite" class="hidden-visually">Thank you for completing the form!</div> aka getByText('Thank you for completing the').first() 2) <div id="nc-vue-87" data-v-8609a4c1="" class="empty-content__name">Thank you for completing the form!</div> aka getByLabel('Thank you for completing the').getByText('Thank you for completing the') Call log: - Expect "toBeVisible" with timeout 5000ms - waiting for getByText('Thank you for completing the form!') 50 | await submitView.checkOption('Pick colors', 'Blue') 51 | await submitView.submit() > 52 | await expect(submitView.successMessage).toBeVisible() | ^ 53 | 54 | // Navigate to Results view. The router will redirect to the summary tab. 55 | await page.goto(page.url().replace(/\/submit.*$/, '/results')) at /home/runner/actions-runner/_work/forms/forms/playwright/e2e/results-view.spec.ts:52:43

Check failure on line 52 in playwright/e2e/results-view.spec.ts

View workflow job for this annotation

GitHub Actions / test

[chromium] › playwright/e2e/results-view.spec.ts:75:2 › Results view › Responses tab shows individual submission

3) [chromium] › playwright/e2e/results-view.spec.ts:75:2 › Results view › Responses tab shows individual submission Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toBeVisible() failed Locator: getByText('Thank you for completing the form!') Expected: visible Error: strict mode violation: getByText('Thank you for completing the form!') resolved to 2 elements: 1) <div data-v-2f8d97d7="" aria-live="polite" class="hidden-visually">Thank you for completing the form!</div> aka getByText('Thank you for completing the').first() 2) <div id="nc-vue-87" data-v-8609a4c1="" class="empty-content__name">Thank you for completing the form!</div> aka getByLabel('Thank you for completing the').getByText('Thank you for completing the') Call log: - Expect "toBeVisible" with timeout 5000ms - waiting for getByText('Thank you for completing the form!') 50 | await submitView.checkOption('Pick colors', 'Blue') 51 | await submitView.submit() > 52 | await expect(submitView.successMessage).toBeVisible() | ^ 53 | 54 | // Navigate to Results view. The router will redirect to the summary tab. 55 | await page.goto(page.url().replace(/\/submit.*$/, '/results')) at /home/runner/actions-runner/_work/forms/forms/playwright/e2e/results-view.spec.ts:52:43

Check failure on line 52 in playwright/e2e/results-view.spec.ts

View workflow job for this annotation

GitHub Actions / test

[chromium] › playwright/e2e/results-view.spec.ts:75:2 › Results view › Responses tab shows individual submission

3) [chromium] › playwright/e2e/results-view.spec.ts:75:2 › Results view › Responses tab shows individual submission Error: expect(locator).toBeVisible() failed Locator: getByText('Thank you for completing the form!') Expected: visible Error: strict mode violation: getByText('Thank you for completing the form!') resolved to 2 elements: 1) <div data-v-2f8d97d7="" aria-live="polite" class="hidden-visually">Thank you for completing the form!</div> aka getByText('Thank you for completing the').first() 2) <div id="nc-vue-87" data-v-8609a4c1="" class="empty-content__name">Thank you for completing the form!</div> aka getByLabel('Thank you for completing the').getByText('Thank you for completing the') Call log: - Expect "toBeVisible" with timeout 5000ms - waiting for getByText('Thank you for completing the form!') 50 | await submitView.checkOption('Pick colors', 'Blue') 51 | await submitView.submit() > 52 | await expect(submitView.successMessage).toBeVisible() | ^ 53 | 54 | // Navigate to Results view. The router will redirect to the summary tab. 55 | await page.goto(page.url().replace(/\/submit.*$/, '/results')) at /home/runner/actions-runner/_work/forms/forms/playwright/e2e/results-view.spec.ts:52:43

Check failure on line 52 in playwright/e2e/results-view.spec.ts

View workflow job for this annotation

GitHub Actions / test

[chromium] › playwright/e2e/results-view.spec.ts:59:2 › Results view › Summary tab shows submitted data

2) [chromium] › playwright/e2e/results-view.spec.ts:59:2 › Results view › Summary tab shows submitted data Error: expect(locator).toBeVisible() failed Locator: getByText('Thank you for completing the form!') Expected: visible Error: strict mode violation: getByText('Thank you for completing the form!') resolved to 2 elements: 1) <div data-v-2f8d97d7="" aria-live="polite" class="hidden-visually">Thank you for completing the form!</div> aka getByText('Thank you for completing the').first() 2) <div id="nc-vue-87" data-v-8609a4c1="" class="empty-content__name">Thank you for completing the form!</div> aka getByLabel('Thank you for completing the').getByText('Thank you for completing the') Call log: - Expect "toBeVisible" with timeout 5000ms - waiting for getByText('Thank you for completing the form!') 50 | await submitView.checkOption('Pick colors', 'Blue') 51 | await submitView.submit() > 52 | await expect(submitView.successMessage).toBeVisible() | ^ 53 | 54 | // Navigate to Results view. The router will redirect to the summary tab. 55 | await page.goto(page.url().replace(/\/submit.*$/, '/results')) at /home/runner/actions-runner/_work/forms/forms/playwright/e2e/results-view.spec.ts:52:43

// Navigate to Results view via URL — the SPA route transition
// from submit → results after submission causes a brief redirect loop,
// so we use direct navigation instead of clicking the TopBar.
// Navigate to Results view. The router will redirect to the summary tab.
await page.goto(page.url().replace(/\/submit.*$/, '/results'))
await page.waitForURL(/\/results(?:\?.*)?$/)
await page.waitForURL(/\/results\/summary$/)
})

test('Summary tab shows submitted data', async ({ resultsView }) => {
Expand All @@ -80,76 +78,38 @@
// Should show the individual submission with the answers
await expect(resultsView.responsesTab).toBeChecked()
await expect(resultsView.responseCount).toBeVisible()
await expect(resultsView.page).toHaveURL(/\/results\?view=responses$/)
await expect(resultsView.page).toHaveURL(/\/results\/responses$/)
})

test('Tab switching between Summary and Responses updates the URL', async ({
resultsView,
}) => {
// Start on Summary
await expect(resultsView.summaryTab).toBeChecked()
await expect(resultsView.page).toHaveURL(/\/results\?view=summary$/)
await expect(resultsView.page).toHaveURL(/\/results\/summary$/)

// Switch to Responses
await resultsView.switchToResponses()
await expect(resultsView.responsesTab).toBeChecked()
await expect(resultsView.summaryTab).not.toBeChecked()
await expect(resultsView.page).toHaveURL(/\/results\?view=responses$/)
await expect(resultsView.page).toHaveURL(/\/results\/responses$/)

// Switch back to Summary
await resultsView.switchToSummary()
await expect(resultsView.summaryTab).toBeChecked()
await expect(resultsView.responsesTab).not.toBeChecked()
await expect(resultsView.page).toHaveURL(/\/results\?view=summary$/)
await expect(resultsView.page).toHaveURL(/\/results\/summary$/)
})

test('Explicit query route wins over remembered localStorage view', async ({
page,
resultsView,
}) => {
await page.evaluate(() => {
const match = window.location.pathname.match(
/\/apps\/forms\/([^/]+)\/results$/,
)
if (!match) {
throw new Error('Expected results route before setting localStorage')
}

localStorage.setItem(
`nextcloud_forms_${match[1]}_activeResponseView`,
'responses',
)
})

await page.goto(page.url().replace(/\/results.*$/, '/results?view=summary'))
await page.waitForURL(/\/results\?view=summary$/)

await expect(resultsView.summaryTab).toBeChecked()
await expect(resultsView.responsesTab).not.toBeChecked()
})
test('Navigating to /results redirects to /results/summary', async ({ page }) => {
// Start on the responses tab
await page.goto(page.url().replace(/\/summary$/, '/responses'))
await page.waitForURL(/\/results\/responses$/)

test('Query-less results route restores the remembered localStorage view', async ({
page,
resultsView,
}) => {
await page.evaluate(() => {
const match = window.location.pathname.match(
/\/apps\/forms\/([^/]+)\/results$/,
)
if (!match) {
throw new Error('Expected results route before setting localStorage')
}

localStorage.setItem(
`nextcloud_forms_${match[1]}_activeResponseView`,
'responses',
)
})

await page.goto(page.url().replace(/\/results.*$/, '/results'))
await page.waitForURL(/\/results\?view=responses$/)

await expect(resultsView.responsesTab).toBeChecked()
await expect(resultsView.summaryTab).not.toBeChecked()
// Navigate to the parent results route
await page.goto(page.url().replace(/\/responses$/, ''))
await page.waitForURL(/\/results\/summary$/)
await expect(page).toHaveURL(/\/results\/summary$/)
})
})

5 changes: 3 additions & 2 deletions src/Forms.vue
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,10 @@ export default {
return false
}

if (route.name === 'results') {
const resultRoutes = ['results', 'results.summary', 'results.responses']
if (resultRoutes.includes(route.name)) {
return (
form.permissions.includes(route.name) || form.submissionCount > 0
form.permissions.includes('results') || form.submissionCount > 0
)
}

Expand Down
6 changes: 5 additions & 1 deletion src/components/TopBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,11 @@ export default {

computed: {
currentView() {
return this.availableViews.filter((v) => v.id === this.$route.name)[0]
return this.availableViews.find(
(v) =>
this.$route.name === v.id
|| this.$route.name.startsWith(v.id + '.'),
)
},

availableViews() {
Expand Down
32 changes: 30 additions & 2 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,37 @@ const routes = [
},
{
path: '/:hash/results',
components: { default: Results },
name: 'results',
props: { default: true },
children: [
{
path: '',
name: 'results',
redirect: (to) => {
const validViews = ['summary', 'responses']
const storedView = localStorage.getItem(
`nextcloud_forms_${to.params.hash as string}_activeResponseView`,
)
const lastViewId =
storedView && validViews.includes(storedView)
? storedView
: 'summary'
return {
name: `results.${lastViewId}`,
params: { hash: to.params.hash },
}
},
},
{
path: 'summary',
name: 'results.summary',
components: { default: Results },
},
{
path: 'responses',
name: 'results.responses',
components: { default: Results },
},
],
},
{
path: '/:hash/submit/:submissionId?',
Expand Down
Loading
Loading