Skip to content
Merged
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ permissions:
contents: read
on:
pull_request:
branches: [master]
branches: [master, dev]

jobs:
Prettier-check:
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"dev:server": "pnpm --filter upup-react-file-uploader exec node server/index.js",
"test": "turbo run test",
"test:coverage": "pnpm --filter upup-react-file-uploader exec jest --coverage --coverageDirectory ../../coverage",
"test:e2e": "pnpm --filter upup-react-file-uploader exec playwright test",
"test:e2e": "pnpm --filter upup-react-file-uploader run test:e2e",
"test:e2e:ui": "pnpm --filter upup-react-file-uploader run test:e2e:ui",
"typecheck": "turbo run typecheck",
"playwright:install": "pnpm --filter upup-react-file-uploader run playwright:install",
"release": "pnpm --filter upup-react-file-uploader run release"
Expand Down
21 changes: 21 additions & 0 deletions packages/upup/e2e/fixtures/baseTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { test as baseTest, expect } from '@playwright/test'
import { UploaderPage } from '../pages/UploaderPage'
import { MockUploadApi, setupMockUploadApi } from './mockUploadApi'

type UpupPageObjects = {
uploaderPage: UploaderPage
mockUploadApi: MockUploadApi
}

export const test = baseTest.extend<UpupPageObjects>({
uploaderPage: async ({ page }, use) => {
const uploaderPage = new UploaderPage(page)
await use(uploaderPage)
},
mockUploadApi: async ({ page }, use) => {
const mockApi = await setupMockUploadApi(page)
await use(mockApi)
},
})

export { expect }
35 changes: 35 additions & 0 deletions packages/upup/e2e/fixtures/mockUploadApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Page } from '@playwright/test'

export interface MockUploadApi {
getUploadCount: () => number
waitForUploads: (count: number) => Promise<void>
}

export async function setupMockUploadApi(page: Page): Promise<MockUploadApi> {
let uploadCount = 0

await page.route('**/api/upload**', async route => {
uploadCount++
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
success: true,
key: `mock-upload-key-${uploadCount}`,
url: `https://mock-storage.com/file-${uploadCount}.png`,
}),
})
})

return {
getUploadCount: () => uploadCount,
waitForUploads: async (count: number) => {
const promises = Array.from({ length: count }, () =>
page.waitForResponse(
r => r.url().includes('/api/upload') && r.status() === 200,
),
)
await Promise.all(promises)
},
}
}
45 changes: 17 additions & 28 deletions packages/upup/e2e/folder-upload.spec.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
import { expect, test } from '@playwright/test'

// This test targets the Storybook story for UpupUploader
// It simulates selecting a folder by providing files with relative paths
// to the hidden input[type=file] (webkitdirectory behavior in Chromium)
import { setupMockUploadApi } from './fixtures/mockUploadApi'

