Migrate from Cypress to Vitest Browser Mode (#5828)

- Remove `e2e` folder entirely
- Remove all hacky resolutions and yarn patches
- Remove `@types/jasmine`, `js2ts` (convert a JS file to TS), and
`vue-template-compiler` from `private/`
- Remove e2e CI job
- Add browsers tests for vue, svelte, and react headless components and
hooks.
- Add new (browser) tests for transloadit, aws-s3, and dashboard.
- Remove final useless scripts from `package.json`, use direct
references in CI.
- Fix Dropzone component accessibility discovered during testing
- Clean up github workflows (move linters.yml into ci.yml, update
e2e.yml)

**Why Vitest Browser Mode?**

We could have used playwright but vitest browser mode uses it under the
hood and we get the use the vitest we know a love. No two entirely
different setups, no different assertions to relearn, write e2e tests as
if you're writing unit tests. Easy, fast, beautiful.

https://vitest.dev/guide/browser/

**Has every single e2e test been rewritten?**

No there were quite a few tests that have a lot overlap with existing or
newly added tests. There were also some tests that were so heavily
mocked inside and out you start to wonder what the value still is. Open
to discuss which tests still need to be added.
This commit is contained in:
Merlijn Vos 2025-07-28 11:27:37 +02:00 committed by GitHub
parent 8e89788551
commit 7bf319646a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
96 changed files with 1499 additions and 5878 deletions

View file

@ -30,7 +30,7 @@ env:
jobs:
unit_tests:
name: Unit tests
name: Tests
runs-on: ubuntu-latest
strategy:
matrix:
@ -57,16 +57,20 @@ jobs:
- name: Install dependencies
run:
corepack yarn install
env:
# https://docs.cypress.io/guides/references/advanced-installation#Skipping-installation
CYPRESS_INSTALL_BINARY: 0
- name: Install Playwright Browsers
run: corepack yarn dlx playwright install --with-deps
- name: Build
run: corepack yarn run build
- name: Run tests
run: corepack yarn run test
env:
COMPANION_DATADIR: ./output
COMPANION_DOMAIN: localhost:3020
COMPANION_PROTOCOL: http
COMPANION_REDIS_URL: redis://localhost:6379
types:
name: Type tests
name: Types
runs-on: ubuntu-latest
steps:
- name: Checkout sources
@ -90,7 +94,23 @@ jobs:
- name: Install dependencies
run:
corepack yarn install
env:
# https://docs.cypress.io/guides/references/advanced-installation#Skipping-installation
CYPRESS_INSTALL_BINARY: 0
- run: corepack yarn run typecheck
lint_js:
name: Lint
runs-on: ubuntu-latest
env:
SKIP_YARN_COREPACK_CHECK: true
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run:
corepack yarn workspaces focus @uppy-dev/build
- name: Run linter
run: corepack yarn run check:ci

View file

@ -1,52 +0,0 @@
name: Companion
on:
push:
branches: [main]
paths:
- yarn.lock
- 'packages/@uppy/companion/**'
- '.github/workflows/companion.yml'
pull_request:
# We want all branches so we configure types to be the GH default again
types: [opened, synchronize, reopened]
paths:
- yarn.lock
- 'packages/@uppy/companion/**'
- '.github/workflows/companion.yml'
env:
YARN_ENABLE_GLOBAL_CACHE: false
jobs:
test:
name: Unit tests
runs-on: ubuntu-latest
strategy:
matrix:
# fix node versions so we don't get sudden unrelated CI breakage
node-version: [18.20.8, 20.19.1]
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run:
echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: ${{matrix.node-version}}
- name: Install dependencies
run: corepack yarn workspaces focus @uppy/companion
- name: Run tests
run: corepack yarn workspace @uppy/companion test
- name: Run type checks in focused workspace
run: corepack yarn workspace @uppy/companion typecheck

View file

@ -1,12 +1,5 @@
name: End-to-end tests
# SECURITY NOTE: This workflow uses pull_request_target which has access to secrets.
# This is needed because we want to be able to run e2e tests on someone else's code, but with our own credentials (which is needed for the tests).
# `pull_request_target` will always run without manual approval, even if "Require approval for all external contributors" is enabled in the repo settings.
# Therefore we implement a "safe to test" label that must be manually added once we have checked that the diff is safe.
# Example of how this could be exploited before we implemented this label: https://github.com/transloadit/uppy/pull/5798/commits/d214a893355bd72ae771fb1c52ebe87948a98440
# For PRs from forks, secrets are only provided when the "safe to test" label is present.
# This allows maintainers to safely test external contributions while preventing
# malicious actors from accessing secrets.
name: Output
on:
push:
branches: [main]
@ -18,8 +11,8 @@ on:
- 'website/**'
- '.github/**'
- '!.github/workflows/e2e.yml'
pull_request_target:
types: [opened, synchronize, reopened, labeled]
pull_request:
types: [opened, synchronize, reopened]
paths-ignore:
- '**.md'
- '**.d.ts'
@ -27,10 +20,6 @@ on:
- 'private/**'
- 'website/**'
- '.github/**'
pull_request:
types: [opened, synchronize, reopened]
paths:
- .github/workflows/e2e.yml
permissions:
pull-requests: write
@ -39,12 +28,8 @@ env:
jobs:
compare_diff:
name: Diff lib folders
runs-on: ubuntu-latest
# Apply same security condition as e2e job
if: |
github.event_name == 'push' ||
github.event.pull_request.head.repo.full_name == github.repository ||
contains(github.event.pull_request.labels.*.name, 'safe to test')
env:
DIFF_BUILDER: true
outputs:
@ -54,9 +39,7 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 2
ref:
${{ github.event.pull_request && format('refs/pull/{0}/merge',
github.event.pull_request.number) || github.sha }}
ref: ${{ github.event.pull_request && format('refs/pull/{0}/merge', github.event.pull_request.number) || github.sha }}
- run: git reset HEAD^ --hard
- name: Get yarn cache directory path
id: yarn-cache-dir-path
@ -130,86 +113,3 @@ jobs:
```
</details>
e2e:
name: Browser tests
runs-on: ubuntu-latest
# Only run E2E tests with secrets if:
# 1. PR is from the same repository (trusted), OR
# 2. PR has the "safe to test" label (maintainer approved)
if: |
github.event_name == 'push' ||
github.event.pull_request.head.repo.full_name == github.repository ||
contains(github.event.pull_request.labels.*.name, 'safe to test')
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run:
echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Create cache folder for Cypress
id: cypress-cache-dir-path
run: echo "dir=$(mktemp -d)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.cypress-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-cypress
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Start Redis
uses: supercharge/redis-github-action@ea9b21c6ecece47bd99595c532e481390ea0f044 # 1.8.0
with:
redis-version: 7
- name: Install dependencies
run: corepack yarn install --immutable
env:
# https://docs.cypress.io/guides/references/advanced-installation#Binary-cache
CYPRESS_CACHE_FOLDER: ${{ steps.cypress-cache-dir-path.outputs.dir }}
- name: Build Uppy packages
run: corepack yarn build
- name: Run end-to-end browser tests
run: corepack yarn run e2e:ci
env:
COMPANION_DATADIR: ./output
COMPANION_DOMAIN: localhost:3020
COMPANION_PROTOCOL: http
COMPANION_REDIS_URL: redis://localhost:6379
# Secrets are safe to provide here because job-level condition ensures
# this only runs for trusted PRs or PRs with "safe to test" label
COMPANION_UNSPLASH_KEY: ${{ secrets.COMPANION_UNSPLASH_KEY }}
COMPANION_UNSPLASH_SECRET: ${{ secrets.COMPANION_UNSPLASH_SECRET }}
COMPANION_AWS_KEY: ${{ secrets.COMPANION_AWS_KEY }}
COMPANION_AWS_SECRET: ${{ secrets.COMPANION_AWS_SECRET }}
COMPANION_AWS_BUCKET: ${{ secrets.COMPANION_AWS_BUCKET }}
COMPANION_AWS_REGION: ${{ secrets.COMPANION_AWS_REGION }}
VITE_COMPANION_URL: http://localhost:3020
VITE_TRANSLOADIT_KEY: ${{ secrets.TRANSLOADIT_KEY }}
VITE_TRANSLOADIT_SECRET: ${{ secrets.TRANSLOADIT_SECRET }}
VITE_TRANSLOADIT_TEMPLATE: ${{ secrets.TRANSLOADIT_TEMPLATE }}
VITE_TRANSLOADIT_SERVICE_URL: ${{ secrets.TRANSLOADIT_SERVICE_URL }}
# https://docs.cypress.io/guides/references/advanced-installation#Binary-cache
CYPRESS_CACHE_FOLDER: ${{ steps.cypress-cache-dir-path.outputs.dir }}
- name: Upload videos in case of failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: videos-and-screenshots
path: |
e2e/cypress/videos/
e2e/cypress/screenshots/

View file

@ -1,39 +0,0 @@
name: Linters
on:
push:
branches: [main]
paths-ignore:
- '.github/**'
- '!.github/workflows/linters.yml'
- '!.github/CONTRIBUTING.md'
pull_request:
# We want all branches so we configure types to be the GH default again
types: [opened, synchronize, reopened]
paths-ignore:
- '.github/**'
- '!.github/workflows/linters.yml'
- '!.github/CONTRIBUTING.md'
env:
YARN_ENABLE_GLOBAL_CACHE: false
SKIP_YARN_COREPACK_CHECK: true
jobs:
lint_js:
name: Lint JavaScript/TypeScript
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: yarn
- name: Install dependencies
run:
corepack yarn workspaces focus @uppy-dev/build
- name: Run linter
run: corepack yarn run check:ci

View file

@ -59,7 +59,7 @@ jobs:
run: corepack yarn run build
- name: Upload "${{ inputs.package }}" to CDN
if: ${{ !inputs.force }}
run: corepack yarn run uploadcdn "$PACKAGE" "$VERSION"
run: corepack yarn workspace uppy node upload-to-cdn.js "$PACKAGE" "$VERSION"
env:
PACKAGE: ${{inputs.package}}
VERSION: ${{inputs.version}}
@ -67,7 +67,7 @@ jobs:
EDGLY_SECRET: ${{secrets.EDGLY_SECRET}}
- name: Upload "${{ inputs.package }}" to CDN
if: ${{ inputs.force }}
run: corepack yarn run uploadcdn "$PACKAGE" "$VERSION" -- --force
run: corepack yarn workspace uppy node upload-to-cdn.js "$PACKAGE" "$VERSION" --force
env:
PACKAGE: ${{inputs.package}}
VERSION: ${{inputs.version}}

1
.gitignore vendored
View file

@ -18,6 +18,7 @@ tsconfig.tsbuildinfo
tsconfig.build.tsbuildinfo
.svelte-kit
.turbo
__screenshots__
dist/
lib/

View file

@ -1,9 +0,0 @@
{
"extends": "@parcel/config-default",
"transformers": {
"*.{js,mjs,jsx,cjs,ts,tsx}": [
"@parcel/transformer-js",
"@parcel/transformer-react-refresh-wrap"
]
}
}

View file

@ -1,17 +0,0 @@
import AwsS3Multipart from '@uppy/aws-s3'
import { Uppy } from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
const uppy = new Uppy()
.use(Dashboard, { target: '#app', inline: true })
.use(AwsS3Multipart, {
limit: 2,
endpoint: process.env.VITE_COMPANION_URL,
shouldUseMultipart: true,
})
// Keep this here to access uppy in tests
window.uppy = uppy

View file

@ -1,11 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>dashboard-aws-multipart</title>
<script defer type="module" src="app.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

View file

@ -1,17 +0,0 @@
import AwsS3 from '@uppy/aws-s3'
import { Uppy } from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
const uppy = new Uppy()
.use(Dashboard, { target: '#app', inline: true })
.use(AwsS3, {
limit: 2,
endpoint: process.env.VITE_COMPANION_URL,
shouldUseMultipart: false,
})
// Keep this here to access uppy in tests
window.uppy = uppy

View file

@ -1,11 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>dashboard-aws</title>
<script defer type="module" src="app.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

View file

@ -1,18 +0,0 @@
import Compressor from '@uppy/compressor'
import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
const uppy = new Uppy()
.use(Dashboard, {
target: document.body,
inline: true,
})
.use(Compressor, {
mimeType: 'image/webp',
})
// Keep this here to access uppy in tests
window.uppy = uppy

View file

@ -1,9 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>dashboard-compressor</title>
<script defer type="module" src="app.js"></script>
</head>
<body></body>
</html>

View file

@ -1,25 +0,0 @@
import { Uppy } from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import Transloadit from '@uppy/transloadit'
import generateSignatureIfSecret from './generateSignatureIfSecret.js'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
// Environment variables:
// https://en.parceljs.org/env.html
const uppy = new Uppy()
.use(Dashboard, { target: '#app', inline: true })
.use(Transloadit, {
service: process.env.VITE_TRANSLOADIT_SERVICE_URL,
waitForEncoding: true,
assemblyOptions: () =>
generateSignatureIfSecret(process.env.VITE_TRANSLOADIT_SECRET, {
auth: { key: process.env.VITE_TRANSLOADIT_KEY },
template_id: process.env.VITE_TRANSLOADIT_TEMPLATE,
}),
})
// Keep this here to access uppy in tests
window.uppy = uppy

View file

@ -1,42 +0,0 @@
const enc = new TextEncoder('utf-8')
async function sign(secret, body) {
const algorithm = { name: 'HMAC', hash: 'SHA-384' }
const key = await crypto.subtle.importKey(
'raw',
enc.encode(secret),
algorithm,
false,
['sign', 'verify'],
)
const signature = await crypto.subtle.sign(
algorithm.name,
key,
enc.encode(body),
)
return `sha384:${Array.from(new Uint8Array(signature), (x) => x.toString(16).padStart(2, '0')).join('')}`
}
function getExpiration(future) {
return new Date(Date.now() + future)
.toISOString()
.replace('T', ' ')
.replace(/\.\d+Z$/, '+00:00')
}
/**
* Adds an expiration date and signs the params object if a secret is passed to
* it. If no secret is given, it returns the same object.
*
* @param {string | undefined} secret
* @param {object} params
* @returns {{ params: string, signature?: string }}
*/
export default async function generateSignatureIfSecret(secret, params) {
let signature
if (secret) {
params.auth.expires = getExpiration(5 * 60 * 1000)
params = JSON.stringify(params)
signature = await sign(secret, params)
}
return { params, signature }
}

View file

@ -1,11 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>dashboard-transloadit</title>
<script defer type="module" src="app.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

View file

@ -1,25 +0,0 @@
import { Uppy } from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import Tus from '@uppy/tus'
import Unsplash from '@uppy/unsplash'
import Url from '@uppy/url'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
function onShouldRetry(err, retryAttempt, options, next) {
if (err?.originalResponse?.getStatus() === 418) {
return true
}
return next(err)
}
const companionUrl = 'http://localhost:3020'
const uppy = new Uppy()
.use(Dashboard, { target: '#app', inline: true })
.use(Tus, { endpoint: 'https://tusd.tusdemo.net/files', onShouldRetry })
.use(Url, { target: Dashboard, companionUrl })
.use(Unsplash, { target: Dashboard, companionUrl })
// Keep this here to access uppy in tests
window.uppy = uppy

View file

@ -1,11 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>dashboard-tus</title>
<script defer type="module" src="app.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

View file

@ -1,36 +0,0 @@
import Audio from '@uppy/audio'
import Compressor from '@uppy/compressor'
import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import DropTarget from '@uppy/drop-target'
import GoldenRetriever from '@uppy/golden-retriever'
import ImageEditor from '@uppy/image-editor'
import RemoteSources from '@uppy/remote-sources'
import ScreenCapture from '@uppy/screen-capture'
import Webcam from '@uppy/webcam'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
const COMPANION_URL = 'http://companion.uppy.io'
const uppy = new Uppy()
.use(Dashboard, { target: '#app', inline: true })
.use(RemoteSources, { companionUrl: COMPANION_URL })
.use(Webcam, {
target: Dashboard,
showVideoSourceDropdown: true,
showRecordingLength: true,
})
.use(Audio, {
target: Dashboard,
showRecordingLength: true,
})
.use(ScreenCapture, { target: Dashboard })
.use(ImageEditor, { target: Dashboard })
.use(DropTarget, { target: document.body })
.use(Compressor)
.use(GoldenRetriever, { serviceWorker: true })
// Keep this here to access uppy in tests
window.uppy = uppy

View file

@ -1,11 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>dashboard-ui</title>
<script defer type="module" src="app.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

View file

@ -1,53 +0,0 @@
<template>
<UppyContextProvider :uppy="uppy">
<main>
<h1>Vue Headless Components Test</h1>
<article id="files-list">
<h2>With list</h2>
<Dropzone />
<FilesList />
<UploadButton />
</article>
<article id="files-grid">
<h2>With grid</h2>
<Dropzone />
<FilesGrid :columns="2" />
<UploadButton />
</article>
</main>
</UppyContextProvider>
</template>
<script>
import Uppy from '@uppy/core'
import Tus from '@uppy/tus'
import {
Dropzone,
FilesGrid,
FilesList,
UploadButton,
UppyContextProvider,
} from '@uppy/vue'
import Vue from 'vue'
export default {
name: 'App',
components: {
UppyContextProvider,
Dropzone,
FilesList,
FilesGrid,
UploadButton,
},
computed: {
uppy: () =>
new Uppy().use(Tus, {
endpoint: 'https://tusd.tusdemo.net/files/',
}),
},
}
</script>
<style src="@uppy/vue/dist/styles.css"></style>

View file

@ -1,11 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>dashboard-vue</title>
<script defer type="module" src="index.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

View file

@ -1,4 +0,0 @@
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

View file

@ -1,21 +0,0 @@
import { Uppy } from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import Unsplash from '@uppy/unsplash'
import Url from '@uppy/url'
import XHRUpload from '@uppy/xhr-upload'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
const companionUrl = 'http://localhost:3020'
const uppy = new Uppy()
.use(Dashboard, { target: '#app', inline: true })
.use(XHRUpload, {
endpoint: 'https://xhr-server.herokuapp.com/upload',
limit: 6,
})
.use(Url, { target: Dashboard, companionUrl })
.use(Unsplash, { target: Dashboard, companionUrl })
// Keep this here to access uppy in tests
window.uppy = uppy

View file

@ -1,11 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>dashboard-xhr</title>
<script defer type="module" src="app.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

View file

@ -1,31 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>End-to-End test suite</title>
</head>
<body>
<h1>Test apps</h1>
<nav>
<ul>
<li><a href="dashboard-aws/index.html">dashboard-aws</a></li>
<li>
<a href="dashboard-aws-multipart/index.html"
>dashboard-aws-multipart</a
>
</li>
<li>
<a href="dashboard-compressor/index.html">dashboard-compressor</a>
</li>
<li><a href="react/index.html">react</a></li>
<li>
<a href="dashboard-transloadit/index.html">dashboard-transloadit</a>
</li>
<li><a href="dashboard-tus/index.html">dashboard-tus</a></li>
<li><a href="dashboard-xhr/index.html">dashboard-xhr</a></li>
<li><a href="dashboard-ui/index.html">dashboard-ui</a></li>
<li><a href="dashboard-vue/index.html">dashboard-vue</a></li>
</ul>
</nav>
</body>
</html>

View file

@ -1,47 +0,0 @@
/** biome-ignore-all lint/nursery/useUniqueElementIds: it's fine */
import Uppy from '@uppy/core'
import { Dashboard, DashboardModal, DragDrop } from '@uppy/react'
import RemoteSources from '@uppy/remote-sources'
import ThumbnailGenerator from '@uppy/thumbnail-generator'
import React, { useState } from 'react'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
import '@uppy/drag-drop/dist/style.css'
const uppyDashboard = new Uppy({ id: 'dashboard' }).use(RemoteSources, {
companionUrl: 'http://companion.uppy.io',
sources: ['GoogleDrive', 'OneDrive', 'Unsplash', 'Zoom', 'Url'],
})
const uppyModal = new Uppy({ id: 'modal' })
const uppyDragDrop = new Uppy({ id: 'drag-drop' }).use(ThumbnailGenerator)
export default function App() {
const [open, setOpen] = useState(false)
// TODO: Parcel is having a bad time resolving React inside @uppy/react for some reason.
// We are using Parcel in an odd way and I don't think there is an easy fix.
// const files = useUppyState(uppyDashboard, (state) => state.files)
// drag-drop has no visual output so we test it via the uppy instance
window.uppy = uppyDragDrop
return (
<div
style={{
maxWidth: '30em',
margin: '5em 0',
display: 'grid',
gridGap: '2em',
}}
>
<button type="button" id="open" onClick={() => setOpen(!open)}>
Open Modal
</button>
{/* <p>Dashboard file count: {Object.keys(files).length}</p> */}
<Dashboard id="dashboard" uppy={uppyDashboard} />
<DashboardModal id="modal" open={open} uppy={uppyModal} />
<DragDrop id="drag-drop" uppy={uppyDragDrop} />
</div>
)
}

View file

@ -1,11 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>dashboard-react</title>
<script defer type="module" src="index.jsx"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

View file

@ -1,7 +0,0 @@
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
const container = document.getElementById('app')
const root = createRoot(container)
root.render(<App />)

View file

@ -1,20 +0,0 @@
import { defineConfig } from 'cypress'
import installLogsPrinter from 'cypress-terminal-report/src/installLogsPrinter.js'
import startMockServer from './mock-server.mjs'
export default defineConfig({
defaultCommandTimeout: 16_000,
requestTimeout: 16_000,
e2e: {
baseUrl: 'http://localhost:1234',
specPattern: 'cypress/integration/*.spec.ts',
setupNodeEvents(on) {
// implement node event listeners here
installLogsPrinter(on)
startMockServer('localhost', 4678)
},
},
})

View file

@ -1,983 +0,0 @@
{
"plugins": {
"Dashboard": {
"isHidden": false,
"fileCardFor": null,
"activeOverlayType": null,
"showAddFilesPanel": false,
"activePickerPanel": false,
"metaFields": [
{
"id": "license",
"name": "License",
"placeholder": "specify license"
},
{
"id": "caption",
"name": "Caption",
"placeholder": "add caption"
}
],
"targets": [
{
"id": "Dashboard:StatusBar",
"name": "StatusBar",
"type": "progressindicator"
},
{
"id": "Dashboard:Informer",
"name": "Informer",
"type": "progressindicator"
},
{
"id": "GoogleDrive",
"name": "Google Drive",
"type": "acquirer"
},
{
"id": "Instagram",
"name": "Instagram",
"type": "acquirer"
},
{
"id": "Dropbox",
"name": "Dropbox",
"type": "acquirer"
},
{
"id": "Url",
"name": "Link",
"type": "acquirer"
},
{
"id": "Webcam",
"name": "Camera",
"type": "acquirer"
}
],
"areInsidesReadyToBeVisible": true,
"containerWidth": 750,
"containerHeight": 490
},
"GoogleDrive": {
"currentSelection": [],
"authenticated": false,
"files": [],
"folders": [],
"directories": [],
"activeRow": -1,
"filterInput": "",
"isSearchVisible": false,
"hasTeamDrives": false,
"teamDrives": [],
"teamDriveId": ""
},
"Instagram": {
"currentSelection": [],
"authenticated": true,
"files": [
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/420d2bdc2cb30251d7ecf8e516f7fa7d/5D55A291/t51.2885-15/e35/s320x320/50692753_2474466385958388_3994336603663016042_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Feb 11, 2019, 3:34 PM.jpeg",
"mimeType": "image/jpeg",
"id": "1976930126949922734_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/ff2400b726bbd3415c4a07b723615578/5D3C08E9/t51.2885-15/e35/s150x150/50692753_2474466385958388_3994336603663016042_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1976930126949922734_104680",
"modifiedDate": "1549888457"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/78aa49ad8ccbe12b07fb20f07c1b9a1d/5D520E84/t51.2885-15/e35/s320x320/49858772_2238267119827712_38852393303322952_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/b3eda9daaa214951c1fb1a0f7000d8de/5D3F3F89/t51.2885-15/e35/s150x150/49858772_2238267119827712_38852393303322952_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=0",
"modifiedDate": "1547843980"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/ec36a8b8b7db546abeb3cdcf12b52ba5/5D77DC5F/t51.2885-15/e35/s320x320/49858316_1974473399524283_2231924729468134373_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM 1.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_1046801",
"thumbnail": "https://scontent.cdninstagram.com/vp/d8ef3dd687b6aab396cae9284226c7df/5D711727/t51.2885-15/e35/s150x150/49858316_1974473399524283_2231924729468134373_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=1",
"modifiedDate": "1547843980"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/3055f2ae78d775fc031654d20e791ebc/5D51235E/t51.2885-15/e35/s320x320/49933915_184580175831450_4288362971970794931_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM 2.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_1046802",
"thumbnail": "https://scontent.cdninstagram.com/vp/3d1b5aa16b8d64bc7c14ae1e3b13dfbe/5D5DD553/t51.2885-15/e35/c0.0.1079.1079a/s150x150/49933915_184580175831450_4288362971970794931_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=2",
"modifiedDate": "1547843980"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/3da7cd1f79dcecde1e871ea09cf65a87/5D737FDF/t51.2885-15/e35/s320x320/50515172_1138232866338235_1751853475314282763_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM 3.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_1046803",
"thumbnail": "https://scontent.cdninstagram.com/vp/5c51a2dac516e575a66cf10c39615faf/5D6922A7/t51.2885-15/e35/s150x150/50515172_1138232866338235_1751853475314282763_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=3",
"modifiedDate": "1547843980"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/9b869a8590154eb01bdb8c4d834dbc96/5D68F919/t51.2885-15/e35/s320x320/49536508_2715697688455532_3941725382632324585_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM 4.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_1046804",
"thumbnail": "https://scontent.cdninstagram.com/vp/9d5ef7d80443c6e606dd2032bd9bbef3/5D736861/t51.2885-15/e35/s150x150/49536508_2715697688455532_3941725382632324585_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=4",
"modifiedDate": "1547843980"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/e93879efc4dad02ed95f28c845b30c1b/5D6EE4D2/t51.2885-15/e35/s320x320/50024282_2330888297144674_8558997522361102927_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM 5.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_1046805",
"thumbnail": "https://scontent.cdninstagram.com/vp/c173758a1dbcdc033973a4d9b77061fa/5D3FD13A/t51.2885-15/e35/c0.0.1079.1079a/s150x150/50024282_2330888297144674_8558997522361102927_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=5",
"modifiedDate": "1547843980"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/385551f9d659254e79e52e2069962abe/5D3DB99B/t51.2885-15/e35/s320x320/49541690_1326265230849388_780299101204315442_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM 6.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_1046806",
"thumbnail": "https://scontent.cdninstagram.com/vp/39449810cc5a750d7da2ff0311986e5f/5D6F4796/t51.2885-15/e35/c0.0.1079.1079a/s150x150/49541690_1326265230849388_780299101204315442_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=6",
"modifiedDate": "1547843980"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/6dbe8e2193c6f35cc9a4b3681331281d/5D6B8838/t51.2885-15/e35/s320x320/47693310_2066019920179632_9021194778493613415_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM 7.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_1046807",
"thumbnail": "https://scontent.cdninstagram.com/vp/d82f97e1cd037d3b5d8a9d3be1620725/5D54D1D0/t51.2885-15/e35/c0.0.1079.1079a/s150x150/47693310_2066019920179632_9021194778493613415_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=7",
"modifiedDate": "1547843980"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/ab34b879e64b6b74d8cedfc7193dbc7f/5D5846E9/t51.2885-15/e35/s320x320/49306549_310052152966468_747321250340934181_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM 8.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_1046808",
"thumbnail": "https://scontent.cdninstagram.com/vp/4899a15cbcf805e4fbb18197322b4187/5D5118EF/t51.2885-15/e35/c0.0.1079.1079a/s150x150/49306549_310052152966468_747321250340934181_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=8",
"modifiedDate": "1547843980"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/9898c850ebf16fb48c7c67f40fa768aa/5D5D74F8/t51.2885-15/e35/s320x320/49766698_1079661332208739_5393337093402545472_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM 9.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_1046809",
"thumbnail": "https://scontent.cdninstagram.com/vp/24ff0de8fd62247a5b4d46657e078504/5D56FE10/t51.2885-15/e35/c0.0.1079.1079a/s150x150/49766698_1079661332208739_5393337093402545472_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=9",
"modifiedDate": "1547843980"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/945b83923551c2419d02fb394ca93ecf/5D6FFDF2/t51.2885-15/e35/s320x320/39220053_1153571588114505_375944023631724544_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Aug 23, 2018, 2:58 PM.jpeg",
"mimeType": "image/jpeg",
"id": "1852250486931812607_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/c0b40cc16747180b59ae966afb3c020d/5D3CFA02/t51.2885-15/e35/s150x150/39220053_1153571588114505_375944023631724544_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1852250486931812607_104680?carousel_id=0",
"modifiedDate": "1535025486"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/61b460c2217002b2ca277d8e2844717c/5D5EFFFD/t51.2885-15/e35/s320x320/39346998_282273142585083_1015643341825507328_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Aug 23, 2018, 2:58 PM 1.jpeg",
"mimeType": "image/jpeg",
"id": "1852250486931812607_1046801",
"thumbnail": "https://scontent.cdninstagram.com/vp/4b39d19ad2583c95acbb11e762579e35/5D58090D/t51.2885-15/e35/s150x150/39346998_282273142585083_1015643341825507328_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1852250486931812607_104680?carousel_id=1",
"modifiedDate": "1535025486"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/e7a7a3f0701ce18a1b036c24cb7cea9b/5D5AFDE4/t51.2885-15/e35/s320x320/38699807_314497562641072_2259807118783676416_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Aug 23, 2018, 2:58 PM 2.jpeg",
"mimeType": "image/jpeg",
"id": "1852250486931812607_1046802",
"thumbnail": "https://scontent.cdninstagram.com/vp/853fe9cc8931bd25d910f1cb27e34c91/5D3C8E14/t51.2885-15/e35/s150x150/38699807_314497562641072_2259807118783676416_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1852250486931812607_104680?carousel_id=2",
"modifiedDate": "1535025486"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/3fb0681ab45a93f538a6388d77d1e0a6/5D6C7686/t51.2885-15/e35/s320x320/39628514_334313577140078_7876516111540289536_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Aug 23, 2018, 2:58 PM 3.jpeg",
"mimeType": "image/jpeg",
"id": "1852250486931812607_1046803",
"thumbnail": "https://scontent.cdninstagram.com/vp/d047037085ec8f9bcf5737275a54741b/5D70D676/t51.2885-15/e35/s150x150/39628514_334313577140078_7876516111540289536_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1852250486931812607_104680?carousel_id=3",
"modifiedDate": "1535025486"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/be4a02058c30e658896f3a1cc10938fd/5D59783D/t51.2885-15/e35/s320x320/39507580_504831159978201_5212373753534611456_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Aug 23, 2018, 1:20 PM.jpeg",
"mimeType": "image/jpeg",
"id": "1852201222927172582_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/4a05e2f9b3464fa502d8102e98e81c13/5D5A45CD/t51.2885-15/e35/s150x150/39507580_504831159978201_5212373753534611456_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1852201222927172582_104680",
"modifiedDate": "1535019613"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/1ed3c5ce49461c85d30e74eb371c5142/5D3D332D/t51.2885-15/e35/s320x320/38235951_280335879462106_4098398707425214464_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Aug 14, 2018, 1:40 AM.jpeg",
"mimeType": "image/jpeg",
"id": "1845325816068388330_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/fb2dacd2d08807cacc48a76acbb8cb32/5D7001DD/t51.2885-15/e35/s150x150/38235951_280335879462106_4098398707425214464_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1845325816068388330_104680",
"modifiedDate": "1534200001"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/2aaab426ea979d4fec469c53aa748a1a/5D5B581D/t51.2885-15/e35/s320x320/38097421_294352187994556_231473126364413952_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Aug 14, 2018, 1:37 AM.jpeg",
"mimeType": "image/jpeg",
"id": "1845324587128869025_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/568448b82d5883bf6c5b2b554dcb3c71/5D5BFCA9/t51.2885-15/e35/c135.0.809.809a/s150x150/38097421_294352187994556_231473126364413952_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1845324587128869025_104680",
"modifiedDate": "1534199854"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/73a478dae4d7ed45ba86bb51c6f1279c/5D3BC45B/t51.2885-15/e35/s320x320/38004914_1332543240181648_8551268285629333504_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Aug 2, 2018, 8:27 PM.jpeg",
"mimeType": "image/jpeg",
"id": "1837195735932389212_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/55047e7ff9ab12727fc38865852cfea4/5D3A6523/t51.2885-15/e35/s150x150/38004914_1332543240181648_8551268285629333504_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1837195735932389212_104680",
"modifiedDate": "1533230820"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/4436a67f44ab681184d648b8cef2bd0d/5D5C35EB/t51.2885-15/e35/s320x320/36979476_227621117961595_1230588248623939584_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jul 12, 2018, 6:58 PM.jpeg",
"mimeType": "image/jpeg",
"id": "1821930696162038399_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/b5626d3918d93266ee92b414853b7eaa/5D515F04/t51.2885-15/e35/c123.0.735.735a/s150x150/36979476_227621117961595_1230588248623939584_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1821930696162038399_104680",
"modifiedDate": "1531411085"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/f26b47afa38a487002fb9cd823c911a9/5D53FC32/t51.2885-15/e35/s320x320/36037683_1012778022214785_7430470254473510912_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jun 24, 2018, 7:33 PM.jpeg",
"mimeType": "image/jpeg",
"id": "1808902468027558819_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/b93e11cda790a99ffd3bcdbfd50199e4/5D4044D4/t51.2885-15/e35/c101.0.606.606a/s150x150/36037683_1012778022214785_7430470254473510912_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1808902468027558819_104680?carousel_id=0",
"modifiedDate": "1529857999"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/1be4dae4e20a9b63e887ea5879590a9b/5D526FF1/t51.2885-15/e35/s320x320/35575252_166097084248741_3980291461882052608_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jun 24, 2018, 7:33 PM 1.jpeg",
"mimeType": "image/jpeg",
"id": "1808902468027558819_1046801",
"thumbnail": "https://scontent.cdninstagram.com/vp/6c1e545968b58fa4bd9279284f5e4eb2/5D3A341F/t51.2885-15/e35/c135.0.809.809a/s150x150/35575252_166097084248741_3980291461882052608_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1808902468027558819_104680?carousel_id=1",
"modifiedDate": "1529857999"
},
{
"isFolder": false,
"icon": "video",
"name": "Instagram Jun 24, 2018, 7:33 PM 2.mp4",
"mimeType": "video/mp4",
"id": "1808902468027558819_1046802",
"thumbnail": null,
"requestPath": "1808902468027558819_104680?carousel_id=2",
"modifiedDate": "1529857999"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/862ced4e0f047ba14ff29c6cc0318c44/5D57DF38/t51.2885-15/e35/s320x320/35278170_837444053128139_5243357991205339136_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jun 20, 2018, 2:42 AM.jpeg",
"mimeType": "image/jpeg",
"id": "1805494813368147007_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/4829dc1acad2af234ff4628d7d4750c2/5D3D81C8/t51.2885-15/e35/s150x150/35278170_837444053128139_5243357991205339136_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1805494813368147007_104680",
"modifiedDate": "1529451775"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/7c4ceb66525b802444dccd5a86924fc2/5D74C314/t51.2885-15/e35/s320x320/28764233_1638269642893040_4480301736187133952_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Mar 18, 2018, 9:32 PM.jpeg",
"mimeType": "image/jpeg",
"id": "1737934383083311905_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/5f57985b3b0a727ba6cb8b5107f8a992/5D50EF6C/t51.2885-15/e35/s150x150/28764233_1638269642893040_4480301736187133952_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1737934383083311905_104680",
"modifiedDate": "1521397944"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/899393f947e53ac5b36a995d67d111df/5D5BD35A/t51.2885-15/e35/s320x320/26067758_541222112905573_5423593755456307200_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 11, 2018, 5:18 AM.jpeg",
"mimeType": "image/jpeg",
"id": "1689609196894186490_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/fd06fd5bb588684209877d1414315f16/5D5973AA/t51.2885-15/e35/s150x150/26067758_541222112905573_5423593755456307200_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1689609196894186490_104680",
"modifiedDate": "1515637133"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/4cde437dd22560fab9d5eb3de0b8b2e0/5D7201E3/t51.2885-15/e35/s320x320/25036240_2079885088923563_8690166922391584768_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Dec 22, 2017, 8:05 PM.jpeg",
"mimeType": "image/jpeg",
"id": "1675559745477333499_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/db7f2fbf1db02963e8bea721101cc287/5D6E489B/t51.2885-15/e35/s150x150/25036240_2079885088923563_8690166922391584768_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1675559745477333499_104680",
"modifiedDate": "1513962308"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/af35659718f36655e8bf703f25bb233c/5D5F644F/t51.2885-15/e35/s320x320/24331847_1536227273080845_4856051751851130880_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Dec 4, 2017, 7:44 AM.jpeg",
"mimeType": "image/jpeg",
"id": "1662141091721036563_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/190fd6922e69ad087597ba0697938cf5/5D5B97BC/t51.2885-15/e35/c102.0.875.875/s150x150/24331847_1536227273080845_4856051751851130880_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1662141091721036563_104680",
"modifiedDate": "1512362680"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/b83e927ac2d13ad1ac9d2cafdc01f959/5D405387/t51.2885-15/e35/s320x320/23417132_213197029221132_8238897611998756864_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Nov 12, 2017, 7:02 PM.jpeg",
"mimeType": "image/jpeg",
"id": "1646537186320644618_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/5b346e973c29acbbcaea0f61f1809f4e/5D560A77/t51.2885-15/e35/s150x150/23417132_213197029221132_8238897611998756864_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1646537186320644618_104680",
"modifiedDate": "1510502549"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/e83ae26b09c5187a1a53083f8bdeb364/5D76BF66/t51.2885-15/e35/s320x320/23347341_1932821443410985_573390529291616256_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Nov 7, 2017, 4:55 AM.jpeg",
"mimeType": "image/jpeg",
"id": "1642487154005217857_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/992905011fba71ca828cb6f5b83df600/5D6F5B96/t51.2885-15/e35/s150x150/23347341_1932821443410985_573390529291616256_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1642487154005217857_104680?carousel_id=0",
"modifiedDate": "1510019748"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/50ae2bf3f9340ff4f1d78653087d7450/5D71B641/t51.2885-15/e35/s320x320/23280046_500879000292815_4369528731417444352_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Nov 7, 2017, 4:55 AM 1.jpeg",
"mimeType": "image/jpeg",
"id": "1642487154005217857_1046801",
"thumbnail": "https://scontent.cdninstagram.com/vp/01cf69185d6c33521df01696920039f4/5D56B014/t51.2885-15/e35/c2.0.598.598/s150x150/23280046_500879000292815_4369528731417444352_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1642487154005217857_104680?carousel_id=1",
"modifiedDate": "1510019748"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/1b1512e3914c2fda53ad3a840c2681be/5D6D9C4A/t51.2885-15/e35/s320x320/22858331_1958241871109983_4354847188874952704_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Oct 29, 2017, 7:22 PM.jpeg",
"mimeType": "image/jpeg",
"id": "1636400252659303992_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/5a5c1885bbd867feda749149e1dd56d6/5D5B9732/t51.2885-15/e35/s150x150/22858331_1958241871109983_4354847188874952704_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1636400252659303992_104680?carousel_id=0",
"modifiedDate": "1509294133"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/8615347d7e76a9300f3593abf4e9016c/5D6C344D/t51.2885-15/e35/s320x320/23098451_123562928335776_7617655378888622080_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Oct 29, 2017, 7:22 PM 1.jpeg",
"mimeType": "image/jpeg",
"id": "1636400252659303992_1046801",
"thumbnail": "https://scontent.cdninstagram.com/vp/9811c2c8bfd4f7d97045d8bbf3ecd2d1/5D708FBD/t51.2885-15/e35/s150x150/23098451_123562928335776_7617655378888622080_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1636400252659303992_104680?carousel_id=1",
"modifiedDate": "1509294133"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/01a9ab9149a22a597a80fc4cbf13738f/5D50E262/t51.2885-15/e35/s320x320/23098990_848783778612174_4475941923474898944_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Oct 29, 2017, 7:22 PM 2.jpeg",
"mimeType": "image/jpeg",
"id": "1636400252659303992_1046802",
"thumbnail": "https://scontent.cdninstagram.com/vp/ea649769b559a7f076bd727bee49a2d6/5D3C7892/t51.2885-15/e35/s150x150/23098990_848783778612174_4475941923474898944_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1636400252659303992_104680?carousel_id=2",
"modifiedDate": "1509294133"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/fc516c0e146dbf9b0fca960a9411fdf7/5D3ED20B/t51.2885-15/e35/s320x320/22802126_1517752501642900_5606082552176574464_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Oct 29, 2017, 7:22 PM 3.jpeg",
"mimeType": "image/jpeg",
"id": "1636400252659303992_1046803",
"thumbnail": "https://scontent.cdninstagram.com/vp/001f32c24f6ccd508cc4905336073053/5D71F973/t51.2885-15/e35/s150x150/22802126_1517752501642900_5606082552176574464_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1636400252659303992_104680?carousel_id=3",
"modifiedDate": "1509294133"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/9ccca5c9eadd69ccc13e53cf4f43b314/5D390493/t51.2885-15/e35/s320x320/22802520_139793160103830_5225518814476632064_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Oct 29, 2017, 7:22 PM 4.jpeg",
"mimeType": "image/jpeg",
"id": "1636400252659303992_1046804",
"thumbnail": "https://scontent.cdninstagram.com/vp/3d85f3116a34cbd465421dca31972e16/5D752E63/t51.2885-15/e35/s150x150/22802520_139793160103830_5225518814476632064_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1636400252659303992_104680?carousel_id=4",
"modifiedDate": "1509294133"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/031438a7a7f73fadff9af4f9f27e897a/5D5AB962/t51.2885-15/e35/s320x320/22639252_183626528874474_2694579090425380864_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Oct 22, 2017, 2:52 PM.jpeg",
"mimeType": "image/jpeg",
"id": "1631190853481371017_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/793785790f639b4d1c281f8f85b617b7/5D5CC492/t51.2885-15/e35/s150x150/22639252_183626528874474_2694579090425380864_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1631190853481371017_104680",
"modifiedDate": "1508673124"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/1c45c002dca07b795008b734a2dadf1a/5D567634/t51.2885-15/e35/s320x320/22580990_1537478786332265_2919339273100460032_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Oct 17, 2017, 12:11 AM.jpeg",
"mimeType": "image/jpeg",
"id": "1627123826827378513_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/9cdda68ef82a7d5415184d24b7938aae/5D5B8E4C/t51.2885-15/e35/s150x150/22580990_1537478786332265_2919339273100460032_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1627123826827378513_104680?carousel_id=0",
"modifiedDate": "1508188297"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/83c22b41fcf93fbf4c0c2a2164c74eba/5D6009D3/t51.2885-15/e35/s320x320/22430535_1082180631919539_4957960914784485376_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Oct 17, 2017, 12:11 AM 1.jpeg",
"mimeType": "image/jpeg",
"id": "1627123826827378513_1046801",
"thumbnail": "https://scontent.cdninstagram.com/vp/b1ec088d4bad03e47344f9e8742e26af/5D5CF2AB/t51.2885-15/e35/s150x150/22430535_1082180631919539_4957960914784485376_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1627123826827378513_104680?carousel_id=1",
"modifiedDate": "1508188297"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/b4fa4fc2530639cc555702153d5142da/5D5C414B/t51.2885-15/e35/s320x320/22430474_501048343603948_3378745776992681984_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Oct 17, 2017, 12:11 AM 2.jpeg",
"mimeType": "image/jpeg",
"id": "1627123826827378513_1046802",
"thumbnail": "https://scontent.cdninstagram.com/vp/0d97e18dbf6176009d599a76b76b83db/5D57C4BB/t51.2885-15/e35/s150x150/22430474_501048343603948_3378745776992681984_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1627123826827378513_104680?carousel_id=2",
"modifiedDate": "1508188297"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/c7bc86b8e610c7d9dc6642f53697ded2/5D53711A/t51.2885-15/e35/s320x320/22430500_182297355651006_8614244764025356288_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Oct 17, 2017, 12:11 AM 3.jpeg",
"mimeType": "image/jpeg",
"id": "1627123826827378513_1046803",
"thumbnail": "https://scontent.cdninstagram.com/vp/d87f16c611a0f28c4b49a5c14136d13c/5D39C3EA/t51.2885-15/e35/s150x150/22430500_182297355651006_8614244764025356288_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1627123826827378513_104680?carousel_id=3",
"modifiedDate": "1508188297"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/89a31fd438a9953a17ac5687da438581/5D54979A/t51.2885-15/e35/s320x320/22637761_173098443269988_8262666012554428416_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Oct 17, 2017, 12:11 AM 4.jpeg",
"mimeType": "image/jpeg",
"id": "1627123826827378513_1046804",
"thumbnail": "https://scontent.cdninstagram.com/vp/2b6932601eec3e25c447510f777b227d/5D40AF6A/t51.2885-15/e35/s150x150/22637761_173098443269988_8262666012554428416_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1627123826827378513_104680?carousel_id=4",
"modifiedDate": "1508188297"
},
{
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/d6d074a714c2e6758cd1fc3732211dc0/5D5C94AD/t51.2885-15/e35/s320x320/22344026_1843951665618388_1364607905617149952_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Oct 10, 2017, 7:53 PM.jpeg",
"mimeType": "image/jpeg",
"id": "1622645126576609002_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/1b8dd33db144490ca9087796b16792ca/5D3DCDD5/t51.2885-15/e35/s150x150/22344026_1843951665618388_1364607905617149952_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1622645126576609002_104680",
"modifiedDate": "1507654394"
}
],
"folders": [],
"directories": [
{
"id": "recent"
}
],
"activeRow": -1,
"filterInput": "",
"isSearchVisible": false,
"didFirstRender": true,
"loading": false
},
"Dropbox": {
"currentSelection": [],
"authenticated": false,
"files": [],
"folders": [],
"directories": [],
"activeRow": -1,
"filterInput": "",
"isSearchVisible": false
},
"Webcam": {
"cameraReady": false
}
},
"files": {
"uppy-instagramjan1820191139pm6jpeg-image/jpeg": {
"source": "Instagram",
"id": "uppy-instagramjan1820191139pm6jpeg-image/jpeg",
"name": "Instagram Jan 18, 2019, 11:39 PM 6.jpeg",
"extension": "jpeg",
"meta": {
"username": "John",
"license": "Creative Commons",
"name": "Instagram Jan 18, 2019, 11:39 PM 6.jpeg",
"type": "image/jpeg"
},
"type": "image/jpeg",
"data": {
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/385551f9d659254e79e52e2069962abe/5D3DB99B/t51.2885-15/e35/s320x320/49541690_1326265230849388_780299101204315442_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM 6.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_1046806",
"thumbnail": "https://scontent.cdninstagram.com/vp/39449810cc5a750d7da2ff0311986e5f/5D6F4796/t51.2885-15/e35/c0.0.1079.1079a/s150x150/49541690_1326265230849388_780299101204315442_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=6",
"modifiedDate": "1547843980"
},
"progress": {
"uploadStarted": 1556219160869,
"uploadComplete": true,
"percentage": 100,
"bytesUploaded": 69427,
"bytesTotal": 69427
},
"size": null,
"isRemote": true,
"remote": {
"companionUrl": "http://localhost:3020",
"url": "http://localhost:3020/instagram/get/1959779810654008285_104680?carousel_id=6",
"body": {
"fileId": "1959779810654008285_1046806"
},
"providerOptions": {
"companionUrl": "http://localhost:3020",
"provider": "instagram",
"authProvider": "instagram",
"pluginId": "Instagram"
}
},
"preview": "https://scontent.cdninstagram.com/vp/39449810cc5a750d7da2ff0311986e5f/5D6F4796/t51.2885-15/e35/c0.0.1079.1079a/s150x150/49541690_1326265230849388_780299101204315442_n.jpg?_nc_ht=scontent.cdninstagram.com",
"serverToken": "4a8616b1-58c9-464c-a5f0-450f4fac7959",
"response": {
"uploadURL": "https://master.tus.io/files/0db2bbb15785b5fbe1e04450a2ad7e27+NMjcmLga7xPuGJT6Ri65E4qN34Hck4CpgPG4r8uQdR2iyOTNDS2_hSHzROTuV.ApP.F9tLbZR04303y.X0apIHXPstqQAgOPyO1tbUKfqJ3SQ6XkX_gRfmc8hadlJK_H"
},
"uploadURL": "https://master.tus.io/files/0db2bbb15785b5fbe1e04450a2ad7e27+NMjcmLga7xPuGJT6Ri65E4qN34Hck4CpgPG4r8uQdR2iyOTNDS2_hSHzROTuV.ApP.F9tLbZR04303y.X0apIHXPstqQAgOPyO1tbUKfqJ3SQ6XkX_gRfmc8hadlJK_H",
"isPaused": false
},
"uppy-instagramjan1820191139pm5jpeg-image/jpeg": {
"source": "Instagram",
"id": "uppy-instagramjan1820191139pm5jpeg-image/jpeg",
"name": "Instagram Jan 18, 2019, 11:39 PM 5.jpeg",
"extension": "jpeg",
"meta": {
"username": "John",
"license": "Creative Commons",
"name": "Instagram Jan 18, 2019, 11:39 PM 5.jpeg",
"type": "image/jpeg"
},
"type": "image/jpeg",
"data": {
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/e93879efc4dad02ed95f28c845b30c1b/5D6EE4D2/t51.2885-15/e35/s320x320/50024282_2330888297144674_8558997522361102927_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM 5.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_1046805",
"thumbnail": "https://scontent.cdninstagram.com/vp/c173758a1dbcdc033973a4d9b77061fa/5D3FD13A/t51.2885-15/e35/c0.0.1079.1079a/s150x150/50024282_2330888297144674_8558997522361102927_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=5",
"modifiedDate": "1547843980"
},
"progress": {
"uploadStarted": 1556219160871,
"uploadComplete": true,
"percentage": 100,
"bytesUploaded": null,
"bytesTotal": null
},
"size": null,
"isRemote": true,
"remote": {
"companionUrl": "http://localhost:3020",
"url": "http://localhost:3020/instagram/get/1959779810654008285_104680?carousel_id=5",
"body": {
"fileId": "1959779810654008285_1046805"
},
"providerOptions": {
"companionUrl": "http://localhost:3020",
"provider": "instagram",
"authProvider": "instagram",
"pluginId": "Instagram"
}
},
"preview": "https://scontent.cdninstagram.com/vp/c173758a1dbcdc033973a4d9b77061fa/5D3FD13A/t51.2885-15/e35/c0.0.1079.1079a/s150x150/50024282_2330888297144674_8558997522361102927_n.jpg?_nc_ht=scontent.cdninstagram.com",
"serverToken": "78105341-af5e-4576-9fc7-d0a11becdc51",
"response": {
"uploadURL": "https://master.tus.io/files/691bd69026e91a7568a240cb909139e5+gDSpFJXfEGIaPQyzAZk0OZ_y5g2fsNI2FPdNuoCQl0ka7Ch.uAXWtsUhjyB3ieH84JkHl03_h5jAXJn4X6TZRsF8yfZqVVBaYRdZbOqpZcYoBwdQ.SNFmASrukqcmYVa"
},
"uploadURL": "https://master.tus.io/files/691bd69026e91a7568a240cb909139e5+gDSpFJXfEGIaPQyzAZk0OZ_y5g2fsNI2FPdNuoCQl0ka7Ch.uAXWtsUhjyB3ieH84JkHl03_h5jAXJn4X6TZRsF8yfZqVVBaYRdZbOqpZcYoBwdQ.SNFmASrukqcmYVa",
"isPaused": false
},
"uppy-instagramjan1820191139pm4jpeg-image/jpeg": {
"source": "Instagram",
"id": "uppy-instagramjan1820191139pm4jpeg-image/jpeg",
"name": "Instagram Jan 18, 2019, 11:39 PM 4.jpeg",
"extension": "jpeg",
"meta": {
"username": "John",
"license": "Creative Commons",
"name": "Instagram Jan 18, 2019, 11:39 PM 4.jpeg",
"type": "image/jpeg"
},
"type": "image/jpeg",
"data": {
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/9b869a8590154eb01bdb8c4d834dbc96/5D68F919/t51.2885-15/e35/s320x320/49536508_2715697688455532_3941725382632324585_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM 4.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_1046804",
"thumbnail": "https://scontent.cdninstagram.com/vp/9d5ef7d80443c6e606dd2032bd9bbef3/5D736861/t51.2885-15/e35/s150x150/49536508_2715697688455532_3941725382632324585_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=4",
"modifiedDate": "1547843980"
},
"progress": {
"uploadStarted": 1556219160872,
"uploadComplete": true,
"percentage": 100,
"bytesUploaded": 79296,
"bytesTotal": 79296
},
"size": null,
"isRemote": true,
"remote": {
"companionUrl": "http://localhost:3020",
"url": "http://localhost:3020/instagram/get/1959779810654008285_104680?carousel_id=4",
"body": {
"fileId": "1959779810654008285_1046804"
},
"providerOptions": {
"companionUrl": "http://localhost:3020",
"provider": "instagram",
"authProvider": "instagram",
"pluginId": "Instagram"
}
},
"preview": "https://scontent.cdninstagram.com/vp/9d5ef7d80443c6e606dd2032bd9bbef3/5D736861/t51.2885-15/e35/s150x150/49536508_2715697688455532_3941725382632324585_n.jpg?_nc_ht=scontent.cdninstagram.com",
"serverToken": "11061e76-6fa9-4c0d-b877-c0759f077d94",
"response": {
"uploadURL": "https://master.tus.io/files/f49cc001f5447ea90c898b69b1789d96+GD3uaee_lz.07izRtBlC1E99eum2CAWHHGOgdiTDyP8DpGM31fiBIQrCzCnbjGZ2xVqad3W1S1SL5R96dRlNdNKie4uDpRrhKcW17RQBkWyKyiE5fDKe8FhEo.9JstBx"
},
"uploadURL": "https://master.tus.io/files/f49cc001f5447ea90c898b69b1789d96+GD3uaee_lz.07izRtBlC1E99eum2CAWHHGOgdiTDyP8DpGM31fiBIQrCzCnbjGZ2xVqad3W1S1SL5R96dRlNdNKie4uDpRrhKcW17RQBkWyKyiE5fDKe8FhEo.9JstBx",
"isPaused": false
},
"uppy-instagramjan1820191139pm3jpeg-image/jpeg": {
"source": "Instagram",
"id": "uppy-instagramjan1820191139pm3jpeg-image/jpeg",
"name": "Instagram Jan 18, 2019, 11:39 PM 3.jpeg",
"extension": "jpeg",
"meta": {
"username": "John",
"license": "Creative Commons",
"name": "Instagram Jan 18, 2019, 11:39 PM 3.jpeg",
"type": "image/jpeg"
},
"type": "image/jpeg",
"data": {
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/3da7cd1f79dcecde1e871ea09cf65a87/5D737FDF/t51.2885-15/e35/s320x320/50515172_1138232866338235_1751853475314282763_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM 3.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_1046803",
"thumbnail": "https://scontent.cdninstagram.com/vp/5c51a2dac516e575a66cf10c39615faf/5D6922A7/t51.2885-15/e35/s150x150/50515172_1138232866338235_1751853475314282763_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=3",
"modifiedDate": "1547843980"
},
"progress": {
"uploadStarted": 1556219160872,
"uploadComplete": true,
"percentage": 100,
"bytesUploaded": 129055,
"bytesTotal": 129055
},
"size": null,
"isRemote": true,
"remote": {
"companionUrl": "http://localhost:3020",
"url": "http://localhost:3020/instagram/get/1959779810654008285_104680?carousel_id=3",
"body": {
"fileId": "1959779810654008285_1046803"
},
"providerOptions": {
"companionUrl": "http://localhost:3020",
"provider": "instagram",
"authProvider": "instagram",
"pluginId": "Instagram"
}
},
"preview": "https://scontent.cdninstagram.com/vp/5c51a2dac516e575a66cf10c39615faf/5D6922A7/t51.2885-15/e35/s150x150/50515172_1138232866338235_1751853475314282763_n.jpg?_nc_ht=scontent.cdninstagram.com",
"serverToken": "b12c3999-1be2-4a15-86ea-530e73e6da76",
"response": {
"uploadURL": "https://master.tus.io/files/9a89b79d6ed7b6ced857a036aad43fb6+WmV6kpylmuFV1HHA3JKIgmU5wJRwyRmweOpEUjQT86Zc5YLVa_nwsmV.P4Pl4oF15rmhb5PvkukMVTKN98WQff_QGD0TjBSHDYqVNOVGuJ8_w4.A.IWOl3yRSk58rkHf"
},
"uploadURL": "https://master.tus.io/files/9a89b79d6ed7b6ced857a036aad43fb6+WmV6kpylmuFV1HHA3JKIgmU5wJRwyRmweOpEUjQT86Zc5YLVa_nwsmV.P4Pl4oF15rmhb5PvkukMVTKN98WQff_QGD0TjBSHDYqVNOVGuJ8_w4.A.IWOl3yRSk58rkHf",
"isPaused": false
},
"uppy-instagramfeb112019334pmjpeg-image/jpeg": {
"source": "Instagram",
"id": "uppy-instagramfeb112019334pmjpeg-image/jpeg",
"name": "Instagram Feb 11, 2019, 3:34 PM.jpeg",
"extension": "jpeg",
"meta": {
"username": "John",
"license": "Creative Commons",
"name": "Instagram Feb 11, 2019, 3:34 PM.jpeg",
"type": "image/jpeg"
},
"type": "image/jpeg",
"data": {
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/420d2bdc2cb30251d7ecf8e516f7fa7d/5D55A291/t51.2885-15/e35/s320x320/50692753_2474466385958388_3994336603663016042_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Feb 11, 2019, 3:34 PM.jpeg",
"mimeType": "image/jpeg",
"id": "1976930126949922734_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/ff2400b726bbd3415c4a07b723615578/5D3C08E9/t51.2885-15/e35/s150x150/50692753_2474466385958388_3994336603663016042_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1976930126949922734_104680",
"modifiedDate": "1549888457"
},
"progress": {
"uploadStarted": 1556219160874,
"uploadComplete": true,
"percentage": 100,
"bytesUploaded": 86926,
"bytesTotal": 86926
},
"size": null,
"isRemote": true,
"remote": {
"companionUrl": "http://localhost:3020",
"url": "http://localhost:3020/instagram/get/1976930126949922734_104680",
"body": {
"fileId": "1976930126949922734_104680"
},
"providerOptions": {
"companionUrl": "http://localhost:3020",
"provider": "instagram",
"authProvider": "instagram",
"pluginId": "Instagram"
}
},
"preview": "https://scontent.cdninstagram.com/vp/ff2400b726bbd3415c4a07b723615578/5D3C08E9/t51.2885-15/e35/s150x150/50692753_2474466385958388_3994336603663016042_n.jpg?_nc_ht=scontent.cdninstagram.com",
"serverToken": "dde6723d-29f7-4983-8b42-0a9e18683872",
"response": {
"uploadURL": "https://master.tus.io/files/533f69131228bc58faef2b247af1f1bd+hHkHXy5FmiPzutH2jEfQMMHiUtae68lrmZKRenqvSSU94FcsKxurqzg10jHSvV3lFps9zdzXfbmb8VBEX6znqffM603qapJuDhfeNZkgrBxOkgKglyvQLV_9ydgaJNLE"
},
"uploadURL": "https://master.tus.io/files/533f69131228bc58faef2b247af1f1bd+hHkHXy5FmiPzutH2jEfQMMHiUtae68lrmZKRenqvSSU94FcsKxurqzg10jHSvV3lFps9zdzXfbmb8VBEX6znqffM603qapJuDhfeNZkgrBxOkgKglyvQLV_9ydgaJNLE",
"isPaused": false
},
"uppy-instagramjan1820191139pmjpeg-image/jpeg": {
"source": "Instagram",
"id": "uppy-instagramjan1820191139pmjpeg-image/jpeg",
"name": "Instagram Jan 18, 2019, 11:39 PM.jpeg",
"extension": "jpeg",
"meta": {
"username": "John",
"license": "Creative Commons",
"name": "Instagram Jan 18, 2019, 11:39 PM.jpeg",
"type": "image/jpeg"
},
"type": "image/jpeg",
"data": {
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/78aa49ad8ccbe12b07fb20f07c1b9a1d/5D520E84/t51.2885-15/e35/s320x320/49858772_2238267119827712_38852393303322952_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_104680",
"thumbnail": "https://scontent.cdninstagram.com/vp/b3eda9daaa214951c1fb1a0f7000d8de/5D3F3F89/t51.2885-15/e35/s150x150/49858772_2238267119827712_38852393303322952_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=0",
"modifiedDate": "1547843980"
},
"progress": {
"uploadStarted": 1556219160874,
"uploadComplete": true,
"percentage": 100,
"bytesUploaded": 80471,
"bytesTotal": 80471
},
"size": null,
"isRemote": true,
"remote": {
"companionUrl": "http://localhost:3020",
"url": "http://localhost:3020/instagram/get/1959779810654008285_104680?carousel_id=0",
"body": {
"fileId": "1959779810654008285_104680"
},
"providerOptions": {
"companionUrl": "http://localhost:3020",
"provider": "instagram",
"authProvider": "instagram",
"pluginId": "Instagram"
}
},
"preview": "https://scontent.cdninstagram.com/vp/b3eda9daaa214951c1fb1a0f7000d8de/5D3F3F89/t51.2885-15/e35/s150x150/49858772_2238267119827712_38852393303322952_n.jpg?_nc_ht=scontent.cdninstagram.com",
"serverToken": "60675af3-fec5-45d8-87d6-30363a0534fa",
"response": {
"uploadURL": "https://master.tus.io/files/1f4f0dabad36e2995d4fde177ce64d0c+C.z6wtvRCGwt5hr4hUpbLE3NrduyiOiDn55wE8e81j88CRvceybDQ04iOQhlfl817wlPa0f2JU5aPjhZlp.pA4sBKPqLOlDMAkakwsVIGR4IW5nLYRoRGuauiJjYDMW."
},
"uploadURL": "https://master.tus.io/files/1f4f0dabad36e2995d4fde177ce64d0c+C.z6wtvRCGwt5hr4hUpbLE3NrduyiOiDn55wE8e81j88CRvceybDQ04iOQhlfl817wlPa0f2JU5aPjhZlp.pA4sBKPqLOlDMAkakwsVIGR4IW5nLYRoRGuauiJjYDMW.",
"isPaused": false
},
"uppy-instagramjan1820191139pm1jpeg-image/jpeg": {
"source": "Instagram",
"id": "uppy-instagramjan1820191139pm1jpeg-image/jpeg",
"name": "Instagram Jan 18, 2019, 11:39 PM 1.jpeg",
"extension": "jpeg",
"meta": {
"username": "John",
"license": "Creative Commons",
"name": "Instagram Jan 18, 2019, 11:39 PM 1.jpeg",
"type": "image/jpeg"
},
"type": "image/jpeg",
"data": {
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/ec36a8b8b7db546abeb3cdcf12b52ba5/5D77DC5F/t51.2885-15/e35/s320x320/49858316_1974473399524283_2231924729468134373_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM 1.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_1046801",
"thumbnail": "https://scontent.cdninstagram.com/vp/d8ef3dd687b6aab396cae9284226c7df/5D711727/t51.2885-15/e35/s150x150/49858316_1974473399524283_2231924729468134373_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=1",
"modifiedDate": "1547843980"
},
"progress": {
"uploadStarted": 1556219160876,
"uploadComplete": false,
"percentage": 15,
"bytesUploaded": 12561.5,
"bytesTotal": 80471
},
"size": null,
"isRemote": true,
"remote": {
"companionUrl": "http://localhost:3020",
"url": "http://localhost:3020/instagram/get/1959779810654008285_104680?carousel_id=1",
"body": {
"fileId": "1959779810654008285_1046801"
},
"providerOptions": {
"companionUrl": "http://localhost:3020",
"provider": "instagram",
"authProvider": "instagram",
"pluginId": "Instagram"
}
},
"preview": "https://scontent.cdninstagram.com/vp/d8ef3dd687b6aab396cae9284226c7df/5D711727/t51.2885-15/e35/s150x150/49858316_1974473399524283_2231924729468134373_n.jpg?_nc_ht=scontent.cdninstagram.com",
"serverToken": "d6f75847-6685-456e-9db6-c345f32749af",
"isPaused": true
},
"uppy-instagramjan1820191139pm2jpeg-image/jpeg": {
"source": "Instagram",
"id": "uppy-instagramjan1820191139pm2jpeg-image/jpeg",
"name": "Instagram Jan 18, 2019, 11:39 PM 2.jpeg",
"extension": "jpeg",
"meta": {
"username": "John",
"license": "Creative Commons",
"name": "Instagram Jan 18, 2019, 11:39 PM 2.jpeg",
"type": "image/jpeg"
},
"type": "image/jpeg",
"data": {
"isFolder": false,
"icon": "https://scontent.cdninstagram.com/vp/3055f2ae78d775fc031654d20e791ebc/5D51235E/t51.2885-15/e35/s320x320/49933915_184580175831450_4288362971970794931_n.jpg?_nc_ht=scontent.cdninstagram.com",
"name": "Instagram Jan 18, 2019, 11:39 PM 2.jpeg",
"mimeType": "image/jpeg",
"id": "1959779810654008285_1046802",
"thumbnail": "https://scontent.cdninstagram.com/vp/3d1b5aa16b8d64bc7c14ae1e3b13dfbe/5D5DD553/t51.2885-15/e35/c0.0.1079.1079a/s150x150/49933915_184580175831450_4288362971970794931_n.jpg?_nc_ht=scontent.cdninstagram.com",
"requestPath": "1959779810654008285_104680?carousel_id=2",
"modifiedDate": "1547843980"
},
"progress": {
"uploadStarted": 1556219160878,
"uploadComplete": true,
"percentage": 100,
"bytesUploaded": 111226,
"bytesTotal": 111226
},
"size": null,
"isRemote": true,
"remote": {
"companionUrl": "http://localhost:3020",
"url": "http://localhost:3020/instagram/get/1959779810654008285_104680?carousel_id=2",
"body": {
"fileId": "1959779810654008285_1046802"
},
"providerOptions": {
"companionUrl": "http://localhost:3020",
"provider": "instagram",
"authProvider": "instagram",
"pluginId": "Instagram"
}
},
"preview": "https://scontent.cdninstagram.com/vp/3d1b5aa16b8d64bc7c14ae1e3b13dfbe/5D5DD553/t51.2885-15/e35/c0.0.1079.1079a/s150x150/49933915_184580175831450_4288362971970794931_n.jpg?_nc_ht=scontent.cdninstagram.com",
"serverToken": "92ae2486-90f0-4e2f-82f9-285ad87e2c5a",
"response": {
"uploadURL": "https://master.tus.io/files/c83164658faa4fdd7a12dc9e6044510f+YJKdoXIXl7PjA_UXhQ.PDqPhnk4A_O_LWm.3qau9X8kzHXg7dgI.igu5n_yPVBG0PezDPi2b8fSjBfc2KZjX3qUZxST9pb0iQP20_wKPax9Y4slQRQr7qpiUugFTljbA"
},
"uploadURL": "https://master.tus.io/files/c83164658faa4fdd7a12dc9e6044510f+YJKdoXIXl7PjA_UXhQ.PDqPhnk4A_O_LWm.3qau9X8kzHXg7dgI.igu5n_yPVBG0PezDPi2b8fSjBfc2KZjX3qUZxST9pb0iQP20_wKPax9Y4slQRQr7qpiUugFTljbA",
"isPaused": false
}
},
"currentUploads": {
"cjux0phjn00012a5vzjw0buly": {
"fileIDs": [
"uppy-instagramjan1820191139pm6jpeg-image/jpeg",
"uppy-instagramjan1820191139pm5jpeg-image/jpeg",
"uppy-instagramjan1820191139pm4jpeg-image/jpeg",
"uppy-instagramjan1820191139pm3jpeg-image/jpeg",
"uppy-instagramfeb112019334pmjpeg-image/jpeg",
"uppy-instagramjan1820191139pmjpeg-image/jpeg",
"uppy-instagramjan1820191139pm1jpeg-image/jpeg",
"uppy-instagramjan1820191139pm2jpeg-image/jpeg"
],
"step": 0,
"result": {}
}
},
"allowNewUpload": true,
"capabilities": {
"uploadProgress": true,
"individualCancellation": true,
"resumableUploads": true
},
"totalProgress": 1328,
"meta": {
"username": "John",
"license": "Creative Commons"
},
"info": {
"isHidden": true,
"type": "info",
"message": ""
},
"error": null
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

View file

@ -1 +0,0 @@
./cat.jpg

View file

@ -1 +0,0 @@
./cat.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 MiB

View file

@ -1,5 +0,0 @@
%PDF-1.
1 0 obj<</Pages 2 0 R>>endobj
2 0 obj<</Kids[3 0 R]/Count 1>>endobj
3 0 obj<</Parent 2 0 R>>endobj
trailer <</Root 1 0 R>>

View file

@ -1,217 +0,0 @@
describe('Dashboard with @uppy/aws-s3-multipart', () => {
beforeEach(() => {
cy.visit('/dashboard-aws-multipart')
cy.get('.uppy-Dashboard-input:first').as('file-input')
cy.intercept({ method: 'POST', pathname: '/s3/multipart' }).as('post')
cy.intercept({ method: 'GET', pathname: '/s3/multipart/*/1' }).as('get')
cy.intercept({ method: 'PUT' }).as('put')
})
it('should upload cat image successfully', () => {
cy.get('@file-input').selectFile('cypress/fixtures/images/cat.jpg', {
force: true,
})
cy.get('.uppy-StatusBar-actionBtn--upload').click()
cy.wait(['@post', '@get', '@put'])
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Complete')
})
it('should upload Russian poem image successfully', () => {
const fileName = '١٠ كم мест для Нью-Йорке.pdf'
cy.get('@file-input').selectFile(`cypress/fixtures/images/${fileName}`, {
force: true,
})
cy.get('.uppy-StatusBar-actionBtn--upload').click()
cy.wait(['@post', '@get', '@put'])
cy.get('.uppy-Dashboard-Item-name').should('contain', fileName)
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Complete')
})
it('should handle retry request gracefully', () => {
cy.get('@file-input').selectFile('cypress/fixtures/images/cat.jpg', {
force: true,
})
cy.intercept('POST', '/s3/multipart', {
forceNetworkError: true,
times: 1,
}).as('createMultipartUpload-fails')
cy.get('.uppy-StatusBar-actionBtn--upload').click()
cy.wait(['@createMultipartUpload-fails'])
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Upload failed')
cy.intercept('POST', '/s3/multipart', {
statusCode: 200,
times: 1,
body: JSON.stringify({
key: 'mocked-key-attempt1',
uploadId: 'mocked-uploadId-attempt1',
}),
}).as('createMultipartUpload-attempt1')
cy.intercept(
'GET',
'/s3/multipart/mocked-uploadId-attempt1/1?key=mocked-key-attempt1',
{ forceNetworkError: true },
).as('signPart-fails')
cy.intercept(
'DELETE',
'/s3/multipart/mocked-uploadId-attempt1?key=mocked-key-attempt1',
{ statusCode: 200, body: '{}' },
).as('abortAttempt-1')
cy.get('.uppy-StatusBar-actions > .uppy-c-btn').click()
cy.wait([
'@createMultipartUpload-attempt1',
'@signPart-fails',
'@abortAttempt-1',
])
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Upload failed')
cy.intercept('POST', '/s3/multipart', {
statusCode: 200,
times: 1,
body: JSON.stringify({
key: 'mocked-key-attempt2',
uploadId: 'mocked-uploadId-attempt2',
}),
}).as('createMultipartUpload-attempt2')
cy.intercept(
'GET',
'/s3/multipart/mocked-uploadId-attempt2/1?key=mocked-key-attempt2',
{
statusCode: 200,
headers: {
ETag: 'W/"222-GXE2wLoMKDihw3wxZFH1APdUjHM"',
},
body: JSON.stringify({ url: '/put-fail', expires: 8 }),
},
).as('signPart-toFail')
cy.intercept(
'DELETE',
'/s3/multipart/mocked-uploadId-attempt2?key=mocked-key-attempt2',
{ statusCode: 200, body: '{}' },
).as('abortAttempt-2')
cy.intercept('PUT', '/put-fail', { forceNetworkError: true }).as(
'put-fails',
)
cy.get('.uppy-StatusBar-actions > .uppy-c-btn').click()
cy.wait([
'@createMultipartUpload-attempt2',
'@signPart-toFail',
...Array(5).fill('@put-fails'),
'@abortAttempt-2',
])
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Upload failed')
cy.intercept(
'GET',
'/s3/multipart/mocked-uploadId-attempt2/1?key=mocked-key-attempt2',
{
statusCode: 200,
headers: {
ETag: 'ETag-attempt2',
},
body: JSON.stringify({ url: '/put-success-attempt2', expires: 8 }),
},
).as('signPart-attempt2')
cy.intercept('PUT', '/put-success-attempt2', {
statusCode: 200,
headers: {
ETag: 'ETag-attempt2',
},
}).as('put-attempt2')
cy.intercept(
'POST',
'/s3/multipart/mocked-uploadId-attempt2/complete?key=mocked-key-attempt2',
{ forceNetworkError: true },
).as('completeMultipartUpload-fails')
cy.get('.uppy-StatusBar-actions > .uppy-c-btn').click()
cy.wait([
'@createMultipartUpload-attempt2',
'@signPart-attempt2',
'@put-attempt2',
'@completeMultipartUpload-fails',
'@abortAttempt-2',
])
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Upload failed')
cy.intercept('POST', '/s3/multipart', {
statusCode: 200,
times: 1,
body: JSON.stringify({
key: 'mocked-key-attempt3',
uploadId: 'mocked-uploadId-attempt3',
}),
}).as('createMultipartUpload-attempt3')
let intercepted = 0
cy.intercept(
'GET',
'/s3/multipart/mocked-uploadId-attempt3/1?key=mocked-key-attempt3',
(req) => {
if (intercepted++ < 2) {
// Ensure that Uppy can recover from at least 2 network errors at this stage.
req.destroy()
return
}
req.reply({
statusCode: 200,
headers: {
ETag: 'ETag-attempt3',
},
body: JSON.stringify({ url: '/put-success-attempt3', expires: 8 }),
})
},
).as('signPart-attempt3')
cy.intercept('PUT', '/put-success-attempt3', {
statusCode: 200,
headers: {
ETag: 'ETag-attempt3',
},
}).as('put-attempt3')
cy.intercept(
'POST',
'/s3/multipart/mocked-uploadId-attempt3/complete?key=mocked-key-attempt3',
{
statusCode: 200,
body: JSON.stringify({
location: 'someLocation',
}),
},
).as('completeMultipartUpload-attempt3')
cy.get('.uppy-StatusBar-actions > .uppy-c-btn').click()
cy.wait([
'@createMultipartUpload-attempt3',
...Array(3).fill('@signPart-attempt3'),
'@put-attempt3',
'@completeMultipartUpload-attempt3',
])
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Complete')
})
it('should complete when resuming after pause', () => {
cy.get('@file-input').selectFile(
[
'cypress/fixtures/images/cat.jpg',
'cypress/fixtures/images/traffic.jpg',
],
{ force: true },
)
cy.get('.uppy-StatusBar-actionBtn--upload').click()
cy.wait('@post')
cy.get('button[data-cy=togglePauseResume]').click()
cy.wait(300) // Wait an arbitrary amount of time as a user would do.
cy.get('button[data-cy=togglePauseResume]').click()
cy.wait('@get')
cy.get('button[data-cy=togglePauseResume]').click()
cy.wait(300) // Wait an arbitrary amount of time as a user would do.
cy.get('button[data-cy=togglePauseResume]').click()
cy.wait(['@get', '@put'])
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Complete')
})
})

View file

@ -1,18 +0,0 @@
describe('Dashboard with @uppy/aws-s3', () => {
beforeEach(() => {
cy.visit('/dashboard-aws')
cy.get('.uppy-Dashboard-input:first').as('file-input')
cy.intercept({ method: 'GET', pathname: '/s3/params' }).as('get')
cy.intercept({ method: 'POST' }).as('post')
})
it('should upload cat image successfully', () => {
cy.get('@file-input').selectFile('cypress/fixtures/images/cat.jpg', {
force: true,
})
cy.get('.uppy-StatusBar-actionBtn--upload').click()
cy.wait(['@post', '@get'])
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Complete')
})
})

View file

@ -1,63 +0,0 @@
function uglierBytes(text) {
const KB = 2 ** 10
const MB = KB * KB
if (text.endsWith(' KB')) {
return Number(text.slice(0, -3)) * KB
}
if (text.endsWith(' MB')) {
return Number(text.slice(0, -3)) * MB
}
if (text.endsWith(' B')) {
return Number(text.slice(0, -2))
}
throw new Error(
`Not what the computer thinks a human-readable size string look like: ${text}`,
)
}
describe('dashboard-compressor', () => {
beforeEach(() => {
cy.visit('/dashboard-compressor')
cy.get('.uppy-Dashboard-input:first').as('file-input')
})
it('should compress images', () => {
const sizeBeforeCompression = []
cy.get('@file-input').selectFile(
[
'cypress/fixtures/images/cat.jpg',
'cypress/fixtures/images/traffic.jpg',
],
{ force: true },
)
cy.get('.uppy-Dashboard-Item-statusSize').each((element) => {
const text = element.text()
sizeBeforeCompression.push(uglierBytes(text))
})
cy.window().then(({ uppy }) => {
uppy.on('preprocess-complete', (file) => {
expect(file.extension).to.equal('webp')
expect(file.type).to.equal('image/webp')
cy.get('.uppy-Dashboard-Item-statusSize').should((elements) => {
expect(elements).to.have.length(sizeBeforeCompression.length)
for (let i = 0; i < elements.length; i++) {
expect(sizeBeforeCompression[i]).to.be.greaterThan(
uglierBytes(elements[i].textContent),
)
}
})
})
cy.get('.uppy-StatusBar-actionBtn--upload').click()
})
})
})

View file

@ -1,378 +0,0 @@
import type Uppy from '@uppy/core'
import type Transloadit from '@uppy/transloadit'
function getPlugin<M = any, B = any>(uppy: Uppy<M, B>) {
return uppy.getPlugin<Transloadit<M, B>>('Transloadit')!
}
describe('Dashboard with Transloadit', () => {
beforeEach(() => {
cy.visit('/dashboard-transloadit')
cy.get('.uppy-Dashboard-input:first').as('file-input')
})
it('should upload all files as a single assembly with UppyFile metadata in Upload-Metadata', () => {
cy.intercept({ path: '/assemblies', method: 'POST' }).as('createAssembly')
cy.get('@file-input').selectFile(
[
'cypress/fixtures/images/cat.jpg',
'cypress/fixtures/images/traffic.jpg',
],
{ force: true },
)
cy.window().then(({ uppy }) => {
// Set metadata on all files
uppy.setMeta({ sharedMetaProperty: 'bar' })
const [file1, file2] = uppy.getFiles()
// Set unique metdata per file as before that's how we determined to create multiple assemblies
uppy.setFileMeta(file1.id, { one: 'one' })
uppy.setFileMeta(file2.id, { two: 'two' })
cy.get('.uppy-StatusBar-actionBtn--upload').click()
cy.intercept('POST', '/resumable/*', (req) => {
expect(req.headers['upload-metadata']).to.include('sharedMetaProperty')
req.continue()
})
cy.wait(['@createAssembly']).then(() => {
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Complete')
// should only create one assembly
cy.get('@createAssembly.all').should('have.length', 1)
})
})
})
it.skip('should close assembly when cancelled', () => {
cy.intercept({ path: '/resumable/*', method: 'POST' }).as('tusCreate')
cy.intercept({ path: '/assemblies', method: 'POST' }).as('createAssemblies')
cy.intercept({ path: '/assemblies/*', method: 'DELETE' }).as('delete')
cy.window().then(({ uppy }) => {
cy.get('@file-input').selectFile(
[
'cypress/fixtures/images/cat.jpg',
'cypress/fixtures/images/traffic.jpg',
'cypress/fixtures/images/car.jpg',
],
{ force: true },
)
cy.get('.uppy-StatusBar-actionBtn--upload').click()
cy.wait(['@createAssemblies', '@tusCreate']).then(() => {
const { assembly } = getPlugin(uppy)
expect(assembly.closed).to.be.false
uppy.cancelAll()
cy.wait(['@delete']).then(() => {
expect(assembly.closed).to.be.true
})
})
})
})
it('should not emit error if upload is cancelled right away', () => {
cy.intercept({ path: '/assemblies', method: 'POST' }).as('createAssemblies')
cy.get('@file-input').selectFile('cypress/fixtures/images/cat.jpg', {
force: true,
})
cy.get('.uppy-StatusBar-actionBtn--upload').click()
const handler = cy.spy()
cy.window().then(({ uppy }) => {
const { files } = uppy.getState()
uppy.on('upload-error', handler)
const [fileID] = Object.keys(files)
uppy.removeFile(fileID)
uppy.removeFile(fileID)
cy.wait('@createAssemblies').then(() => expect(handler).not.to.be.called)
})
})
it('should not re-use erroneous tus keys', () => {
function createAssemblyStatus({
message,
assembly_id,
bytes_expected,
...other
}) {
return {
message,
assembly_id,
parent_id: null,
account_id: 'deadbeef',
account_name: 'foo',
account_slug: 'foo',
template_id: null,
template_name: null,
instance: 'test.transloadit.com',
assembly_url: `http://api2.test.transloadit.com/assemblies/${assembly_id}`,
assembly_ssl_url: `https://api2-test.transloadit.com/assemblies/${assembly_id}`,
uppyserver_url: 'https://api2-test.transloadit.com/companion/',
companion_url: 'https://api2-test.transloadit.com/companion/',
websocket_url: 'about:blank',
tus_url: 'https://api2-test.transloadit.com/resumable/files/',
bytes_received: 0,
bytes_expected,
upload_duration: 0.162,
client_agent: null,
client_ip: null,
client_referer: null,
transloadit_client:
'uppy-core:3.2.0,uppy-transloadit:3.1.3,uppy-tus:3.1.0,uppy-dropbox:3.1.1,uppy-box:2.1.1,uppy-facebook:3.1.1,uppy-google-drive:3.1.1,uppy-instagram:3.1.1,uppy-onedrive:3.1.1,uppy-zoom:2.1.1,uppy-url:3.3.1',
start_date: new Date().toISOString(),
upload_meta_data_extracted: false,
warnings: [],
is_infinite: false,
has_dupe_jobs: false,
execution_start: null,
execution_duration: null,
queue_duration: 0.009,
jobs_queue_duration: 0,
notify_start: null,
notify_url: null,
notify_response_code: null,
notify_response_data: null,
notify_duration: null,
last_job_completed: null,
fields: {},
running_jobs: [],
bytes_usage: 0,
executing_jobs: [],
started_jobs: [],
parent_assembly_status: null,
params: '{}',
template: null,
merged_params: '{}',
expected_tus_uploads: 1,
started_tus_uploads: 0,
finished_tus_uploads: 0,
tus_uploads: [],
uploads: [],
results: {},
build_id: '4765326956',
status_endpoint: `https://api2-test.transloadit.com/assemblies/${assembly_id}`,
...other,
}
}
cy.get('@file-input').selectFile(['cypress/fixtures/images/cat.jpg'], {
force: true,
})
// SETUP for FIRST ATTEMPT (error response from Transloadit backend)
const assemblyIDAttempt1 = '500e56004f4347a288194edd7c7a0ae1'
const tusIDAttempt1 = 'a9daed4af4981880faf29b0e9596a14d'
cy.intercept('POST', 'https://api2.transloadit.com/assemblies', {
statusCode: 200,
body: JSON.stringify(
createAssemblyStatus({
ok: 'ASSEMBLY_UPLOADING',
message: 'The Assembly is still in the process of being uploaded.',
assembly_id: assemblyIDAttempt1,
bytes_expected: 263871,
}),
),
}).as('createAssembly')
cy.intercept('POST', 'https://api2-test.transloadit.com/resumable/files/', {
statusCode: 201,
headers: {
Location: `https://api2-test.transloadit.com/resumable/files/${tusIDAttempt1}`,
},
times: 1,
}).as('tusCall')
cy.intercept(
'PATCH',
`https://api2-test.transloadit.com/resumable/files/${tusIDAttempt1}`,
{
statusCode: 204,
headers: {
'Upload-Length': '263871',
'Upload-Offset': '263871',
},
times: 1,
},
)
cy.intercept(
'HEAD',
`https://api2-test.transloadit.com/resumable/files/${tusIDAttempt1}`,
{ statusCode: 204 },
)
cy.intercept(
'GET',
`https://api2-test.transloadit.com/assemblies/${assemblyIDAttempt1}`,
{
statusCode: 200,
body: JSON.stringify(
createAssemblyStatus({
error: 'INVALID_FILE_META_DATA',
http_code: 400,
message: 'Whatever error message from Transloadit backend',
reason: 'Whatever reason',
msg: 'Whatever error from Transloadit backend',
assembly_id: '500e56004f4347a288194edd7c7a0ae1',
bytes_expected: 263871,
}),
),
},
).as('failureReported')
cy.intercept('POST', 'https://transloaditstatus.com/client_error', {
statusCode: 200,
body: '{}',
})
// FIRST ATTEMPT
cy.get('.uppy-StatusBar-actionBtn--upload').click()
cy.wait(['@createAssembly', '@tusCall', '@failureReported'])
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Upload failed')
// SETUP for SECOND ATTEMPT
const assemblyIDAttempt2 = '6a3fa40e527d4d73989fce678232a5e1'
const tusIDAttempt2 = 'b8ebed4af4981880faf29b0e9596b25e'
cy.intercept('POST', 'https://api2.transloadit.com/assemblies', {
statusCode: 200,
body: JSON.stringify(
createAssemblyStatus({
ok: 'ASSEMBLY_UPLOADING',
message: 'The Assembly is still in the process of being uploaded.',
assembly_id: assemblyIDAttempt2,
tus_url: 'https://api2-test.transloadit.com/resumable/files/attempt2',
bytes_expected: 263871,
}),
),
}).as('createAssembly-attempt2')
cy.intercept(
'POST',
'https://api2-test.transloadit.com/resumable/files/attempt2',
{
statusCode: 201,
headers: {
'Upload-Length': '263871',
'Upload-Offset': '0',
Location: `https://api2-test.transloadit.com/resumable/files/${tusIDAttempt2}`,
},
times: 1,
},
).as('tusCall-attempt2')
cy.intercept(
'PATCH',
`https://api2-test.transloadit.com/resumable/files/${tusIDAttempt2}`,
{
statusCode: 204,
headers: {
'Upload-Length': '263871',
'Upload-Offset': '263871',
'Tus-Resumable': '1.0.0',
},
times: 1,
},
)
cy.intercept(
'HEAD',
`https://api2-test.transloadit.com/resumable/files/${tusIDAttempt2}`,
{ statusCode: 204 },
)
cy.intercept(
'GET',
`https://api2-test.transloadit.com/assemblies/${assemblyIDAttempt2}`,
{
statusCode: 200,
body: JSON.stringify(
createAssemblyStatus({
ok: 'ASSEMBLY_COMPLETED',
http_code: 200,
message: 'The Assembly was successfully completed.',
assembly_id: assemblyIDAttempt2,
bytes_received: 263871,
bytes_expected: 263871,
}),
),
},
).as('assemblyCompleted-attempt2')
// SECOND ATTEMPT
cy.get('.uppy-StatusBar-actions > .uppy-c-btn').click()
cy.wait([
'@createAssembly-attempt2',
'@tusCall-attempt2',
'@assemblyCompleted-attempt2',
])
})
it('should complete on retry', () => {
cy.intercept('/assemblies/*').as('assemblies')
cy.intercept('/resumable/*').as('resumable')
cy.get('@file-input').selectFile(
[
'cypress/fixtures/images/cat.jpg',
'cypress/fixtures/images/traffic.jpg',
],
{ force: true },
)
cy.intercept('POST', 'https://transloaditstatus.com/client_error', {
statusCode: 200,
body: '{}',
})
cy.intercept(
{ method: 'POST', pathname: '/assemblies', times: 1 },
{ statusCode: 500, body: {} },
).as('failedAssemblyCreation')
cy.get('.uppy-StatusBar-actionBtn--upload').click()
cy.wait('@failedAssemblyCreation')
cy.get('button[data-cy=retry]').click()
cy.wait(['@assemblies', '@resumable'])
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Complete')
})
it('should complete when resuming after pause', () => {
cy.intercept({ path: '/assemblies', method: 'POST' }).as('createAssemblies')
cy.intercept({ path: '/resumable/files/', method: 'POST' }).as(
'firstUpload',
)
cy.intercept({ path: '/resumable/files/*', method: 'PATCH' }).as(
'secondUpload',
)
cy.get('@file-input').selectFile(
[
'cypress/fixtures/images/cat.jpg',
'cypress/fixtures/images/traffic.jpg',
],
{ force: true },
)
cy.get('.uppy-StatusBar-actionBtn--upload').click()
cy.wait('@createAssemblies')
// wait for the upload to start, then pause
cy.wait('@firstUpload')
cy.get('button[data-cy=togglePauseResume]').click()
cy.wait(300) // Wait an arbitrary amount of time as a user would do.
cy.get('button[data-cy=togglePauseResume]').click()
cy.wait('@secondUpload')
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Complete')
})
})

