diff --git a/.prettierignore b/.prettierignore index c0d99ece4f..b95078792c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -49,4 +49,5 @@ src/main/webapp/fonts/* # If using jenv for JDK management /.java-version - +# Ignore e2e mock data files +/e2e/data/*.csv diff --git a/e2e/data/test_file.csv b/e2e/data/test_file.csv new file mode 100644 index 0000000000..68f742262b --- /dev/null +++ b/e2e/data/test_file.csv @@ -0,0 +1,4 @@ +first,second,third +1,one,true +2,two,false +3,three,true diff --git a/e2e/data/test_file2.csv b/e2e/data/test_file2.csv new file mode 100644 index 0000000000..02f17b0b95 --- /dev/null +++ b/e2e/data/test_file2.csv @@ -0,0 +1,4 @@ +first,second,third,four +1,one,true,a +2,two,false,b +3,three,true,c diff --git a/e2e/files.spec.ts b/e2e/files.spec.ts index 75fd3ba28e..0f07ff616b 100644 --- a/e2e/files.spec.ts +++ b/e2e/files.spec.ts @@ -1,4 +1,5 @@ import { Page, expect, test } from '@playwright/test' +import path from 'path' import { testAuth } from './fixtures/authenticatedUserPages' import { createFile, @@ -7,8 +8,14 @@ import { deleteProject, entityUrlPathname, generateEntityName, + getEntityFileHandleId, + getEntityIdFromPathname, } from './helpers/entities' -import { getAccessTokenFromCookie, getAdminPAT } from './helpers/testUser' +import { + dismissAlert, + getAccessTokenFromCookie, + getAdminPAT, +} from './helpers/testUser' import { Project } from './helpers/types' import { userConfigs } from './helpers/userConfig' import { waitForInitialPageLoad } from './helpers/utils' @@ -18,11 +25,13 @@ const expectFilePageLoaded = async ( fileEntityId: string, page: Page, ) => { - await page.waitForURL(entityUrlPathname(fileEntityId)) - await expect(page.getByText(`Discussion about ${fileName}`)).toBeVisible() + await testAuth.step('file page is loaded', async () => { + await page.waitForURL(entityUrlPathname(fileEntityId)) + await expect(page.getByText(`Discussion about ${fileName}`)).toBeVisible() - await expect(page.getByText('Loading provenance...')).not.toBeVisible() - await expect(page.getByText(fileEntityId, { exact: true })).toBeVisible() + await expect(page.getByText('Loading provenance...')).not.toBeVisible() + await expect(page.getByText(fileEntityId, { exact: true })).toBeVisible() + }) } const openFileSharingSettings = async (page: Page) => { @@ -80,15 +89,6 @@ const confirmSharingSettings = async ( ) } -const confirmAndClosePermissionsSavedAlert = async (page: Page) => { - const permissionsAlert = page.getByRole('alert').filter({ - has: page.getByText('Permissions were successfully saved to Synapse'), - }) - await expect(permissionsAlert).toBeVisible() - await permissionsAlert.getByRole('button').click() - await expect(permissionsAlert).not.toBeVisible() -} - const saveFileSharingSettings = async (page: Page) => { const saveButton = page.getByRole('button', { name: 'Save' }) await saveButton.click() @@ -99,7 +99,62 @@ const saveFileSharingSettings = async (page: Page) => { page.getByRole('heading', { name: 'File Sharing Settings' }), ).not.toBeVisible() - await confirmAndClosePermissionsSavedAlert(page) + await dismissAlert(page, 'Permissions were successfully saved to Synapse') +} + +const uploadFile = async ( + page: Page, + filePath: string, + uploadType: 'initialUpload' | 'newVersion', +) => { + await testAuth.step('open file upload modal', async () => { + const uploadButtonText = + uploadType === 'initialUpload' + ? 'Upload or Link to a File' + : 'Upload a New Version of File' + await page.getByRole('button', { name: uploadButtonText }).click() + }) + + await testAuth.step('choose file', async () => { + const fileChooserPromise = page.waitForEvent('filechooser') + await page.getByRole('button', { name: 'Browse...' }).click() + if (uploadType === 'initialUpload') { + await page + .getByRole('menu') + .getByRole('link') + .filter({ hasText: 'Files' }) + .click() + } + const fileChooser = await fileChooserPromise + await fileChooser.setFiles(path.join(__dirname, filePath)) + }) + + await testAuth.step('wait for file upload modal to close', async () => { + await expect(page.getByText('Initializing......')).not.toBeVisible() + await expect( + page.getByRole('heading', { name: 'Upload or Link to File' }), + ).not.toBeVisible() + }) + + await testAuth.step('ensure there was not an upload error', async () => { + await expect( + page.getByRole('heading', { name: 'Upload Error' }), + ).not.toBeVisible() + }) +} + +const dismissFileUploadAlert = async (page: Page) => { + await testAuth.step('dismiss file upload alert', async () => { + await dismissAlert(page, 'File successfully uploaded') + }) +} + +const getFileMD5 = async (page: Page) => { + return await testAuth.step('get file MD5', async () => { + const row = page.getByRole('row').filter({ hasText: 'MD5' }) + expect(row.getByRole('cell')).toHaveCount(2) + return row.getByRole('cell').filter({ hasNotText: 'MD5' }).textContent() + }) } let userProject: Project @@ -282,4 +337,210 @@ test.describe('Files', () => { }) }, ) + + testAuth( + 'should create and delete a file', + async ({ userPage, browserName }) => { + test.fail( + browserName === 'webkit', + `Playwright is not preserving the File's lastModified time in webkit, so + file upload fails because it seems like the file was modified during + upload. See https://github.com/microsoft/playwright/issues/27452. Fix + planned for Playwright v1.40.`, + ) + + const fileName = 'test_file.csv' + const filePath = `data/${fileName}` + const updatedFilePath = `data/test_file2.csv` + + await testAuth.step('go to files tab', async () => { + await userPage.goto(entityUrlPathname(userProject.id)) + await waitForInitialPageLoad(userPage) + await expect( + userPage.getByRole('heading', { name: userProject.name }), + ).toBeVisible() + await userPage.getByRole('link', { name: 'Files', exact: true }).click() + }) + + await testAuth.step('upload file', async () => { + await uploadFile(userPage, filePath, 'initialUpload') + await dismissFileUploadAlert(userPage) + }) + + const fileLink = await testAuth.step('get link to file', async () => { + const fileLink = userPage.getByRole('link', { name: fileName }) + await expect(fileLink).toBeVisible() + return fileLink + }) + + const { fileEntityId } = await testAuth.step( + 'get fileEntityId', + async () => { + const fileLinkHref = await fileLink.getAttribute('href') + expect(fileLinkHref).not.toBeNull() + + const fileEntityId = getEntityIdFromPathname(fileLinkHref!) + expect(fileEntityId).not.toBe('') + + return { fileEntityId } + }, + ) + + const md5v1 = await testAuth.step('view file', async () => { + await fileLink.click() + await expectFilePageLoaded(fileName, fileEntityId, userPage) + return await getFileMD5(userPage) + }) + + await testAuth.step('re-upload file', async () => { + await uploadFile(userPage, filePath, 'newVersion') + await expectFilePageLoaded(fileName, fileEntityId, userPage) + }) + + await testAuth.step( + 'confirm re-upload did not change md5 or version', + async () => { + const md5reupload = await getFileMD5(userPage) + expect(md5v1).toEqual(md5reupload) + await expect(userPage.getByText('V1 (Current)')).toBeVisible() + }, + ) + + // Upload success alert intermittently appears when re-uploading the same file + await testAuth.step( + 'dismiss file upload alert for re-uploaded file, if visible', + async () => { + if ( + await userPage.getByText('File successfully uploaded').isVisible() + ) { + await dismissFileUploadAlert(userPage) + } + }, + ) + + await testAuth.step('upload a new file', async () => { + await uploadFile(userPage, updatedFilePath, 'newVersion') + await expectFilePageLoaded(fileName, fileEntityId, userPage) + await dismissFileUploadAlert(userPage) + }) + + await testAuth.step( + 'confirm uploading new file changed md5 and version', + async () => { + const md5v2 = await getFileMD5(userPage) + expect(md5v1).not.toEqual(md5v2) + await expect(userPage.getByText('V2 (Current)')).toBeVisible() + }, + ) + + await testAuth.step( + 'add associated fileHandleIds to cleanup list', + async () => { + const fileHandleIdV1 = await getEntityFileHandleId( + userPage, + getAdminPAT(), + fileEntityId, + 1, + ) + const fileHandleIdV2 = await getEntityFileHandleId( + userPage, + getAdminPAT(), + fileEntityId, + 2, + ) + fileHandleIds.push(fileHandleIdV1) + fileHandleIds.push(fileHandleIdV2) + }, + ) + + await testAuth.step('move file to trash can', async () => { + await testAuth.step('delete file', async () => { + await userPage.getByRole('button', { name: 'File Tools' }).click() + await userPage.getByRole('menuitem', { name: 'Delete File' }).click() + }) + + await testAuth.step('confirm deletion', async () => { + await expect( + userPage.getByRole('heading', { name: 'Confirm Deletion' }), + ).toBeVisible() + await expect( + userPage.getByText( + `Are you sure you want to delete File "${fileName}"?`, + ), + ).toBeVisible() + await expect( + userPage.getByRole('button', { name: 'Cancel' }), + ).toBeVisible() + + await userPage.getByRole('button', { name: 'Delete' }).click() + }) + + await testAuth.step('confirm that file was deleted', async () => { + await userPage.waitForURL( + `${entityUrlPathname(userProject.id)}/files/`, + ) + + await expect( + userPage.getByRole('heading', { name: 'Files' }), + ).toBeVisible() + await expect( + userPage.getByRole('link', { name: fileName }), + ).not.toBeVisible() + + await dismissAlert(userPage, 'The File was successfully deleted.') + }) + }) + + await testAuth.step('remove file from trash can', async () => { + await testAuth.step('go to trash can', async () => { + await userPage.getByLabel('Trash Can').click() + + const trashCanHeading = userPage.getByRole('heading', { + name: 'Trash Can', + }) + await expect(trashCanHeading).toBeVisible() + + // click on heading, so tooltip on trash can nav button is hidden + await trashCanHeading.click() + }) + + await testAuth.step('remove file from trash can', async () => { + const fileCheckbox = userPage.getByRole('checkbox', { + name: `Select ${fileEntityId}`, + }) + await expect(fileCheckbox).not.toBeChecked() + + // Currently programmatically dispatching the click event + // because the following aren't working: + // - await fileCheckbox.click() -> fails due to