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
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
80 changes: 79 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"lint:fix": "eslint --ext .js,.ts,.vue src --fix",
"start:nextcloud": "node playwright/start-nextcloud-server.mjs",
"stylelint": "stylelint \"src/**/*.scss\" \"src/**/*.vue\"",
"stylelint:fix": "stylelint \"src/**/*.scss\" \"src/**/*.vue\" --fix"
"stylelint:fix": "stylelint \"src/**/*.scss\" \"src/**/*.vue\" --fix",
"typecheck": "vue-tsc --noEmit -p tsconfig.json"
},
"browserslist": [
"extends @nextcloud/browserslist-config"
Expand Down Expand Up @@ -58,12 +59,15 @@
"@nextcloud/stylelint-config": "^3.2.2",
"@nextcloud/vite-config": "^2.5.2",
"@playwright/test": "^1.62.1",
"@types/markdown-it": "^14.1.2",
"@types/node": "^26.1.2",
"@types/qrcode": "^1.5.6",
"@vue/tsconfig": "^0.9.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.6",
"prettier": "^3.9.6",
"vite": "^7.3.6"
"vite": "^7.3.6",
"vue-tsc": "^3.3.7"
},
"engines": {
"node": "^24.0.0",
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 @@ -51,11 +51,9 @@ test.describe('Results view', () => {
await submitView.submit()
await expect(submitView.successMessage).toBeVisible()

// 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 @@ test.describe('Results view', () => {
// 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$/)
})
})

7 changes: 4 additions & 3 deletions playwright/support/sections/SubmitSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ export class SubmitSection {
constructor(public readonly page: Page) {
this.clearFormButton = this.page.getByRole('button', { name: 'Clear form' })
this.submitButton = this.page.getByRole('button', { name: 'Submit' })
this.successMessage = this.page.getByText(
'Thank you for completing the form!',
)
this.successMessage = this.page
.getByRole('main')
.locator('.forms-emptycontent')
.getByText('Thank you for completing the form!')
}

/**
Expand Down
Loading
Loading