View file

@ -1,52 +0,0 @@
import {
runRemoteUnsplashUploadTest,
runRemoteUrlImageUploadTest,
} from './reusable-tests.ts'
// NOTE: we have to use different files to upload per test
// because we are uploading to https://tusd.tusdemo.net,
// constantly uploading the same images gives a different cached result (or something).
describe('Dashboard with Tus', () => {
beforeEach(() => {
cy.visit('/dashboard-tus')
cy.get('.uppy-Dashboard-input:first').as('file-input')
cy.intercept('/files/*').as('tus')
cy.intercept({ method: 'POST', pathname: '/files' }).as('post')
cy.intercept({ method: 'PATCH', pathname: '/files/*' }).as('patch')
})
it('should upload cat image successfully', () => {
cy.get('@file-input').selectFile('cypress/fixtures/images/cat.jpg', {
force: true,
})
cy.get('.uppy-StatusBar-actionBtn--upload').click()
cy.wait(['@post', '@patch']).then(() => {
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Complete')
})
})
it('should start exponential backoff when receiving HTTP 429', () => {
cy.get('@file-input').selectFile('cypress/fixtures/images/baboon.png', {
force: true,
})
cy.intercept(
{ method: 'PATCH', pathname: '/files/*', times: 2 },
{ statusCode: 429, body: {} },
).as('patch')
cy.get('.uppy-StatusBar-actionBtn--upload').click()
cy.wait('@tus').then(() => {
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Complete')
})
})
it('should upload remote image with URL plugin', () => {
runRemoteUrlImageUploadTest()
})
it('should upload remote image with Unsplash plugin', () => {
runRemoteUnsplashUploadTest()
})
})

View file

@ -1,56 +0,0 @@
describe('dashboard-ui', () => {
beforeEach(() => {
cy.visit('/dashboard-ui')
cy.get('.uppy-Dashboard-input:first').as('file-input')
cy.get('.uppy-Dashboard-AddFiles').as('drop-target')
})
it('should not throw when calling uppy.destroy()', () => {
cy.get('@file-input').selectFile(
[
'cypress/fixtures/images/cat.jpg',
'cypress/fixtures/images/traffic.jpg',
],
{ force: true },
)
cy.window().then(({ uppy }) => {
expect(uppy.destroy()).to.not.throw
})
})
it('should render thumbnails', () => {
cy.get('@file-input').selectFile(
[
'cypress/fixtures/images/cat.jpg',
'cypress/fixtures/images/traffic.jpg',
],
{ force: true },
)
cy.get('.uppy-Dashboard-Item-previewImg')
.should('have.length', 2)
.each((element) => expect(element).attr('src').to.include('blob:'))
})
it('should support drag&drop', () => {
cy.get('@drop-target').selectFile(
[
'cypress/fixtures/images/cat.jpg',
'cypress/fixtures/images/cat-symbolic-link',
'cypress/fixtures/images/cat-symbolic-link.jpg',
'cypress/fixtures/images/traffic.jpg',
],
{ action: 'drag-drop' },
)
cy.get('.uppy-Dashboard-Item').should('have.length', 4)
cy.get('.uppy-Dashboard-Item-previewImg')
.should('have.length', 3)
.each((element) => expect(element).attr('src').to.include('blob:'))
cy.window().then(({ uppy }) => {
expect(
JSON.stringify(uppy.getFiles().map((file) => file.meta.relativePath)),
).to.be.equal('[null,null,null,null]')
})
})
})