test('folder upload selects and displays structured files', async ({
page,
}) => {
// Navigate to the Storybook iframe story (baseURL provided by config)
// Storybook generates ids from titles by lowercasing and stripping punctuation.
// Title: 'UpUpUploader' => id: 'upupuploader'
// Setup mock before navigation
const mockUploadApi = await setupMockUploadApi(page)

// Navigate to Storybook
await page.goto('/iframe.html?id=upupuploader--uploader-with-button')

// Wait for the UI to be ready using a visible anchor
// Wait for UI
await expect(page.getByRole('button', { name: /browse/i })).toBeVisible({
timeout: 20000,
})

// The story renders the component centered; find the hidden file input
// Find file input
const fileInput = page.getByTestId('upup-file-input')
await expect(fileInput).toHaveCount(1, { timeout: 15000 })

// Create a virtual folder with nested files
// Create virtual folder files
const files = [
{
name: 'docs/readme.txt',
Expand All @@ -32,7 +30,6 @@ test('folder upload selects and displays structured files', async ({
name: 'images/photo.png',
mimeType: 'image/png',
buffer: Buffer.from(
// Tiny 1x1 transparent PNG
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8Xw8AAn0B4y1bq3kAAAAASUVORK5CYII=',
'base64',
),
Expand All @@ -55,17 +52,16 @@ test('folder upload selects and displays structured files', async ({
})),
)

// After selection, file list should render items
// Use the file names as text content anchors
// Verify files displayed
await expect(page.getByText('readme.txt')).toBeVisible()
await expect(page.getByText('photo.png')).toBeVisible()
await expect(page.getByText('photo2.png')).toBeVisible()

// Validate count text on the Upload button
// Check upload button
const uploadBtn = page.getByRole('button', { name: /Upload 3 files/i })
await expect(uploadBtn).toBeVisible()

// Optional: ensure ordering by relative path (images/ before images/sub/ before docs/)
// Verify ordering
const allNames = await page
.locator('[data-testid="upup-file-item"], .upup-preview-scroll *')
.filter({ hasText: /readme\.txt|photo2?\.png/i })
Expand All @@ -74,18 +70,11 @@ test('folder upload selects and displays structured files', async ({
/photo\.png[\s\S]*photo2\.png[\s\S]*readme\.txt/i,
)

// Trigger upload and verify backend was called for each file
// Click upload and wait for all responses
const uploadPromise = mockUploadApi.waitForUploads(files.length)
await uploadBtn.click()
const expectedCount = 3
let seen = 0
await Promise.all(
Array.from({ length: expectedCount }).map(() =>
page.waitForResponse(r => {
const ok = r.url().includes('/api/upload') && r.status() === 200
if (ok) seen++
return ok
}),
),
)
expect(seen).toBe(expectedCount)
await uploadPromise

// Verify all files were uploaded
expect(mockUploadApi.getUploadCount()).toBe(files.length)
})
13 changes: 13 additions & 0 deletions packages/upup/e2e/locators/UploaderLocators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const UploaderLocators = {
fileInput: 'input[type="file"]',
myDeviceBtn: 'My Device',
addMoreBtn: 'Add More',
removeAllFilesBtn: 'Remove all files',
removeFileBtn: 'Remove file',
uploaderRegion: 'Add your documents here, you',

messages: {
dropInstructions:
'Add your documents here, you can upload up to 10 files max',
},
} as const
54 changes: 54 additions & 0 deletions packages/upup/e2e/pages/UploaderPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Locator, Page } from '@playwright/test'
import { UploaderLocators as locators } from '../locators/UploaderLocators'

export class UploaderPage {
readonly page: Page
readonly fileInput: Locator
readonly myDeviceBtn: Locator
readonly addMoreBtn: Locator
readonly removeAllFilesBtn: Locator
readonly uploaderRegion: Locator

constructor(page: Page) {
this.page = page
this.fileInput = page.locator(locators.fileInput)
this.myDeviceBtn = page.getByRole('button', {
name: locators.myDeviceBtn,
})
this.addMoreBtn = page.getByRole('button', {
name: locators.addMoreBtn,
})
this.removeAllFilesBtn = page.getByRole('button', {
name: locators.removeAllFilesBtn,
})
this.uploaderRegion = page.getByLabel(locators.uploaderRegion)
}

async goTo(): Promise<void> {
await this.page.goto(
'http://localhost:53050/iframe.html?id=upupuploader--uploader-with-button&viewMode=story',
)
await this.page.waitForLoadState('domcontentloaded')
await this.uploaderRegion.waitFor({ state: 'visible' })
}

async uploadFiles(files: string[]): Promise<void> {
await this.fileInput.setInputFiles(files)
}

async clickUpload(fileCount: number): Promise<void> {
const uploadBtn = this.page.getByRole('button', {
name: `Upload ${fileCount} file${fileCount > 1 ? 's' : ''}`,
})
await uploadBtn.click()
}

async addMoreFiles(files: string[]): Promise<void> {
await this.addMoreBtn.click()
await this.fileInput.setInputFiles(files)
}

async removeAllFiles(): Promise<void> {
await this.removeAllFilesBtn.click()
}
}
56 changes: 56 additions & 0 deletions packages/upup/e2e/specs/upload.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { expect, test } from '../fixtures/baseTest'
import { UploaderLocators as locators } from '../locators/UploaderLocators'

test.describe('Upup Uploader Component - Full Validation', () => {
test.beforeEach(async ({ uploaderPage }) => {
await uploaderPage.goTo()
})

test('should display initial upload instructions', async ({
uploaderPage,
}) => {
await expect(uploaderPage.uploaderRegion).toContainText(
locators.messages.dropInstructions,
)
})

test('should handle selection of 10 files', async ({ uploaderPage }) => {
const files = Array.from(
{ length: 10 },
(_, i) => `test-files/image${i + 1}.jpg`,
)

await uploaderPage.uploadFiles(files)

await expect(uploaderPage.uploaderRegion).toContainText(
'10 files selected',
)
})

test('should allow removing all files to reset state', async ({
uploaderPage,
}) => {
await uploaderPage.uploadFiles([
'test-files/image1.jpg',
'test-files/image2.jpg',
])
await uploaderPage.removeAllFiles()

await expect(uploaderPage.uploaderRegion).toContainText(
locators.messages.dropInstructions,
)
})

test('upload button should hide after clicking', async ({
uploaderPage,
page,
mockUploadApi,
}) => {
await uploaderPage.uploadFiles(['test-files/image1.jpg'])
const uploadBtn = page.getByRole('button', { name: 'Upload 1 file' })

await uploadBtn.click()

await expect(uploadBtn).not.toBeVisible({ timeout: 5000 })
})
})
4 changes: 2 additions & 2 deletions packages/upup/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@
"test:watch": "pnpm exec jest --watch",
"test:coverage": "pnpm exec jest --coverage",
"test:ci": "pnpm exec jest --coverage --ci --maxWorkers=2",
"test:e2e": "pnpm exec playwright test",
"test:e2e:ui": "pnpm exec playwright test --ui",
"test:e2e": "dotenv -e ../../.env -e ../../local-dev/.env.ports -- pnpm exec playwright test",
"test:e2e:ui": "dotenv -e ../../.env -e ../../local-dev/.env.ports -- pnpm exec playwright test --ui",
"playwright:install": "pnpm exec playwright install --with-deps",
"lint": "pnpm exec eslint src/",
"lint:fix": "pnpm exec eslint src/ --fix",
Expand Down
8 changes: 4 additions & 4 deletions packages/upup/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig, devices } from '@playwright/test'

// Use a dedicated Storybook port for E2E to avoid conflicts with local dev
const STORYBOOK_PORT = 6007
// Read Storybook port from environment (loaded from local-dev/.env.ports)
const STORYBOOK_PORT = process.env.STORYBOOK_PORT || '53050'
const storyUrl = `http://localhost:${STORYBOOK_PORT}`

export default defineConfig({
Expand All @@ -20,8 +20,8 @@ export default defineConfig({
timeout: 60_000,
// Start Storybook before running tests; reuse if already running
webServer: {
// Launch Storybook directly with the desired port in CI-friendly mode
command: `pnpm exec storybook dev -p ${STORYBOOK_PORT} --ci --no-open`,
// Launch Storybook using the package.json script
command: `pnpm run storybook`,
url: `http://localhost:${STORYBOOK_PORT}`,
timeout: 120_000,
reuseExistingServer: true,
Expand Down
8 changes: 7 additions & 1 deletion packages/upup/src/frontend/components/FileList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,13 @@ export default memo(function FileList() {
classNames.fileListFooter,
)}
>
<ShouldRender if={uploadStatus !== UploadStatus.SUCCESSFUL}>
{/* FIX: Hide upload button when status is SUCCESSFUL or FAILED */}
<ShouldRender
if={
uploadStatus !== UploadStatus.SUCCESSFUL &&
uploadStatus !== UploadStatus.FAILED
}
>
<button
className={cn(
'upup-disabled:animate-pulse upup-ml-auto upup-rounded-full upup-bg-blue-600 upup-px-4 upup-py-2 upup-text-sm upup-font-medium upup-text-white',
Expand Down
Binary file added packages/upup/test-files/image1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/upup/test-files/image10.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/upup/test-files/image2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/upup/test-files/image3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/upup/test-files/image4.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/upup/test-files/image5.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/upup/test-files/image6.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/upup/test-files/image7.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/upup/test-files/image8.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/upup/test-files/image9.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading