-
Notifications
You must be signed in to change notification settings - Fork 76
Fixes: error handling on New Form when underlying file is changed. #1491
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <?xml version="1.0"?> | ||
| <h:html xmlns="http://www.w3.org/2002/xforms" | ||
| xmlns:h="http://www.w3.org/1999/xhtml" | ||
| xmlns:jr="http://openrosa.org/javarosa" | ||
| xmlns:odk="http://www.opendatakit.org/xforms" | ||
| xmlns:orx="http://openrosa.org/xforms"> | ||
| <h:head> | ||
| <h:title>Missing meta element</h:title> | ||
| <model> | ||
| <instance> | ||
| <data id="missing-meta"> | ||
| <firstname/> | ||
| </data> | ||
| </instance> | ||
| <bind nodeset="/data/firstname"/> | ||
| </model> | ||
| </h:head> | ||
| <h:body> | ||
| <input ref="/data/firstname"> | ||
| <label>First Name</label> | ||
| </input> | ||
| </h:body> | ||
| </h:html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import { test, expect } from '@playwright/test'; | ||
| import fs from 'fs'; | ||
| import os from 'os'; | ||
| import path from 'path'; | ||
| import BackendClient from '../backend-client'; | ||
|
|
||
| const appUrl = process.env.ODK_URL; | ||
| const user = process.env.ODK_USER; | ||
| const password = process.env.ODK_PASSWORD; | ||
| const projectId = process.env.PROJECT_ID; | ||
|
|
||
| let publishedForm; | ||
|
|
||
| test.beforeAll(async ({ playwright }, testInfo) => { | ||
| const backendClient = new BackendClient(playwright, `${testInfo.project.name}_form_upload`); | ||
| await backendClient.alwaysHideModal(); | ||
| publishedForm = await backendClient.createForm(); | ||
| await backendClient.dispose(); | ||
| }); | ||
|
|
||
| const login = async (page) => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this function is used in other e2e tests, what do you think about making it reusable somehow? Like creating a util file? |
||
| await page.goto(appUrl); | ||
| await expect(page.getByRole('heading', { name: 'Log in' })).toBeVisible(); | ||
|
|
||
| await page.getByPlaceholder('email address').fill(user); | ||
| await page.getByPlaceholder('password').fill(password); | ||
|
|
||
| await page.getByRole('button', { name: 'Log in' }).click(); | ||
|
|
||
| await page.waitForURL(appUrl); | ||
| }; | ||
|
|
||
| test.describe('Form Upload', () => { | ||
| test('clears file input when server returns an error', async ({ page }) => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand why the next e2e test is useful: it's hard to mock file modification in component tests. But this one I don't feel as sure about. I feel like this one could be tested in component tests easily enough (or maybe already is?). We frequently mock server errors in component tests. |
||
| await login(page); | ||
|
|
||
| // Navigate to create form page | ||
| await page.goto(`${appUrl}/#/projects/${projectId}`); | ||
| await page.getByRole('button', { name: 'New' }).click(); | ||
|
|
||
| // Verify modal is open | ||
| await expect(page.locator('#form-new')).toBeVisible(); | ||
|
|
||
| const formXml = path.join(__dirname, '../data/form-without-meta.xml'); | ||
|
|
||
| await page.locator('#form-new input[type="file"]').setInputFiles(formXml); | ||
|
|
||
| // Verify filename is displayed | ||
| await expect(page.locator('#form-new-filename')).toContainText('form-without-meta'); | ||
|
|
||
| // Click upload | ||
| await page.getByRole('button', { name: 'Upload' }).click(); | ||
|
|
||
| // Wait for error alert to appear | ||
| await expect(page.locator('#form-new .red-alert')).toContainText("The form does not contain a 'meta' group"); | ||
|
|
||
| // Verify file input is cleared (filename should not be visible) | ||
| await expect(page.locator('#form-new-filename')).not.toBeVisible(); | ||
| }); | ||
|
|
||
| test('shows error when file is modified before clicking upload anyway', async ({ page, playwright }, testInfo) => { | ||
| // Delete the form to put it in trash | ||
| const backendClient = new BackendClient(playwright, `${testInfo.project.name}_form_upload`); | ||
| await backendClient.deleteForm(publishedForm.xmlFormId); | ||
| await backendClient.dispose(); | ||
|
|
||
| // Create a temp file with the same formId as the deleted form | ||
| const formTemplate = fs.readFileSync( | ||
| path.join(__dirname, '../data/form.template.xml'), | ||
| 'utf8' | ||
| ); | ||
| const formXml = formTemplate.replaceAll('{{ formId }}', publishedForm.xmlFormId); | ||
| const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'form-upload-test-')); | ||
| const tempFilePath = path.join(tempDir, `${publishedForm.xmlFormId}.xml`); | ||
| fs.writeFileSync(tempFilePath, formXml); | ||
|
|
||
| try { | ||
| await login(page); | ||
|
|
||
| // Navigate to create form page | ||
| await page.goto(`${appUrl}/#/projects/${projectId}`); | ||
| await page.getByRole('button', { name: 'New' }).click(); | ||
|
|
||
| // Verify modal is open | ||
| await expect(page.locator('#form-new')).toBeVisible(); | ||
|
|
||
| // Upload the temp file | ||
| await page.locator('#form-new input[type="file"]').setInputFiles(tempFilePath); | ||
|
|
||
| // Verify filename is displayed | ||
| await expect(page.locator('#form-new-filename')).toContainText(publishedForm.xmlFormId); | ||
|
|
||
| // Click upload | ||
| await page.getByRole('button', { name: 'Upload' }).click(); | ||
|
|
||
| // Expect a warning that form with the same ID exists in the trash | ||
| await expect(page.locator('.modal-warnings')).toBeVisible(); | ||
| await expect(page.locator('.modal-warnings')).toContainText('Trash'); | ||
|
|
||
| // Modify the temp file | ||
| fs.writeFileSync(tempFilePath, formXml.replaceAll(publishedForm.xmlFormId, `${publishedForm.xmlFormId}_new`)); | ||
|
|
||
| // Click "Upload anyway" | ||
| await page.getByRole('button', { name: 'Upload anyway' }).click(); | ||
|
|
||
| // Expect error that file has been modified | ||
| await expect(page.locator('#form-new .red-alert')).toContainText('could not be read'); | ||
|
|
||
| // Verify file input and warnings are cleared | ||
| await expect(page.locator('#form-new-filename')).not.toBeVisible(); | ||
| await expect(page.locator('.modal-warnings')).not.toBeVisible(); | ||
| } finally { | ||
| // Clean up temp directory | ||
| // fs.rmSync(tempDir, { recursive: true, force: true }); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be commented back in? Or if it's not needed, could the whole |
||
| } | ||
| }); | ||
| }); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. getodk/central#1291 (comment) makes the point that this issue comes up in other cases as well:
Given that, I feel like this change doesn't fully close out #1291. Maybe one option is to file a follow-up issue about form attachments. This could probably come up for entity upload as well. Though it could be that those are rare cases. Certainly form upload seems like the most important case. 👍 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -200,6 +200,21 @@ export default { | |
| return; | ||
| } | ||
|
|
||
| // Assumption: If file is changed/deleted on the disk then it won't be readable | ||
| if (ignoreWarnings) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't totally understand yet why this Case 1: File modification between file selection and initial upload
Case 2: File modification after warnings receipt
Proposal
I'm guessing I don't understand some part of this, so I just wanted to share my current thinking. Does this approach not work because the file can only be read once?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah; Case 1 is intermittent failure for me (at least on Firefox). Last time when I was working on this, browser was able to read the changed file before upload button click. I like your proposal let me try that. |
||
| const reader = new FileReader(); | ||
| reader.onload = () => this.performUpload(ignoreWarnings); | ||
| reader.onerror = () => { | ||
| this.redAlert.show(this.$t('alert.fileNotReadable')); | ||
| this.file = null; | ||
| this.warnings = null; | ||
|
Comment on lines
+209
to
+210
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This pattern of clearing both |
||
| }; | ||
| reader.readAsArrayBuffer(this.file); | ||
| } else { | ||
| this.performUpload(ignoreWarnings); | ||
| } | ||
| }, | ||
| performUpload(ignoreWarnings) { | ||
| const query = ignoreWarnings ? { ignoreWarnings } : null; | ||
| const headers = { 'Content-Type': this.contentType }; | ||
| if (this.contentType !== 'application/xml') { | ||
|
|
@@ -242,7 +257,10 @@ export default { | |
| } | ||
| }) | ||
| .catch(() => { | ||
| if (this.$route === initialRoute) this.warnings = null; | ||
| if (this.$route === initialRoute) { | ||
| this.warnings = null; | ||
| this.file = null; | ||
| } | ||
| }); | ||
| }, | ||
| removeLearnMore(value) { | ||
|
|
@@ -314,7 +332,8 @@ export default { | |
| "uploadAnyway": "Upload anyway" | ||
| }, | ||
| "alert": { | ||
| "fileRequired": "Please choose a file." | ||
| "fileRequired": "Please choose a file.", | ||
| "fileNotReadable": "The file could not be read. It may have been modified or deleted. Please choose the file again." | ||
| }, | ||
| "problem": { | ||
| "400_8": "The Form definition you have uploaded does not appear to be for this Form. It has the wrong formId (expected “{expected}”, got “{actual}”).", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,7 @@ const proxyPaths = [ | |
| ]; | ||
| const devServer = { | ||
| port: 8989, | ||
| host: true, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you say more about why this change is needed?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it is needed to run e2e tests locally otherwise playwright is unable to resolve the host. but this change shouldn't be part of this PR. Community member has also faced related issue: #1274 |
||
| proxy: Object.fromEntries(proxyPaths.map(path => [path, 'http://localhost:8686'])), | ||
| // Because we proxy to nginx, which itself proxies to Backend and other | ||
| // things, the dev server doesn't need to allow CORS. CORS is already limited | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth encoding
xmlFormIdin the URL? Or is that never a problem in e2e tests?