View file

@ -1,49 +0,0 @@
describe.skip('@uppy/vue', () => {
beforeEach(() => {
cy.visit('/dashboard-vue')
cy.get('input[type="file"]').first().as('file-input')
})
it('should render headless components in Vue 3 correctly', () => {
cy.get('@file-input').selectFile(
[
'cypress/fixtures/images/cat.jpg',
'cypress/fixtures/images/traffic.jpg',
],
{ force: true },
)
// Test FilesList shows files correctly
cy.get('ul[data-uppy-element="files-list"]').should('exist')
cy.get('ul[data-uppy-element="files-list"] li').should('have.length', 2)
// Test FilesGrid shows files correctly
cy.get('div[data-uppy-element="files-grid"]').should('exist')
cy.get('div[data-uppy-element="files-grid"] div.uppy-reset').should(
'have.length',
2,
)
// Test UploadButton is functional
cy.get('#files-grid button[data-uppy-element="upload-button"]')
.should('exist')
.and('contain', 'Upload')
.and('not.be.disabled')
.click()
// Check if button shows progress during upload
cy.get('#files-grid button[data-uppy-element="upload-button"] span').should(
'contain',
'Uploaded',
)
// Check if cancel button appears during upload
cy.get('#files-grid button[data-uppy-element="cancel-button"]')
.should('exist')
.and('contain', 'Cancel')
cy.get('#files-grid button[data-uppy-element="upload-button"] span').should(
'contain',
'Complete',
)
})
})

