diff --git a/src/file-uploader/__tests__/file-uploader-item-preview.scenario.tsx b/src/file-uploader/__tests__/file-uploader-item-preview.scenario.tsx new file mode 100644 index 0000000000..f5ffbd78f7 --- /dev/null +++ b/src/file-uploader/__tests__/file-uploader-item-preview.scenario.tsx @@ -0,0 +1,20 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { type FileRow, FileUploader } from '..'; + +export function Scenario() { + const [fileRows, setFileRows] = React.useState>([]); + return ( + + ); +} diff --git a/src/file-uploader/__tests__/file-uploader-label-hint.scenario.tsx b/src/file-uploader/__tests__/file-uploader-label-hint.scenario.tsx new file mode 100644 index 0000000000..3d449cd722 --- /dev/null +++ b/src/file-uploader/__tests__/file-uploader-label-hint.scenario.tsx @@ -0,0 +1,20 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { type FileRow, FileUploader } from '..'; + +export function Scenario() { + const [fileRows, setFileRows] = React.useState>([]); + return ( + + ); +} diff --git a/src/file-uploader/__tests__/file-uploader-long-loading-multiple-files.scenario.tsx b/src/file-uploader/__tests__/file-uploader-long-loading-multiple-files.scenario.tsx new file mode 100644 index 0000000000..3c803a40a8 --- /dev/null +++ b/src/file-uploader/__tests__/file-uploader-long-loading-multiple-files.scenario.tsx @@ -0,0 +1,56 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { type FileRow, FileUploader } from '..'; + +export function Scenario() { + const [fileRows, setFileRows] = React.useState>([]); + + const simulateFileProgressUpdates = ( + fileToProcess: File, + fileToProcessId: string, + fileRows: FileRow[], + resolve: (file: File) => void + ) => { + const fileRowsCopy: FileRow[] = [...fileRows]; + let indexOfFileToUpdate = fileRowsCopy.findIndex( + (fileRow: FileRow) => fileRow.id === fileToProcessId + ); + let numberOfMockedLoadingSteps = 5 - (indexOfFileToUpdate % 3); + let mockedTotalLoadingTime = indexOfFileToUpdate % 2 === 0 ? 10000 : 8000; + for (let i = 0; i <= numberOfMockedLoadingSteps; i++) { + if (i === numberOfMockedLoadingSteps) { + // Simulates an onSuccess event + setTimeout(() => { + resolve(fileToProcess); + }, mockedTotalLoadingTime); + } else { + // Simulates an onLoading event + setTimeout(() => { + fileRowsCopy[indexOfFileToUpdate].progressAmount = (i / numberOfMockedLoadingSteps) * 100; + setFileRows([...fileRowsCopy]); + }, (i / numberOfMockedLoadingSteps) * mockedTotalLoadingTime); + } + } + }; + + return ( + { + return new Promise((resolve) => { + simulateFileProgressUpdates(fileToProcess, fileToProcessId, fileRows, resolve); + }); + }} + progressAmountStartValue={0} + setFileRows={setFileRows} + /> + ); +} diff --git a/src/file-uploader/__tests__/file-uploader-long-loading.scenario.tsx b/src/file-uploader/__tests__/file-uploader-long-loading.scenario.tsx new file mode 100644 index 0000000000..85e2e67635 --- /dev/null +++ b/src/file-uploader/__tests__/file-uploader-long-loading.scenario.tsx @@ -0,0 +1,26 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { type FileRow, FileUploader } from '..'; + +export function Scenario() { + const [fileRows, setFileRows] = React.useState>([]); + return ( + { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ errorMessage: null }); + }, 10000); + }); + }} + setFileRows={setFileRows} + /> + ); +} diff --git a/src/file-uploader/__tests__/file-uploader-overrides.scenario.tsx b/src/file-uploader/__tests__/file-uploader-overrides.scenario.tsx new file mode 100644 index 0000000000..8a40637856 --- /dev/null +++ b/src/file-uploader/__tests__/file-uploader-overrides.scenario.tsx @@ -0,0 +1,67 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { type FileRow, FileUploader } from '..'; +import { useStyletron } from '../../styles' + +export function Scenario() { + const [fileRows, setFileRows] = React.useState>([ + { + errorMessage: null, + file: new File(['test file 1'], 'file-1.txt'), + id: '0', + progressAmount: 100, + status: 'processed', + }, + { + errorMessage: 'Failed to upload', + file: new File(['test file 2'], 'file-2.txt'), + id: '1', + progressAmount: 20, + status: 'error', + }, + ]); + const [, theme] = useStyletron(); + return ( + + ); +} diff --git a/src/file-uploader/__tests__/file-uploader-upload-restrictions.scenario.tsx b/src/file-uploader/__tests__/file-uploader-upload-restrictions.scenario.tsx new file mode 100644 index 0000000000..dda0c4e4f8 --- /dev/null +++ b/src/file-uploader/__tests__/file-uploader-upload-restrictions.scenario.tsx @@ -0,0 +1,25 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { type FileRow, FileUploader } from '..'; + +export function Scenario() { + const [fileRows, setFileRows] = React.useState>([]); + return ( + + ); +} diff --git a/src/file-uploader/__tests__/file-uploader.e2e.ts b/src/file-uploader/__tests__/file-uploader.e2e.ts new file mode 100644 index 0000000000..98c7c8b28b --- /dev/null +++ b/src/file-uploader/__tests__/file-uploader.e2e.ts @@ -0,0 +1,300 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ + +import { expect, test } from '@uber/playwright-test'; +import { mount } from '../../test/integration'; + +const selectors = { + button: '[data-baseweb="button"]', + circleCheckFilledIcon: '[data-baseweb="file-uploader-circle-check-filled-icon"]', + circleExclamationPointFilledIcon: + '[data-baseweb="file-uploader-circle-exclamation-point-filled-icon"]', + deleteButtonComponent: '[data-baseweb="file-uploader-delete-button-component"]', + fileRows: '[data-baseweb="file-uploader-file-rows"]', + fileRowColumn: '[data-baseweb="file-uploader-file-row-column"]', + fileRowContent: '[data-baseweb="file-uploader-file-row-content"]', + fileRowText: '[data-baseweb="file-uploader-file-row-text"]', + fileRowFileName: '[data-baseweb="file-uploader-file-row-file-name"]', + fileRowUploadMessage: '[data-baseweb="file-uploader-file-row-upload-message"]', + fileRowUploadMessageText: '[data-baseweb="file-uploader-file-row-upload-message-text"]', + fileUploader: '[data-baseweb="file-uploader"]', + hint: '[data-baseweb="file-uploader-hint"]', + imagePreviewThumbnail: '[data-baseweb="file-uploader-image-preview-thumbnail"]', + itemPreviewContainer: '[data-baseweb="file-uploader-item-preview-container"]', + label: '[data-baseweb="file-uploader-label"]', + parentRoot: '[data-baseweb="file-uploader-parent-root"]', + paperclipFilledIcon: '[data-baseweb="file-uploader-paperclip-filled-icon"]', +}; + +test.describe('file-uploader', () => { + test('file-uploader passes basic a11y tests', async ({ page }) => { + await mount(page, 'file-uploader--file-uploader'); + }); + + test('file-uploader shows file row success on successful upload', async ({ page }) => { + await mount(page, 'file-uploader--file-uploader'); + + const fileChooserPromise = page.waitForEvent('filechooser'); + await page.click(selectors.button); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles({ + name: 'file.txt', + mimeType: 'text/plain', + buffer: Buffer.from('this is test'), + }); + + await page.waitForSelector(selectors.parentRoot); + await page.waitForSelector(selectors.fileRows); + await page.waitForSelector(selectors.fileRowColumn); + await page.waitForSelector(selectors.fileRowContent); + await page.waitForSelector(selectors.fileRowText); + await page.waitForSelector(selectors.fileRowFileName); + await page.waitForSelector(selectors.fileRowUploadMessage); + await page.waitForSelector(selectors.circleCheckFilledIcon); + const fileNameLocator = page.locator(selectors.fileRowFileName); + const uploadMessageLocator = page.locator(selectors.fileRowUploadMessageText); + await expect(fileNameLocator).toHaveText('file.txt'); + await expect(uploadMessageLocator).toHaveText('Upload successful'); + }); + + test('file-uploader shows no files after upload and clicking on the trash can icon', async ({ + page, + }) => { + await mount(page, 'file-uploader--file-uploader'); + + const fileChooserPromise = page.waitForEvent('filechooser'); + await page.click(selectors.button); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles({ + name: 'file.txt', + mimeType: 'text/plain', + buffer: Buffer.from('this is test'), + }); + + await page.waitForSelector(selectors.circleCheckFilledIcon); + await page.click(selectors.deleteButtonComponent); + const fileNameLocator = page.locator(selectors.fileRowFileName); + await expect(fileNameLocator).toHaveCount(0); + }); + + test('file-uploader shows itemPreview for non image files', async ({ page }) => { + await mount(page, 'file-uploader--item-preview'); + + const fileChooserPromise = page.waitForEvent('filechooser'); + await page.click(selectors.button); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles({ + name: 'file.txt', + mimeType: 'text/plain', + buffer: Buffer.from('this is test'), + }); + + await page.waitForSelector(selectors.itemPreviewContainer); + await page.waitForSelector(selectors.paperclipFilledIcon); + }); + + test('file-uploader shows itemPreview for image files', async ({ page }) => { + await mount(page, 'file-uploader--item-preview'); + + const fileChooserPromise = page.waitForEvent('filechooser'); + await page.click(selectors.button); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles({ + name: 'file.png', + mimeType: 'image/png', + buffer: Buffer.from('this is test'), + }); + + await page.waitForSelector(selectors.itemPreviewContainer); + await page.waitForSelector(selectors.imagePreviewThumbnail); + }); + + test('file-uploader shows label and hint', async ({ page }) => { + await mount(page, 'file-uploader--label-hint'); + await page.waitForSelector(selectors.hint); + await page.waitForSelector(selectors.label); + const hintLocator = page.locator(selectors.hint); + const labelLocator = page.locator(selectors.label); + await expect(hintLocator).toHaveText('Test hint'); + await expect(labelLocator).toHaveText('Test label'); + }); + + test('file-uploader shows file row loading on long loading upload', async ({ page }) => { + await mount(page, 'file-uploader--long-loading'); + + const fileChooserPromise = page.waitForEvent('filechooser'); + await page.click(selectors.button); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles({ + name: 'file.txt', + mimeType: 'text/plain', + buffer: Buffer.from('this is test'), + }); + + await page.waitForSelector(selectors.fileRowUploadMessage); + const fileNameLocator = page.locator(selectors.fileRowFileName); + const uploadMessageLocator = page.locator(selectors.fileRowUploadMessageText); + await expect(fileNameLocator).toHaveText('file.txt'); + await expect(uploadMessageLocator).toHaveText('Description'); + }); + + test('file-uploader shows file row loading on long loading multiple file upload', async ({ + page, + }) => { + await mount(page, 'file-uploader--long-loading-multiple-files'); + + const fileChooserPromise = page.waitForEvent('filechooser'); + await page.click(selectors.button); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles([ + { + name: 'file-1.txt', + mimeType: 'text/plain', + buffer: Buffer.from('this is test'), + }, + { + name: 'file-2.txt', + mimeType: 'text/plain', + buffer: Buffer.from('this is test'), + }, + ]); + + await page.waitForSelector(selectors.fileRowUploadMessage); + const fileNameLocator = page.locator(selectors.fileRowFileName); + const uploadMessageLocator = page.locator(selectors.fileRowUploadMessageText); + await expect(fileNameLocator).toHaveCount(2); + await expect(uploadMessageLocator).toHaveCount(2); + await expect(fileNameLocator.nth(0)).toHaveText('file-1.txt'); + await expect(fileNameLocator.nth(1)).toHaveText('file-2.txt'); + await expect(uploadMessageLocator.nth(0)).toHaveText('Description'); + await expect(uploadMessageLocator.nth(1)).toHaveText('Description'); + }); + + test('file-uploader shows file row too small error on erroneous upload', async ({ page }) => { + await mount(page, 'file-uploader--upload-restrictions'); + + const fileChooserPromise = page.waitForEvent('filechooser'); + await page.click(selectors.button); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles({ + name: 'file.txt', + mimeType: 'text/plain', + buffer: Buffer.from('this is test'), + }); + + await page.waitForSelector(selectors.circleExclamationPointFilledIcon); + const fileNameLocator = page.locator(selectors.fileRowFileName); + const uploadMessageLocator = page.locator(selectors.fileRowUploadMessageText); + await expect(fileNameLocator).toHaveText('file.txt'); + await expect(uploadMessageLocator).toHaveText( + 'Upload failed: file size must be greater than 20 KB' + ); + }); + + test('file-uploader shows file row too large error on erroneous upload', async ({ page }) => { + await mount(page, 'file-uploader--upload-restrictions'); + + let fileContent = ''; + for (let i = 0; i < 100001; i++) { + fileContent += 'a'; + } + + const fileChooserPromise = page.waitForEvent('filechooser'); + await page.click(selectors.button); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles({ + name: 'file.txt', + mimeType: 'text/plain', + buffer: Buffer.from(fileContent), + }); + + await page.waitForSelector(selectors.circleExclamationPointFilledIcon); + const fileNameLocator = page.locator(selectors.fileRowFileName); + const uploadMessageLocator = page.locator(selectors.fileRowUploadMessageText); + await expect(fileNameLocator).toHaveText('file.txt'); + await expect(uploadMessageLocator).toHaveText( + 'Upload failed: file size must be less than 100 KB' + ); + }); + + test('file-uploader shows file row not accepted error on erroneous upload', async ({ page }) => { + await mount(page, 'file-uploader--upload-restrictions'); + + let fileContent = ''; + for (let i = 0; i < 20000; i++) { + fileContent += 'a'; + } + + const fileChooserPromise = page.waitForEvent('filechooser'); + await page.click(selectors.button); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles({ + name: 'file.txt', + mimeType: 'text/plain', + buffer: Buffer.from(fileContent), + }); + + await page.waitForSelector(selectors.circleExclamationPointFilledIcon); + const fileNameLocator = page.locator(selectors.fileRowFileName); + const uploadMessageLocator = page.locator(selectors.fileRowUploadMessageText); + await expect(fileNameLocator).toHaveText('file.txt'); + await expect(uploadMessageLocator).toHaveText( + 'Upload failed: file type of text/plain is not accepted' + ); + }); + + test('file-uploader shows too many files error when too many files are uploaded', async ({ + page, + }) => { + await mount(page, 'file-uploader--upload-restrictions'); + + let fileContent = ''; + for (let i = 0; i < 20000; i++) { + fileContent += 'a'; + } + + const fileChooserPromise = page.waitForEvent('filechooser'); + await page.click(selectors.button); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles([ + { + name: 'file0.png', + mimeType: 'image/png', + buffer: Buffer.from(fileContent), + }, + { + name: 'file1.png', + mimeType: 'image/png', + buffer: Buffer.from(fileContent), + }, + { + name: 'file2.png', + mimeType: 'image/png', + buffer: Buffer.from(fileContent), + }, + { + name: 'file3.png', + mimeType: 'image/png', + buffer: Buffer.from(fileContent), + }, + ]); + + await page.waitForSelector(selectors.circleExclamationPointFilledIcon); + const file0NameLocator = page.locator(selectors.fileRowFileName).nth(0); + const file1NameLocator = page.locator(selectors.fileRowFileName).nth(1); + const file2NameLocator = page.locator(selectors.fileRowFileName).nth(2); + const file3NameLocator = page.locator(selectors.fileRowFileName).nth(3); + const uploadMessageLocator = page.locator(selectors.fileRowUploadMessageText).nth(3); + await expect(file0NameLocator).toHaveText('file0.png'); + await expect(file1NameLocator).toHaveText('file1.png'); + await expect(file2NameLocator).toHaveText('file2.png'); + await expect(file3NameLocator).toHaveText('file3.png'); + await expect(uploadMessageLocator).toHaveText( + 'Upload failed: cannot process more than 3 file(s)' + ); + }); +}); diff --git a/src/file-uploader/__tests__/file-uploader.scenario.tsx b/src/file-uploader/__tests__/file-uploader.scenario.tsx new file mode 100644 index 0000000000..71ec3eba27 --- /dev/null +++ b/src/file-uploader/__tests__/file-uploader.scenario.tsx @@ -0,0 +1,13 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { type FileRow, FileUploader } from '..'; + +export function Scenario() { + const [fileRows, setFileRows] = React.useState>([]); + return ; +} diff --git a/src/file-uploader/__tests__/file-uploader.stories.tsx b/src/file-uploader/__tests__/file-uploader.stories.tsx new file mode 100644 index 0000000000..fe8d973e3e --- /dev/null +++ b/src/file-uploader/__tests__/file-uploader.stories.tsx @@ -0,0 +1,22 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import React from 'react'; +import { Scenario as FileUploaderDefault } from './file-uploader.scenario'; +import { Scenario as FileUploaderItemPreview } from './file-uploader-item-preview.scenario'; +import { Scenario as FileUploaderLabelHint } from './file-uploader-label-hint.scenario'; +import { Scenario as FileUploaderLongLoading } from './file-uploader-long-loading.scenario'; +import { Scenario as FileUploaderLongLoadingMultipleFiles } from './file-uploader-long-loading-multiple-files.scenario'; +import { Scenario as FileUploaderOverrides } from './file-uploader-overrides.scenario'; +import { Scenario as FileUploaderUploadRestrictions } from './file-uploader-upload-restrictions.scenario'; + +export const FileUploader = () => ; +export const ItemPreview = () => ; +export const LabelHint = () => ; +export const LongLoading = () => ; +export const LongLoadingMultipleFiles = () => ; +export const Overrides = () => ; +export const UploadRestrictions = () => ;