View file

@ -1,75 +0,0 @@
import {
interceptCompanionUrlMetaRequest,
interceptCompanionUrlRequest,
runRemoteUnsplashUploadTest,
runRemoteUrlImageUploadTest,
} from './reusable-tests.ts'
describe('Dashboard with XHR', () => {
beforeEach(() => {
cy.visit('/dashboard-xhr')
})
it('should upload remote image with URL plugin', () => {
runRemoteUrlImageUploadTest()
})
it('should return correct file name with URL plugin from remote image with Content-Disposition', () => {
const fileName = `DALL·E IMG_9078 - 学中文 🤑`
cy.get('[data-cy="Url"]').click()
cy.get('.uppy-Url-input').type(
'http://localhost:4678/file-with-content-disposition',
)
interceptCompanionUrlMetaRequest()
cy.get('.uppy-Url-importButton').click()
cy.wait('@url-meta').then(() => {
cy.get('.uppy-Dashboard-Item-name').should('contain', fileName)
cy.get('.uppy-Dashboard-Item-status').should('contain', '84 KB')
})
})
it('should return correct file name with URL plugin from remote image without Content-Disposition', () => {
cy.get('[data-cy="Url"]').click()
cy.get('.uppy-Url-input').type('http://localhost:4678/file-no-headers')
interceptCompanionUrlMetaRequest()
cy.get('.uppy-Url-importButton').click()
cy.wait('@url-meta').then(() => {
cy.get('.uppy-Dashboard-Item-name').should('contain', 'file-no')
cy.get('.uppy-Dashboard-Item-status').should('contain', '0')
})
})
it('should return correct file name even when Companion doesnt supply it', () => {
cy.intercept('POST', 'http://localhost:3020/url/meta', {
statusCode: 200,
headers: {},
body: JSON.stringify({ size: 123, type: 'image/jpeg' }),
}).as('url')
cy.get('[data-cy="Url"]').click()
cy.get('.uppy-Url-input').type(
'http://localhost:4678/file-with-content-disposition',
)
interceptCompanionUrlMetaRequest()
cy.get('.uppy-Url-importButton').click()
cy.wait('@url-meta').then(() => {
cy.get('.uppy-Dashboard-Item-name').should('contain', 'file-with')
cy.get('.uppy-Dashboard-Item-status').should('contain', '123 B')
})
})
it('should upload unknown size files', () => {
cy.get('[data-cy="Url"]').click()
cy.get('.uppy-Url-input').type('http://localhost:4678/unknown-size')
cy.get('.uppy-Url-importButton').click()
interceptCompanionUrlRequest()
cy.get('.uppy-StatusBar-actionBtn--upload').click()
cy.wait('@url').then(() => {
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Complete')
})
})
it('should upload remote image with Unsplash plugin', () => {
runRemoteUnsplashUploadTest()
})
})

View file

@ -1,67 +0,0 @@
describe('@uppy/react', () => {
beforeEach(() => {
cy.visit('/react')
cy.get('#dashboard .uppy-Dashboard-input:first').as('dashboard-input')
cy.get('#modal .uppy-Dashboard-input:first').as('modal-input')
cy.get('#drag-drop .uppy-DragDrop-input').as('dragdrop-input')
})
it('should render Dashboard in React and show thumbnails', () => {
cy.get('@dashboard-input').selectFile(
[
'cypress/fixtures/images/cat.jpg',
'cypress/fixtures/images/traffic.jpg',
],
{ force: true },
)
cy.get('#dashboard .uppy-Dashboard-Item-previewImg')
.should('have.length', 2)
.each((element) => expect(element).attr('src').to.include('blob:'))
})
it('should render Dashboard with Remote Sources plugin pack', () => {
const sources = [
'My Device',
'Google Drive',
'OneDrive',
'Unsplash',
'Zoom',
'Link',
]
cy.get('#dashboard .uppy-DashboardTab-name').each((item, index, list) => {
expect(list).to.have.length(6)
// Returns the current element from the loop
expect(Cypress.$(item).text()).to.eq(sources[index])
})
})
it('should render Modal in React and show thumbnails', () => {
cy.get('#open').click()
cy.get('@modal-input').selectFile(
[
'cypress/fixtures/images/cat.jpg',
'cypress/fixtures/images/traffic.jpg',
],
{ force: true },
)
cy.get('#modal .uppy-Dashboard-Item-previewImg')
.should('have.length', 2)
.each((element) => expect(element).attr('src').to.include('blob:'))
})
it('should render Drag & Drop in React and create a thumbail with @uppy/thumbnail-generator', () => {
const spy = cy.spy()
// @ts-ignore fix me
cy.window().then(({ uppy }) => uppy.on('thumbnail:generated', spy))
cy.get('@dragdrop-input').selectFile(
[
'cypress/fixtures/images/cat.jpg',
'cypress/fixtures/images/traffic.jpg',
],
{ force: true },
)
// not sure how I can accurately wait for the thumbnail
cy.wait(1000).then(() => expect(spy).to.be.called)
})
})

View file

@ -1,54 +0,0 @@
/* global cy */
export const interceptCompanionUrlRequest = () =>
cy
.intercept({ method: 'POST', url: 'http://localhost:3020/url/get' })
.as('url')
export const interceptCompanionUrlMetaRequest = () =>
cy
.intercept({ method: 'POST', url: 'http://localhost:3020/url/meta' })
.as('url-meta')
export function runRemoteUrlImageUploadTest() {
cy.get('[data-cy="Url"]').click()
cy.get('.uppy-Url-input').type(
'https://raw.githubusercontent.com/transloadit/uppy/main/e2e/cypress/fixtures/images/cat.jpg',
)
cy.get('.uppy-Url-importButton').click()
interceptCompanionUrlRequest()
cy.get('.uppy-StatusBar-actionBtn--upload').click()
cy.wait('@url').then(() => {
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Complete')
})
}
export function runRemoteUnsplashUploadTest() {
cy.get('[data-cy="Unsplash"]').click()
cy.get('.uppy-SearchProvider-input').type('book')
cy.intercept({
method: 'GET',
url: 'http://localhost:3020/search/unsplash/list?q=book',
}).as('unsplash-list')
cy.get('.uppy-SearchProvider-searchButton').click()
cy.wait('@unsplash-list')
// Test that the author link is visible
cy.get('.uppy-ProviderBrowserItem')
.first()
.within(() => {
cy.root().click()
// We have hover states that show the author
// but we don't have hover in e2e, so we focus after the click
// to get the same effect. Also tests keyboard users this way.
cy.get('input[type="checkbox"]').focus()
cy.get('a').should('have.css', 'display', 'block')
})
cy.get('.uppy-c-btn-primary').click()
cy.intercept({
method: 'POST',
url: 'http://localhost:3020/search/unsplash/get/*',
}).as('unsplash-get')
cy.get('.uppy-StatusBar-actionBtn--upload').click()
cy.wait('@unsplash-get').then(() => {
cy.get('.uppy-StatusBar-statusPrimary').should('contain', 'Complete')
})
}

View file

@ -1,30 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
import { createFakeFile } from './createFakeFile.ts'
Cypress.Commands.add('createFakeFile', createFakeFile)

View file

@ -1,56 +0,0 @@
declare global {
namespace Cypress {
interface Chainable {
createFakeFile: typeof createFakeFile
}
}
}
interface File {
source: string
name: string
type: string
data: Blob
}
export function createFakeFile(
name?: string,
type?: string,
b64?: string,
): File {
if (!b64) {
b64 =
'PHN2ZyB2aWV3Qm94PSIwIDAgMTIwIDEyMCI+CiAgPGNpcmNsZSBjeD0iNjAiIGN5PSI2MCIgcj0iNTAiLz4KPC9zdmc+Cg=='
}
if (!type) type = 'image/svg+xml'
// https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript
function base64toBlob(base64Data: string, contentType = '') {
const sliceSize = 1024
const byteCharacters = atob(base64Data)
const bytesLength = byteCharacters.length
const slicesCount = Math.ceil(bytesLength / sliceSize)
const byteArrays = new Array(slicesCount)
for (let sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
const begin = sliceIndex * sliceSize
const end = Math.min(begin + sliceSize, bytesLength)
const bytes = new Array(end - begin)
for (let offset = begin, i = 0; offset < end; ++i, ++offset) {
bytes[i] = byteCharacters[offset].charCodeAt(0)
}
byteArrays[sliceIndex] = new Uint8Array(bytes)
}
return new Blob(byteArrays, { type: contentType })
}
const blob = base64toBlob(b64, type)
return {
source: 'test',
name: name || 'test-file',
type: blob.type,
data: blob,
}
}

View file

@ -1,25 +0,0 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands.ts'
// Alternatively you can use CommonJS syntax:
// require('./commands')
// @ts-ignore
import installLogsCollector from 'cypress-terminal-report/src/installLogsCollector.js'
installLogsCollector()

View file

@ -1,24 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
import './commands.ts'
import type { Uppy } from '@uppy/core'
declare global {
interface Window {
uppy: Uppy<any, any>
}
}

View file

@ -1,113 +0,0 @@
#!/usr/bin/env node
import fs from 'node:fs/promises'
import prompts from 'prompts'
/**
* Utility function that strips indentation from multi-line strings.
* Inspired from https://github.com/dmnd/dedent.
*/
function dedent(strings, ...parts) {
const nonSpacingChar = /\S/m.exec(strings[0])
if (nonSpacingChar == null) return ''
const indent =
nonSpacingChar.index -
strings[0].lastIndexOf('\n', nonSpacingChar.index) -
1
const dedentEachLine = (str) =>
str
.split('\n')
.map((line, i) => line.slice(i && indent))
.join('\n')
let returnLines = dedentEachLine(
strings[0].slice(nonSpacingChar.index),
indent,
)
for (let i = 1; i < strings.length; i++) {
returnLines += String(parts[i - 1]) + dedentEachLine(strings[i], indent)
}
return returnLines
}
const packageNames = await fs.readdir(
new URL('../packages/@uppy', import.meta.url),
)
const unwantedPackages = ['core', 'companion', 'redux-dev-tools', 'utils']
const { name } = await prompts({
type: 'text',
name: 'name',
message: 'What should the name of the test be (e.g `dashboard-tus`)?',
validate: (value) => /^[a-z|-]+$/i.test(value),
})
const { packages } = await prompts({
type: 'multiselect',
name: 'packages',
message: 'What packages do you want to test?',
hint: '@uppy/core is automatically included',
choices: packageNames
.filter((pkg) => !unwantedPackages.includes(pkg))
.map((pkg) => ({ title: pkg, value: pkg })),
})
const camelcase = (str) =>
str
.toLowerCase()
.replace(/([-][a-z])/g, (group) => group.toUpperCase().replace('-', ''))
const testUrl = new URL(`cypress/integration/${name}.spec.ts`, import.meta.url)
const test = dedent`
describe('${name}', () => {
beforeEach(() => {
cy.visit('/${name}')
})
})
`
const htmlUrl = new URL(`clients/${name}/index.html`, import.meta.url)
const html = dedent`
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>${name}</title>
<script defer type="module" src="app.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
`
const appUrl = new URL(`clients/${name}/app.js`, import.meta.url)
// dedent is acting weird for this one but this formatting fixes it.
const app = dedent`
import Uppy from '@uppy/core'
${packages.map((pgk) => `import ${camelcase(pgk)} from '@uppy/${pgk}'`).join('\n')}
const uppy = new Uppy()
${packages.map((pkg) => `.use(${camelcase(pkg)})`).join('\n\t')}
// Keep this here to access uppy in tests
window.uppy = uppy
`
await fs.writeFile(testUrl, test)
await fs.mkdir(new URL(`clients/${name}`, import.meta.url))
await fs.writeFile(htmlUrl, html)
await fs.writeFile(appUrl, app)
const homeUrl = new URL('clients/index.html', import.meta.url)
const home = await fs.readFile(homeUrl, 'utf8')
const newHome = home.replace(
'</ul>',
` <li><a href="${name}/index.html">${name}</a></li>\n </ul>`,
)
await fs.writeFile(homeUrl, newHome)
const prettyPath = (url) => url.toString().split('uppy', 2)[1]
console.log(`✅ Generated ${prettyPath(testUrl)}`)
console.log(`✅ Generated ${prettyPath(htmlUrl)}`)
console.log(`✅ Generated ${prettyPath(appUrl)}`)
console.log(`✅ Updated ${prettyPath(homeUrl)}`)

View file

@ -1,64 +0,0 @@
import http from 'node:http'
const requestListener = (req, res) => {
const endpoint = req.url
switch (endpoint) {
case '/file-with-content-disposition': {
const fileName = `DALL·E IMG_9078 - 学中文 🤑`
res.setHeader(
'Content-Disposition',
`attachment; filename="ASCII-name.zip"; filename*=UTF-8''${encodeURIComponent(fileName)}`,
)
res.setHeader('Content-Type', 'image/jpeg')
res.setHeader('Content-Length', '86500')
break
}
case '/file-no-headers':
break
case '/unknown-size':
{
res.setHeader('Content-Type', 'text/html; charset=UTF-8')
res.setHeader('Transfer-Encoding', 'chunked')
const chunkSize = 1e5
if (req.method === 'GET') {
let i = 0
const interval = setInterval(() => {
if (i >= 10) {
// 1MB
clearInterval(interval)
res.end()
return
}
res.write(
Buffer.from(
Array.from({ length: chunkSize }, () => '1').join(''),
),
)
res.write('\n')
i++
}, 10)
} else if (req.method === 'HEAD') {
res.end()
} else {
throw new Error('Unhandled method')
}
}
break
default:
res.writeHead(404).end('Unhandled request')
}
res.end()
}
export default function startMockServer(host, port) {
const server = http.createServer(requestListener)
server.listen(port, host, () => {
console.log(`Mock server is running on http://${host}:${port}`)
})
}
// startMockServer('localhost', 4678)

View file

@ -1,63 +0,0 @@
{
"name": "e2e",
"private": true,
"author": "Merlijn Vos <merlijn@soverin.net>",
"description": "End-to-end test suite for Uppy",
"scripts": {
"client:start": "parcel --no-autoinstall clients/index.html",
"cypress:open": "cypress open",
"cypress:headless": "cypress run",
"generate-test": "yarn node generate-test.mjs"
},
"dependencies": {
"@uppy/audio": "workspace:^",
"@uppy/aws-s3": "workspace:^",
"@uppy/aws-s3-multipart": "workspace:^",
"@uppy/box": "workspace:^",
"@uppy/companion-client": "workspace:^",
"@uppy/core": "workspace:^",
"@uppy/dashboard": "workspace:^",
"@uppy/drag-drop": "workspace:^",
"@uppy/drop-target": "workspace:^",
"@uppy/dropbox": "workspace:^",
"@uppy/facebook": "workspace:^",
"@uppy/file-input": "workspace:^",
"@uppy/form": "workspace:^",
"@uppy/golden-retriever": "workspace:^",
"@uppy/google-drive": "workspace:^",
"@uppy/google-drive-picker": "workspace:^",
"@uppy/google-photos-picker": "workspace:^",
"@uppy/image-editor": "workspace:^",
"@uppy/informer": "workspace:^",
"@uppy/instagram": "workspace:^",
"@uppy/onedrive": "workspace:^",
"@uppy/progress-bar": "workspace:^",
"@uppy/provider-views": "workspace:^",
"@uppy/screen-capture": "workspace:^",
"@uppy/status-bar": "workspace:^",
"@uppy/store-default": "workspace:^",
"@uppy/store-redux": "workspace:^",
"@uppy/thumbnail-generator": "workspace:^",
"@uppy/transloadit": "workspace:^",
"@uppy/tus": "workspace:^",
"@uppy/unsplash": "workspace:^",
"@uppy/url": "workspace:^",
"@uppy/webcam": "workspace:^",
"@uppy/xhr-upload": "workspace:^",
"@uppy/zoom": "workspace:^"
},
"devDependencies": {
"@parcel/transformer-vue": "^2.9.3",
"cypress": "^13.0.0",
"cypress-terminal-report": "^6.0.0",
"deep-freeze": "^0.0.1",
"execa": "^9.6.0",
"parcel": "^2.9.3",
"process": "^0.11.10",
"prompts": "^2.4.2",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"typescript": "~5.4",
"vue": "^3.2.33"
}
}

View file

@ -1,11 +0,0 @@
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noEmit": true,
"target": "es2020",
"lib": ["es2020", "dom"],
"types": ["cypress"]
},
"include": ["cypress/**/*.ts"]
}

View file

@ -14,7 +14,6 @@
"scripts": {
"client": "light-server -p 3000 -s client",
"server": "node ./server/index.js",
"start": "yarn run server & yarn run client",
"test": "echo \"Error: no test specified\" && exit 1"
"start": "yarn run server & yarn run client"
}
}

View file

@ -4,6 +4,7 @@
"type": "module",
"scripts": {
"build": "tsc && vite build",
"test": "vitest run --browser.headless",
"dev": "vite",
"serve": "vite preview"
},
@ -22,8 +23,12 @@
"devDependencies": {
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-react": "^4.6.0",
"@vitest/browser": "^3.2.4",
"playwright": "^1.53.2",
"typescript": "^5.7.3",
"vite": "^6.2.0"
"vite": "^7.0.3",
"vitest": "^3.2.4",
"vitest-browser-react": "^1.0.0"
}
}

View file

@ -11,11 +11,11 @@ import UppyRemoteSources from '@uppy/remote-sources'
import UppyScreenCapture from '@uppy/screen-capture'
import Tus from '@uppy/tus'
import UppyWebcam from '@uppy/webcam'
import React, { useRef, useState } from 'react'
import CustomDropzone from './CustomDropzone.tsx'
import { RemoteSource } from './RemoteSource.js'
import ScreenCapture from './ScreenCapture.tsx'
import Webcam from './Webcam.tsx'
import { useRef, useState } from 'react'
import CustomDropzone from './CustomDropzone'
import { RemoteSource } from './RemoteSource'
import ScreenCapture from './ScreenCapture'
import Webcam from './Webcam'
import './app.css'
import '@uppy/react/dist/styles.css'

View file

@ -0,0 +1,124 @@
import { userEvent } from '@vitest/browser/context'
import { describe, expect, test } from 'vitest'
import { render } from 'vitest-browser-react'
import App from '../src/App'
const createMockFile = (name: string, type: string, size: number = 1024) => {
return new File(['test content'], name, { type })
}
describe('App', () => {
test('renders all main sections and upload button is initially disabled', async () => {
const screen = render(<App />)
await expect.element(screen.getByText('With list')).toBeInTheDocument()
await expect.element(screen.getByText('With grid')).toBeInTheDocument()
await expect
.element(screen.getByText('With custom dropzone'))
.toBeInTheDocument()
const uploadButton = screen.getByRole('button', { name: /upload/i })
await expect.element(uploadButton).toBeInTheDocument()
await expect.element(uploadButton).toBeDisabled()
})
test('can add and remove files and upload', async () => {
const screen = render(<App />)
const fileInput = document.getElementById(
'uppy-dropzone-file-input',
) as Element
await userEvent.upload(fileInput, createMockFile('test.txt', 'text/plain'))
// for list and grid
for (const element of screen.getByText('test.txt').elements()) {
await expect.element(element).toBeInTheDocument()
}
await screen.getByText('remove').first().click()
for (const element of screen.getByText('test.txt').elements()) {
await expect.element(element).not.toBeInTheDocument()
}
await userEvent.upload(fileInput, createMockFile('test.txt', 'text/plain'))
await screen.getByRole('button', { name: /upload/i }).click()
await expect
.element(screen.getByRole('button', { name: /complete/i }))
.toBeInTheDocument()
})
})
describe('ScreenCapture Component', () => {
test('renders with title, control buttons, and close functionality works', async () => {
const screen = render(<App />)
await screen.getByRole('button', { name: 'Screen Capture' }).click()
await expect
.element(screen.getByRole('heading', { name: 'Screen Capture' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Screenshot' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Record' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Stop' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Submit' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Discard' }))
.toBeInTheDocument()
const closeButton = screen.getByText('✕')
await closeButton.click()
})
})
describe('Webcam Component', () => {
test('renders with title, control buttons, and close functionality works', async () => {
const screen = render(<App />)
await screen.getByRole('button', { name: 'Webcam' }).click()
await expect
.element(screen.getByRole('heading', { name: 'Camera' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Snapshot' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Record' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Stop' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Submit' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Discard' }))
.toBeInTheDocument()
const closeButton = screen.getByText('✕')
await closeButton.click()
})
})
describe('RemoteSource Component', () => {
test('renders login button and login interaction works', async () => {
const screen = render(<App />)
await screen.getByRole('button', { name: 'Dropbox' }).click()
const loginButton = screen.getByRole('button', { name: 'Login' })
await expect.element(loginButton).toBeInTheDocument()
await loginButton.click()
await expect.element(loginButton).toBeInTheDocument()
})
})

View file

@ -0,0 +1,14 @@
import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [react(), tailwindcss()],
test: {
browser: {
enabled: true,
provider: 'playwright',
instances: [{ browser: 'chromium' }],
},
},
})

View file

@ -6,6 +6,7 @@
"scripts": {
"dev": "vite dev",
"build": "vite build",
"test": "vitest run --browser.headless",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@ -26,10 +27,13 @@
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/vite": "^4.0.0",
"@vitest/browser": "^3.2.4",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^6.2.5"
"vite": "^6.2.5",
"vitest": "^3.2.4",
"vitest-browser-svelte": "^1.0.0"
}
}

View file

@ -0,0 +1,126 @@
import { userEvent } from '@vitest/browser/context'
import { describe, expect, test } from 'vitest'
import { render } from 'vitest-browser-svelte'
import App from '../src/routes/+page.svelte'
const createMockFile = (name: string, type: string, size: number = 1024) => {
return new File(['test content'], name, { type })
}
describe('App', () => {
test('renders all main sections and upload button is initially disabled', async () => {
const screen = render(App)
await expect.element(screen.getByText('With list')).toBeInTheDocument()
await expect.element(screen.getByText('With grid')).toBeInTheDocument()
await expect
.element(screen.getByText('With custom dropzone'))
.toBeInTheDocument()
const uploadButton = screen.getByRole('button', { name: /upload/i })
await expect.element(uploadButton).toBeInTheDocument()
await expect.element(uploadButton).toBeDisabled()
})
test('can add and remove files and upload', async () => {
const screen = render(App)
const fileInput = document.getElementById(
'uppy-dropzone-file-input',
) as Element
await userEvent.upload(fileInput, createMockFile('test.txt', 'text/plain'))
// for list and grid
for (const element of screen.getByText('test.txt').elements()) {
await expect.element(element).toBeInTheDocument()
}
await screen.getByText('remove').first().click()
for (const element of screen.getByText('test.txt').elements()) {
await expect.element(element).not.toBeInTheDocument()
}
await userEvent.upload(fileInput, createMockFile('test.txt', 'text/plain'))
await screen.getByRole('button', { name: /upload/i }).click()
await expect
.element(screen.getByRole('button', { name: /complete/i }))
.toBeInTheDocument()
})
})
describe('ScreenCapture Component', () => {
test('renders with title, control buttons, and close functionality works', async () => {
const screen = render(App)
await screen
.getByRole('button', { name: 'Screen Capture', exact: true })
.click()
await expect
.element(screen.getByRole('heading', { name: 'Screen Capture' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Screenshot' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Record' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Stop' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Submit' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Discard' }))
.toBeInTheDocument()
const closeButton = screen.getByText('✕')
await closeButton.click()
})
})
describe('Webcam Component', () => {
test('renders with title, control buttons, and close functionality works', async () => {
const screen = render(App)
await screen.getByRole('button', { name: 'Webcam', exact: true }).click()
await expect
.element(screen.getByRole('heading', { name: 'Camera' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Snapshot' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Record' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Stop' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Submit' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Discard' }))
.toBeInTheDocument()
const closeButton = screen.getByText('✕')
await closeButton.click()
})
})
describe('RemoteSource Component', () => {
test('renders login button and login interaction works', async () => {
const screen = render(App)
await screen.getByRole('button', { name: 'Dropbox', exact: true }).click()
const loginButton = screen.getByRole('button', { name: 'Login' })
await expect.element(loginButton).toBeInTheDocument()
await loginButton.click()
await expect.element(loginButton).toBeInTheDocument()
})
})

View file

@ -0,0 +1,14 @@
import { sveltekit } from '@sveltejs/kit/vite'
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
test: {
browser: {
enabled: true,
provider: 'playwright',
instances: [{ browser: 'chromium' }],
},
},
})

View file

@ -6,6 +6,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest run --browser.headless",
"preview": "vite preview --port 5050"
},
"dependencies": {
@ -23,7 +24,10 @@
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@vitejs/plugin-vue": "^5.0.0",
"@vitest/browser": "^3.2.4",
"tailwindcss": "^4.0.0",
"vite": "^6.2.0"
"vite": "^6.2.0",
"vitest": "^3.2.4",
"vitest-browser-vue": "^1.0.0"
}
}

View file

@ -0,0 +1,126 @@
import { userEvent } from '@vitest/browser/context'
import { describe, expect, test } from 'vitest'
import { render } from 'vitest-browser-vue'
import App from '../src/App.vue'
const createMockFile = (name: string, type: string, size: number = 1024) => {
return new File(['test content'], name, { type })
}
describe('App', () => {
test('renders all main sections and upload button is initially disabled', async () => {
const screen = render(App)
await expect.element(screen.getByText('With list')).toBeInTheDocument()
await expect.element(screen.getByText('With grid')).toBeInTheDocument()
await expect
.element(screen.getByText('With custom dropzone'))
.toBeInTheDocument()
const uploadButton = screen.getByRole('button', { name: /upload/i })
await expect.element(uploadButton).toBeInTheDocument()
await expect.element(uploadButton).toBeDisabled()
})
test('can add and remove files and upload', async () => {
const screen = render(App)
const fileInput = document.getElementById(
'uppy-dropzone-file-input',
) as Element
await userEvent.upload(fileInput, createMockFile('test.txt', 'text/plain'))
// for list and grid
for (const element of screen.getByText('test.txt').elements()) {
await expect.element(element).toBeInTheDocument()
}
await screen.getByText('remove').first().click()
for (const element of screen.getByText('test.txt').elements()) {
await expect.element(element).not.toBeInTheDocument()
}
await userEvent.upload(fileInput, createMockFile('test.txt', 'text/plain'))
await screen.getByRole('button', { name: /upload/i }).click()
await expect
.element(screen.getByRole('button', { name: /complete/i }))
.toBeInTheDocument()
})
})
describe('ScreenCapture Component', () => {
test('renders with title, control buttons, and close functionality works', async () => {
const screen = render(App)
await screen
.getByRole('button', { name: 'Screen Capture', exact: true })
.click()
await expect
.element(screen.getByRole('heading', { name: 'Screen Capture' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Screenshot' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Record' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Stop' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Submit' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Discard' }))
.toBeInTheDocument()
const closeButton = screen.getByText('✕')
await closeButton.click()
})
})
describe('Webcam Component', () => {
test('renders with title, control buttons, and close functionality works', async () => {
const screen = render(App)
await screen.getByRole('button', { name: 'Webcam', exact: true }).click()
await expect
.element(screen.getByRole('heading', { name: 'Camera' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Snapshot' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Record' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Stop' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Submit' }))
.toBeInTheDocument()
await expect
.element(screen.getByRole('button', { name: 'Discard' }))
.toBeInTheDocument()
const closeButton = screen.getByText('✕')
await closeButton.click()
})
})
describe('RemoteSource Component', () => {
test('renders login button and login interaction works', async () => {
const screen = render(App)
await screen.getByRole('button', { name: 'Dropbox', exact: true }).click()
const loginButton = screen.getByRole('button', { name: 'Login' })
await expect.element(loginButton).toBeInTheDocument()
await loginButton.click()
await expect.element(loginButton).toBeInTheDocument()
})
})

View file

@ -0,0 +1,14 @@
import tailwindcss from '@tailwindcss/vite'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [vue(), tailwindcss()],
test: {
browser: {
enabled: true,
provider: 'playwright',
instances: [{ browser: 'chromium' }],
},
},
})

View file

@ -9,9 +9,7 @@
"packages/@uppy/*",
"packages/@uppy/angular/projects/uppy/*",
"packages/uppy",
"private/*",
"test/endtoend",
"e2e"
"private/*"
],
"scripts": {
"build": "turbo run build build:css --filter='./packages/@uppy/*' --filter='./packages/uppy'",
@ -22,39 +20,20 @@
"check:ci": "yarn exec biome ci",
"dev": "yarn workspace @uppy-dev/dev dev",
"dev:with-companion": "npm-run-all --parallel start:companion dev",
"e2e:ci": "start-server-and-test 'npm-run-all --parallel e2e:client start:companion:with-loadbalancer' '1234|3020' e2e:headless",
"e2e:client": "yarn workspace e2e client:start",
"e2e:cypress": "yarn workspace e2e cypress:open",
"e2e:generate": "yarn workspace e2e generate-test",
"e2e:headless": "yarn workspace e2e cypress:headless",
"e2e": "npm-run-all --parallel build:watch e2e:client start:companion:with-loadbalancer e2e:cypress",
"size": "echo 'JS Bundle mingz:' && cat ./packages/uppy/dist/uppy.min.js | gzip | wc -c && echo 'CSS Bundle mingz:' && cat ./packages/uppy/dist/uppy.min.css | gzip | wc -c",
"start:companion": "yarn workspace @uppy/companion start:dev",
"start:companion:with-loadbalancer": "e2e/start-companion-with-load-balancer.mjs",
"test": "turbo run test --filter='./packages/@uppy/*' --filter='./packages/uppy'",
"test": "turbo run test --filter='./packages/@uppy/*' --filter='./packages/uppy' --filter='./examples/{react,vue,sveltekit}'",
"test:watch": "turbo watch test --filter='./packages/@uppy/*' --filter='./packages/uppy'",
"typecheck": "turbo run typecheck --filter='./packages/@uppy/*' --filter='./packages/uppy'",
"uploadcdn": "yarn workspace uppy exec -- node upload-to-cdn.js",
"version": "changeset version && corepack yarn install --mode=update-lockfile",
"release": "changeset publish"
},
"resolutions": {
"@types/react": "^18",
"@types/webpack-dev-server": "^4",
"p-queue": "patch:p-queue@npm%3A8.0.1#~/.yarn/patches/p-queue-npm-8.0.1-fe1ddcd827.patch",
"resize-observer-polyfill": "patch:resize-observer-polyfill@npm%3A1.5.1#./.yarn/patches/resize-observer-polyfill-npm-1.5.1-603120e8a0.patch",
"start-server-and-test": "patch:start-server-and-test@npm:1.14.0#.yarn/patches/start-server-and-test-npm-1.14.0-841aa34fdf.patch",
"uuid@^8.3.2": "patch:uuid@npm:8.3.2#.yarn/patches/uuid-npm-8.3.2-eca0baba53.patch"
},
"devDependencies": {
"@biomejs/biome": "2.0.5",
"@changesets/changelog-github": "^0.5.1",
"@changesets/cli": "^2.29.5",
"npm-run-all": "^4.1.5",
"start-server-and-test": "^1.14.0",
"turbo": "^2.5.4",
"typescript": "^5.8.3",
"vue-template-compiler": "workspace:*"
"typescript": "^5.8.3"
},
"packageManager": "yarn@4.4.1+sha224.fd21d9eb5fba020083811af1d4953acc21eeb9f6ff97efd1b3f9d4de",
"engines": {

View file

@ -441,6 +441,51 @@ describe('AwsS3Multipart', () => {
expect(uploadErrorMock.mock.calls.length).toEqual(1)
expect(uploadSuccessMock.mock.calls.length).toEqual(1) // This fails for me becuase upload returned early.
})
it('retries signPart when it fails', async () => {
// The retry logic for signPart happens in the uploadChunk method of HTTPCommunicationQueue
// For a 6MB file, we expect 2 parts, so signPart should be called for each part
let callCount = 0
const signPartWithRetry = vi.fn((file, { partNumber }) => {
callCount++
if (callCount === 1) {
// First call fails with a retryable error
throw { source: { status: 500 } }
}
return {
url: `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${partNumber}&uploadId=6aeb1980f3fc7ce0b5454d25b71992`,
}
})
const core = new Core().use(AwsS3Multipart, {
shouldUseMultipart: true,
retryDelays: [10],
createMultipartUpload: vi.fn(() => ({
uploadId: '6aeb1980f3fc7ce0b5454d25b71992',
key: 'test/upload/multitest.dat',
})),
completeMultipartUpload: vi.fn(async () => ({ location: 'test' })),
abortMultipartUpload: vi.fn(),
signPart: signPartWithRetry,
uploadPartBytes: vi.fn().mockResolvedValue({ status: 200 }),
listParts: undefined as any,
})
const fileSize = 5 * MB + 1 * MB
core.addFile({
source: 'vi',
name: 'multitest.dat',
type: 'application/octet-stream',
data: new File([new Uint8Array(fileSize)], '', {
type: 'application/octet-stream',
}),
})
await core.upload()
// Should be called 3 times: 1 failed + 1 retry + 1 for second part
expect(signPartWithRetry).toHaveBeenCalledTimes(3)
})
})
describe('dynamic companionHeader', () => {

View file

@ -76,6 +76,7 @@
"@types/cors": "2.8.6",
"@types/eslint": "^8.2.0",
"@types/express-session": "1.17.3",
"@types/http-proxy": "^1",
"@types/jsonwebtoken": "8.3.7",
"@types/lodash": "4.14.191",
"@types/morgan": "1.7.37",
@ -85,6 +86,8 @@
"@types/request": "2.48.8",
"@types/webpack": "^5.28.0",
"@types/ws": "8.5.3",
"execa": "^9.6.0",
"http-proxy": "^1.18.1",
"jest": "^29.0.0",
"nock": "^13.1.3",
"supertest": "6.2.4",

View file

@ -59,7 +59,7 @@ const startCompanion = ({ name, port }) =>
? ['--watch-path', 'packages/@uppy/companion/src', '--watch']
: []),
],
cwd: new URL('../', import.meta.url),
cwd: new URL('../../../../', import.meta.url),
stdio: 'inherit',
env: {
// Note: these env variables will override anything set in .env
@ -71,6 +71,8 @@ const startCompanion = ({ name, port }) =>
COMPANION_ENABLE_URL_ENDPOINT: 'true',
COMPANION_LOGGER_PROCESS_NAME: name,
COMPANION_CLIENT_ORIGINS: 'true',
COMPANION_DATADIR: '/tmp',
COMPANION_DOMAIN: 'localhost:3020',
},
})

View file

@ -25,8 +25,18 @@ export default function Dropzone(props: DropzoneProps) {
)
return (
<div className="uppy-reset" data-uppy-element="dropzone">
<input {...getInputProps()} className="uppy:hidden" />
<div
className="uppy-reset"
data-uppy-element="dropzone"
role="presentation"
tabIndex={0}
>
<input
{...getInputProps()}
tabIndex={-1}
name="uppy-dropzone-file-input"
className="uppy:hidden"
/>
<div
{...getRootProps()}
style={{

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

View file

@ -17,7 +17,7 @@ CompressorPlugin.prototype.compress = async (blob: File) => {
}
const sampleImage = fs.readFileSync(
path.join(__dirname, '../../../../e2e/cypress/fixtures/images/image.jpg'),
path.join(__dirname, '../fixtures/image.jpg'),
)
const file1 = {

View file

@ -39,7 +39,9 @@
"preact": "^10.5.13"
},
"devDependencies": {
"@types/deep-freeze": "^0",
"cssnano": "^7.0.7",
"deep-freeze": "^0.0.1",
"jsdom": "^26.1.0",
"postcss": "^8.5.6",
"postcss-cli": "^11.0.1",

View file

@ -5,8 +5,6 @@ import prettierBytes from '@transloadit/prettier-bytes'
import type { Body, Meta } from '@uppy/core'
import type { Locale } from '@uppy/utils/lib/Translator'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
// @ts-expect-error trying to import a file from outside the package
import DeepFrozenStore from '../../../../e2e/cypress/fixtures/DeepFrozenStore.mjs'
import BasePlugin, {
type DefinePluginOpts,
type PluginOpts,
@ -15,6 +13,8 @@ import Core from './index.js'
import { debugLogger } from './loggers.js'
import AcquirerPlugin1 from './mocks/acquirerPlugin1.js'
import AcquirerPlugin2 from './mocks/acquirerPlugin2.js'
// @ts-expect-error untyped
import DeepFrozenStore from './mocks/DeepFrozenStore.mjs'
import InvalidPlugin from './mocks/invalidPlugin.js'
import InvalidPluginWithoutId from './mocks/invalidPluginWithoutId.js'
import InvalidPluginWithoutType from './mocks/invalidPluginWithoutType.js'
@ -23,7 +23,7 @@ import UIPlugin from './UIPlugin.js'
import type { State } from './Uppy.js'
const sampleImage = fs.readFileSync(
path.join(__dirname, '../../../../e2e/cypress/fixtures/images/image.jpg'),
path.join(__dirname, '../../compressor/fixtures/image.jpg'),
)
// @ts-expect-error type object can be second argument

View file

@ -1,8 +1,5 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import deepFreeze from 'deep-freeze'
/* eslint-disable no-underscore-dangle */
/**
* Default store + deepFreeze on setState to make sure nothing is mutated accidentally
*/

View file

@ -10,7 +10,8 @@
"build": "tsc --build tsconfig.build.json",
"build:css": "sass --load-path=../../ src/style.scss dist/style.css && postcss dist/style.css -u cssnano -o dist/style.min.css",
"typecheck": "tsc --build",
"test": "vitest run --environment=jsdom --silent='passed-only'"
"test": "vitest run --silent='passed-only'",
"test:e2e": "vitest watch --project browser --browser.headless false"
},
"keywords": [
"file uploader",
@ -46,6 +47,7 @@
"@uppy/status-bar": "workspace:^",
"@uppy/url": "workspace:^",
"@uppy/webcam": "workspace:^",
"@vitest/browser": "^3.2.4",
"cssnano": "^7.0.7",
"jsdom": "^26.1.0",
"postcss": "^8.5.6",

View file

@ -0,0 +1,33 @@
import Uppy from '@uppy/core'
import { page, userEvent } from '@vitest/browser/context'
import { expect, test } from 'vitest'
import Dashboard from './Dashboard.js'
// Normally you would use one of vitest's framework renderers, such as vitest-browser-react,
// but that's overkill for us so we write our own plain HTML renderer.
function render(html: string) {
document.body.innerHTML = ''
const root = document.createElement('main')
root.innerHTML = html
document.body.appendChild(root)
return root
}
test('Basic Dashboard functionality works in the browser', async () => {
render('<div id="uppy"></div>')
new Uppy().use(Dashboard, {
target: '#uppy',
inline: true,
metaFields: [{ id: 'license', name: 'License' }],
})
await expect.element(page.getByText('Drop files here')).toBeVisible()
const fileInput = document.getElementsByClassName('uppy-Dashboard-input')[0]
await userEvent.upload(fileInput, new File(['Hello, World!'], 'test.txt'))
await expect.element(page.getByText('test.txt')).toBeVisible()
await page.getByTitle('Edit file test.txt').click()
const licenseInput = page.getByLabelText('License')
await expect.element(licenseInput).toBeVisible()
await userEvent.fill(licenseInput.element(), 'MIT')
await page.getByText('Save changes').click()
})

View file

@ -0,0 +1,30 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{
test: {
name: 'unit',
include: [
'src/**/*.test.{ts,tsx}',
'!src/**/*.browser.test.{ts,tsx}',
],
environment: 'jsdom',
},
},
{
test: {
name: 'browser',
include: ['src/**/*.browser.test.{ts,tsx}'],
browser: {
enabled: true,
headless: true,
provider: 'playwright',
instances: [{ browser: 'chromium' }],
},
},
},
],
},
})

View file

@ -44,6 +44,7 @@
"devDependencies": {
"@uppy/core": "workspace:^",
"jsdom": "^26.1.0",
"msw": "^2.10.4",
"typescript": "^5.8.3",
"vitest": "^3.2.4",
"whatwg-fetch": "^3.6.2"

View file

@ -1,10 +1,17 @@
import { once } from 'node:events'
import { createServer } from 'node:http'
import Core from '@uppy/core'
import { describe, expect, it } from 'vitest'
import { HttpResponse, http } from 'msw'
import { setupServer } from 'msw/node'
import { describe, expect, it, vi } from 'vitest'
import Transloadit from './index.ts'
import 'whatwg-fetch'
// Mock EventSource for testing
global.EventSource = vi.fn(() => ({
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
close: vi.fn(),
}))
describe('Transloadit', () => {
it('Does not leave lingering progress if getAssemblyOptions fails', () => {
const error = new Error('expected failure')
@ -69,56 +76,79 @@ describe('Transloadit', () => {
)
})
// For some reason this test doesn't pass on CI
it.skip('Can start an assembly with no files and no fields', async () => {
const server = createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Headers', '*')
res.setHeader('Content-Type', 'application/json')
res.end('{"websocket_url":"about:blank"}')
}).listen()
await once(server, 'listening')
const uppy = new Core({
autoProceed: false,
})
it('should complete when resuming after pause', async () => {
let assemblyCallCount = 0
let firstUploadCallCount = 0
let secondUploadCallCount = 0
const server = setupServer(
http.post('*/assemblies', ({ request }) => {
assemblyCallCount++
return HttpResponse.json({
assembly_id: 'test-assembly-id',
websocket_url: 'ws://localhost:8080',
tus_url: 'https://localhost/resumable/files/',
assembly_ssl_url: 'https://localhost/assemblies/test-assembly-id',
ok: 'ASSEMBLY_EXECUTING',
})
}),
http.get('*/assemblies/*', () => {
return HttpResponse.json({
assembly_id: 'test-assembly-id',
ok: 'ASSEMBLY_COMPLETED',
results: {},
})
}),
http.post('*/resumable/files/', () => {
firstUploadCallCount++
return HttpResponse.json({
tus_enabled: true,
resumable_file_id: `test-file-id-${firstUploadCallCount}`,
})
}),
http.patch('*/resumable/files/*', () => {
secondUploadCallCount++
return HttpResponse.json({
ok: 'RESUMABLE_FILE_UPLOADED',
})
}),
http.post('https://transloaditstatus.com/client_error', () => {
return HttpResponse.json({})
}),
)
server.listen({ onUnhandledRequest: 'error' })
const uppy = new Core()
const successSpy = vi.fn()
uppy.on('complete', successSpy)
uppy.use(Transloadit, {
service: `http://localhost:${server.address().port}`,
alwaysRunAssembly: true,
params: {
auth: { key: 'some auth key string' },
template_id: 'some template id string',
assemblyOptions: {
params: {
auth: { key: 'test-auth-key' },
template_id: 'test-template-id',
},
},
})
await uppy.upload()
server.closeAllConnections()
await new Promise((resolve) => server.close(resolve))
})
// For some reason this test doesn't pass on CI
it.skip('Can start an assembly with no files and some fields', async () => {
const server = createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Headers', '*')
res.setHeader('Content-Type', 'application/json')
res.end('{"websocket_url":"about:blank"}')
}).listen()
await once(server, 'listening')
const uppy = new Core({
autoProceed: false,
uppy.addFile({
source: 'test',
name: 'cat.jpg',
data: Buffer.from('test file content'),
})
uppy.use(Transloadit, {
service: `http://localhost:${server.address().port}`,
alwaysRunAssembly: true,
params: {
auth: { key: 'some auth key string' },
template_id: 'some template id string',
},
fields: ['hasOwnProperty'],
uppy.addFile({
source: 'test',
name: 'traffic.jpg',
data: Buffer.from('test file content 2'),
})
uppy.upload()
await new Promise((resolve) => setTimeout(resolve, 100))
uppy.pauseAll()
await uppy.upload()
server.closeAllConnections()
await new Promise((resolve) => server.close(resolve))
expect(successSpy).toHaveBeenCalled()
server.close()
})
})

View file

@ -9,7 +9,9 @@
"scripts": {
"build": "tsc --build tsconfig.build.json",
"build:css": "sass --load-path=../../ src/style.scss dist/style.css && postcss dist/style.css -u cssnano -o dist/style.min.css",
"typecheck": "tsc --build"
"typecheck": "tsc --build",
"test": "vitest run --silent='passed-only'",
"test:e2e": "vitest watch --project browser --browser.headless false"
},
"keywords": [
"file uploader",
@ -37,10 +39,12 @@
},
"devDependencies": {
"@uppy/core": "workspace:^",
"@vitest/browser": "^3.2.4",
"cssnano": "^7.0.7",
"postcss": "^8.5.6",
"postcss-cli": "^11.0.1",
"sass": "^1.89.2",
"typescript": "^5.8.3"
"typescript": "^5.8.3",
"vitest": "^3.2.4"
}
}

View file

@ -0,0 +1,57 @@
import Uppy from '@uppy/core'
import { page, userEvent } from '@vitest/browser/context'
import { expect, test } from 'vitest'
import Url from './Url.js'
// Normally you would use one of vitest's framework renderers, such as vitest-browser-react,
// but that's overkill for us so we write our own plain HTML renderer.
function render(html: string) {
document.body.innerHTML = ''
const root = document.createElement('main')
root.innerHTML = html
document.body.appendChild(root)
return root
}
test('should return correct file name with URL plugin from remote image with Content-Disposition', async () => {
render('<div id="uppy"></div>')
const uppy = new Uppy().use(Url, {
companionUrl: 'http://localhost:3020',
target: '#uppy',
})
const mockServerUrl = 'http://localhost:62450'
await page
.getByPlaceholder('Enter URL to import a file')
.fill(`${mockServerUrl}/file-with-content-disposition`)
await page.getByText('Import').click()
await new Promise((resolve) => setTimeout(resolve, 500))
await uppy.upload()
const file = uppy.getFiles()[0]
expect(file.name).toBe('DALL·E IMG_9078 - 学中文 🤑')
expect(file.type).toBe('image/jpeg')
expect(file.size).toBe(86500)
})
test('should return correct file name with URL plugin from remote image without Content-Disposition', async () => {
render('<div id="uppy"></div>')
const uppy = new Uppy().use(Url, {
companionUrl: 'http://localhost:3020',
target: '#uppy',
})
const mockServerUrl = 'http://localhost:62450'
await page
.getByPlaceholder('Enter URL to import a file')
.fill(`${mockServerUrl}/file-no-headers`)
await page.getByText('Import').click()
await new Promise((resolve) => setTimeout(resolve, 500))
await uppy.upload()
const file = uppy.getFiles()[0]
expect(file, 'file does not exist').toBeTruthy()
expect(file.name).toBe('file-no-headers')
expect(file.type).toBe('application/octet-stream')
expect(file.size).toBeNull()
})

View file

@ -0,0 +1,21 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{
test: {
name: 'browser',
include: ['src/**/*.browser.test.{ts,tsx}'],
globalSetup: './vitest.setup.ts',
browser: {
enabled: true,
headless: true,
provider: 'playwright',
instances: [{ browser: 'chromium' }],
},
},
},
],
},
})

View file

@ -0,0 +1,70 @@
import { spawn } from 'node:child_process'
import { createServer } from 'node:http'
import { dirname, join } from 'node:path'
import { setTimeout } from 'node:timers/promises'
import { fileURLToPath } from 'node:url'
import { TestProject } from 'vitest/node'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const mockServerPort = 62450
export default async function setup(project: TestProject) {
const mockServer = createServer((req, res) => {
const fileName = `DALL·E IMG_9078 - 学中文 🤑`
if (req.url === '/file-with-content-disposition') {
res.writeHead(200, {
'content-disposition': `attachment; filename="ASCII-name.zip"; filename*=UTF-8''${encodeURIComponent(
fileName,
)}`,
'content-type': 'image/jpeg',
'content-length': '86500',
})
if (req.method === 'HEAD') {
res.end()
} else {
res.end('mock image data')
}
} else if (req.url === '/file-no-headers') {
// Explicitly remove any default content-type
res.removeHeader('content-type')
res.writeHead(200, {})
if (req.method === 'HEAD') {
res.end()
} else {
res.end('mock file content')
}
} else {
res.writeHead(404)
res.end()
}
})
await new Promise<void>((resolve) => {
mockServer.listen(mockServerPort, 'localhost', resolve)
})
const companionProcess = spawn(
'node',
[join(__dirname, '../../@uppy/companion/test/with-load-balancer.mjs')],
{
stdio: 'inherit',
cwd: join(__dirname, '../../../..'),
env: {
...process.env,
// Pass the mock server URL to companion if needed
MOCK_SERVER_URL: `http://localhost:${mockServerPort}`,
},
},
)
await setTimeout(1000)
return () => {
companionProcess.kill()
mockServer.close()
}
}

View file

@ -1,7 +0,0 @@
{
"name": "@types/jasmine",
"version": "0.0.0",
"private": true,
"description": "This package is here to avoid type conflict between @types/jasmine used by Angular and @types/mocha used by everything else.",
"types": "types.d.ts"
}

View file

@ -1,2 +0,0 @@
declare module 'jasmine' {}
declare module 'jasminewd2' {}

View file

@ -1,159 +0,0 @@
#!/usr/bin/env node
/**
* This script can be used to initiate the transition for a plugin from ESM source to
* TS source. It will rename the files, update the imports, and add a `tsconfig.json`.
*/
import { existsSync } from 'node:fs'
import {
appendFile,
open,
opendir,
readFile,
rm,
writeFile,
} from 'node:fs/promises'
import { createRequire } from 'node:module'
import { basename, extname, join } from 'node:path'
import { argv } from 'node:process'
const packageRoot = new URL(`../../packages/${argv[2]}/`, import.meta.url)
let dir
try {
dir = await opendir(new URL('./src/', packageRoot), { recursive: true })
} catch (cause) {
throw new Error(`Unable to find package "${argv[2]}"`, { cause })
}
const packageJSON = JSON.parse(
await readFile(new URL('./package.json', packageRoot), 'utf-8'),
)
if (packageJSON.type !== 'module') {
throw new Error('Cannot convert non-ESM package to TS')
}
const uppyDeps = new Set(
Object.keys(packageJSON.dependencies || {})
.concat(Object.keys(packageJSON.peerDependencies || {}))
.concat(Object.keys(packageJSON.devDependencies || {}))
.filter((pkg) => pkg.startsWith('@uppy/')),
)
// We want TS to check the source files so it doesn't use outdated (or missing) types:
const paths = Object.fromEntries(
(function* generatePaths() {
const require = createRequire(packageRoot)
for (const pkg of uppyDeps) {
const nickname = pkg.slice('@uppy/'.length)
// eslint-disable-next-line import/no-dynamic-require
const pkgJson = require(`../${nickname}/package.json`)
if (pkgJson.main) {
yield [
pkg,
[`../${nickname}/${pkgJson.main.replace(/^(\.\/)?lib\//, 'src/')}`],
]
}
yield [`${pkg}/lib/*`, [`../${nickname}/src/*`]]
}
})(),
)
const references = Array.from(uppyDeps, (pkg) => ({
path: `../${pkg.slice('@uppy/'.length)}/tsconfig.build.json`,
}))
const depsNotYetConvertedToTS = references.filter(
(ref) => !existsSync(new URL(ref.path, packageRoot)),
)
if (depsNotYetConvertedToTS.length) {
// We need to first convert the dependencies, otherwise we won't be working with the correct types.
throw new Error('Some dependencies have not yet been converted to TS', {
cause: depsNotYetConvertedToTS.map((ref) =>
ref.path.replace(/^\.\./, '@uppy'),
),
})
}
let tsConfig
try {
tsConfig = await open(new URL('./tsconfig.json', packageRoot), 'wx')
} catch (cause) {
throw new Error('It seems this package has already been transitioned to TS', {
cause,
})
}
for await (const dirent of dir) {
if (!dirent.isDirectory()) {
const { name } = dirent
const ext = extname(name)
if (ext !== '.js' && ext !== '.jsx') continue // eslint-disable-line no-continue
const filePath =
basename(dirent.path) === name
? dirent.path // Some versions of Node.js give the full path as dirent.path.
: join(dirent.path, name) // Others supply only the path to the parent.
await writeFile(
`${filePath.slice(0, -ext.length)}${ext.replace('js', 'ts')}`,
(await readFile(filePath, 'utf-8'))
.replace(
// The following regex aims to capture all imports and reexports of local .js(x) files to replace it to .ts(x)
// It's far from perfect and will have false positives and false negatives.
/((?:^|\n)(?:import(?:\s+\w+\s+from)?|export\s*\*\s*from|(?:import|export)\s*(?:\{[^}]*\}|\*\s*as\s+\w+\s)\s*from)\s*["']\.\.?\/[^'"]+\.)js(x?["'])/g, // eslint-disable-line max-len
'$1ts$2',
)
.replace(
// The following regex aims to capture all local package.json imports.
/\nimport \w+ from ['"]..\/([^'"]+\/)*package.json['"]\n/g,
(originalImport) =>
`\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n` +
`// @ts-ignore We don't want TS to generate types for the package.json${originalImport}`,
),
)
await rm(filePath)
}
}
await tsConfig.writeFile(
`${JSON.stringify(
{
extends: '../../../tsconfig.shared',
compilerOptions: {
emitDeclarationOnly: false,
noEmit: true,
paths,
},
include: ['./package.json', './src/**/*.*'],
references,
},
undefined,
2,
)}\n`,
)
await tsConfig.close()
await writeFile(
new URL('./tsconfig.build.json', packageRoot),
`${JSON.stringify(
{
extends: '../../../tsconfig.shared',
compilerOptions: {
outDir: './lib',
paths,
resolveJsonModule: false,
rootDir: './src',
},
include: ['./src/**/*.*'],
exclude: ['./src/**/*.test.ts'],
references,
},
undefined,
2,
)}\n`,
)
await appendFile(new URL('./.npmignore', packageRoot), `\ntsconfig.*\n`)
console.log('Done')

View file

@ -1,6 +0,0 @@
{
"name": "vue-template-compiler",
"version": "0.0.1",
"private": true,
"description": "This package is there only to avoid a version conflict in the Vue2 example."
}

3181
yarn.lock

File diff suppressed because it is too large Load diff