feat: cy.selectFile() (#19332)

* feat: cy.attachFile with passed-in contents (#18825)
* feat: attachFile string shorthands (#19045)
* feat: Add drag-n-drop support (#19213)
* Remove cypress-file-upload
This commit is contained in:
Blue F
2021-12-22 09:03:27 -08:00
committed by GitHub
parent 4626f7481c
commit 6d10a7faff
25 changed files with 1535 additions and 68 deletions
+2 -1
View File
@@ -28,7 +28,8 @@
"@types/sizzle": "^2.3.2",
"arch": "^2.2.0",
"blob-util": "^2.0.2",
"bluebird": "3.7.2",
"bluebird": "^3.7.2",
"buffer": "^5.6.0",
"cachedir": "^2.3.0",
"chalk": "^4.1.0",
"check-more-types": "^2.24.0",
+5
View File
@@ -27,3 +27,8 @@ interface NodeEventEmitter {
prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this
eventNames(): Array<string | symbol>
}
// The Buffer type is automatically imported for the browser by webpack
// and we use it for dealing with binary data, especially around the
// selectFile interface.
type BufferType = import("buffer/").Buffer
+35
View File
@@ -662,6 +662,20 @@ declare namespace Cypress {
*/
as(alias: string): Chainable<Subject>
/**
* Select a file with the given <input> element, or drag and drop a file over any DOM subject.
*
* @param {FileReference} files - The file(s) to select or drag onto this element.
* @see https://on.cypress.io/selectfile
* @example
* cy.get('input[type=file]').selectFile(Buffer.from('text'))
* cy.get('input[type=file]').selectFile({
* fileName: 'users.json',
* fileContents: [{name: 'John Doe'}]
* })
*/
selectFile(files: FileReference | FileReference[], options?: Partial<SelectFileOptions>): Chainable<Subject>
/**
* Blur a focused element. This element must currently be in focus.
* If you want to ensure an element is focused before blurring,
@@ -2466,6 +2480,17 @@ declare namespace Cypress {
scrollBehavior: scrollBehaviorOptions
}
interface SelectFileOptions extends Loggable, Timeoutable, ActionableOptions {
/**
* Which user action to perform. `select` matches selecting a file while
* `drag-drop` matches dragging files from the operating system into the
* document.
*
* @default 'select'
*/
action: 'select' | 'drag-drop'
}
interface BlurOptions extends Loggable, Forceable { }
interface CheckOptions extends Loggable, Timeoutable, ActionableOptions {
@@ -5661,6 +5686,16 @@ declare namespace Cypress {
stderr: string
}
type FileReference = string | BufferType | FileReferenceObject
interface FileReferenceObject {
/*
* Buffers and strings will be used as-is. All other types will have `JSON.stringify()` applied.
*/
contents: any
fileName?: string
lastModified?: number
}
interface LogAttrs {
url: string
consoleProps: ObjectLike
-1
View File
@@ -86,7 +86,6 @@
"@urql/vue",
"@vueuse/core",
"bluebird",
"cypress-file-upload",
"cypress-real-events",
"dedent",
"events",
@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html>
<head>
<title>Generic File Inputs</title>
</head>
<script>
const template = document.createElement('template')
template.innerHTML = `
<label for="file-input">Shadow DOM</label>
<input name="file-input" id="file-input" type="file" />
`
class FileInput extends HTMLElement {
constructor() {
super()
this.root = this.attachShadow({ mode: 'open' })
}
connectedCallback() {
this.root.appendChild(template.content.cloneNode(true))
}
}
window.customElements.define('file-input', FileInput);
</script>
<body>
<style>
form {
position: relative;
min-height: 30px;
}
#hidden, #hidden-basic-label {
display: none;
}
#covered, #covering {
position: absolute;
top: 0;
left: 0;
width: 30px;
height: 30px;
}
#covering {
background: black;
z-index: 1;
}
#scroll-form {
display: block;
width: 300px;
height: 50px;
overflow: auto;
}
#tall {
height: 80px;
background: black;
}
</style>
<form>
<label for="basic" id="basic-label">Basic Label</label>
<label for="basic" id="hidden-basic-label">Hidden Label</label>
<input id="basic" type="file" />
<label id="containing-label">
Containing Label
<input type="file" id="contained" />
</label>
</form>
<form>
<input id="multiple" type="file" multpile />
</form>
<form>
<label for="hidden" id="hidden-label">Label for Hidden</label>
<input id="hidden" type="file" />
</form>
<form>
<file-input id="shadow"></file-input>
</form>
<form>
<label for="text-input" id="text-label">Text label</label>
<input type="text" id="text-input" />
<label for="nonexistent" id="nonexistent-label">
Bad label
<input type="file" />
</label>
</form>
<form>
<div id="covering"></div>
<input id="covered" type="file" />
</form>
<form>
<label for="disabled" id="disabled-label">Disabled Label</label>
<input id="disabled" type="file" disabled />
</form>
<form id="scroll-form">
<div id="tall"></div>
<label for="scroll" id="scroll-label">Scroll Label</label>
<input id="scroll" type="file" />
</form>
</body>
</html>
@@ -0,0 +1,675 @@
const { _, $ } = Cypress
// Reading and decoding files from an input element would, in the real world,
// be handled by the application under test, and they would assert on their
// application state. We want to assert on how selectFile behaves directly
// though, and getting the files associated with an <input> as strings is
// handy.
function getFileContents (subject) {
const decoder = new TextDecoder('utf8')
const fileContents = _.map(subject[0].files, (f) => {
return f
.arrayBuffer()
.then((c) => decoder.decode(c))
})
return Promise.all(fileContents)
}
describe('src/cy/commands/actions/selectFile', () => {
beforeEach(() => {
cy.visit('/fixtures/files-form.html')
cy.wrap(Buffer.from('foo')).as('foo')
})
context('#selectFile', () => {
it('selects a single file', () => {
cy.get('#basic')
.selectFile({ contents: '@foo', fileName: 'foo.txt' })
cy.get('#basic')
.then((input) => {
expect(input[0].files.length).to.eq(1)
expect(input[0].files[0].name).to.eq('foo.txt')
expect(input[0].files[0].type).to.eq('')
expect(input[0].files[0].lastModified).to.be.closeTo(Date.now(), 1000)
})
cy.get('#basic')
.then(getFileContents)
.then((contents) => {
expect(contents[0]).to.eql('foo')
})
})
it('selects multiple files', () => {
cy.get('#multiple')
.selectFile([
{
contents: '@foo',
fileName: 'foo.txt',
}, {
contents: Buffer.from('{"a":"bar"}'),
fileName: 'bar.json',
},
Buffer.from('baz'),
])
cy.get('#multiple')
.should('include.value', 'foo.txt')
.then((input) => {
expect(input[0].files[0].name).to.eq('foo.txt')
expect(input[0].files[1].name).to.eq('bar.json')
expect(input[0].files[2].name).to.eq('')
})
cy.get('#multiple')
.then(getFileContents)
.then((contents) => {
expect(contents[0]).to.eq('foo')
expect(contents[1]).to.eq('{"a":"bar"}')
expect(contents[2]).to.eq('baz')
})
})
it('allows custom lastModified', () => {
cy.get('#basic').selectFile({
contents: '@foo',
lastModified: 1234,
})
cy.get('#basic').then((input) => {
expect(input[0].files[0].lastModified).to.eq(1234)
})
})
it('selects files with an input from a label', () => {
cy.get('#basic-label').selectFile({ contents: '@foo' })
cy.get('#basic')
.then(getFileContents)
.then((contents) => {
expect(contents[0]).to.eql('foo')
})
})
it('selects files with an input from a containing label', () => {
cy.get('#containing-label').selectFile({ contents: '@foo' })
cy.get('#contained')
.then(getFileContents)
.then((contents) => {
expect(contents[0]).to.eql('foo')
})
})
it('invokes change and input events on the input', (done) => {
const $input = cy.$$('#basic')
$input.on('input', (e) => {
const obj = _.pick(e.originalEvent, 'bubbles', 'cancelable', 'composed', 'target', 'type')
expect(obj).to.deep.eq({
bubbles: true,
cancelable: false,
composed: true,
target: $input.get(0),
type: 'input',
})
$input.on('change', (e) => {
const obj = _.pick(e.originalEvent, 'bubbles', 'cancelable', 'composed', 'target', 'type')
expect(obj).to.deep.eq({
bubbles: true,
cancelable: false,
composed: false,
target: $input.get(0),
type: 'change',
})
done()
})
})
cy.get('#basic').selectFile({ contents: '@foo' })
})
it('bubbles events', (done) => {
cy.window().then((win) => {
$(win).on('input', () => {
done()
})
})
cy.get('#basic').selectFile({ contents: '@foo' })
})
it('invokes events on the input without changing subject when passed a label', (done) => {
cy.$$('#basic-label').on('input', () => {
throw new Error('shouldn\'t happen')
})
cy.$$('#basic').on('input', () => {
done()
})
cy.get('#basic-label').selectFile({ contents: '@foo' })
.should('have.id', 'basic-label')
})
it('can empty previously filled input', () => {
cy.get('#basic').selectFile({ contents: '@foo' })
cy.get('#basic').selectFile([])
.then((input) => {
expect(input[0].files.length).to.eq(0)
})
})
it('works with shadow DOMs', () => {
cy.get('#shadow')
.shadow()
.find('input')
.as('shadowInput')
.selectFile('@foo')
cy.get('@shadowInput')
.then(getFileContents)
.then((contents) => {
expect(contents[0]).to.eql('foo')
})
})
describe('shorthands', () => {
const validJsonString = `{
"foo": 1,
"bar": {
"baz": "cypress"
}
}
`
it('works with aliased strings', () => {
cy.wrap('foobar').as('alias')
cy.get('#basic').selectFile('@alias')
.then(getFileContents)
.then((contents) => {
expect(contents[0]).to.eql('foobar')
})
})
it('works with aliased objects', () => {
cy.wrap({ foo: 'bar' }).as('alias')
cy.get('#basic').selectFile('@alias')
.then(getFileContents)
.then((contents) => {
expect(contents[0]).to.eql('{"foo":"bar"}')
})
})
it('works with aliased fixtures', () => {
cy.fixture('valid.json').as('myFixture')
cy.get('#basic').selectFile('@myFixture')
.then(getFileContents)
.then((contents) => {
// Because json files are loaded as objects, they get reencoded before
// being used, stripping spaces and newlines
expect(contents[0]).to.eql('{"foo":1,"bar":{"baz":"cypress"}}')
})
})
// Because this is such an important recipe for users, it gets a separate test
// even though readFile already has unit tests around reading files as buffers.
it('works with files read with null encoding', () => {
cy.readFile('cypress/fixtures/valid.json', { encoding: null }).as('myFile')
cy.get('#basic').selectFile('@myFile')
.then(getFileContents)
.then((contents) => {
expect(contents[0]).to.eql(validJsonString)
})
})
it('works with passed in paths', () => {
cy.get('#multiple').selectFile(['cypress/fixtures/valid.json', 'cypress/fixtures/app.js'])
.then(getFileContents)
.then((contents) => {
expect(contents[0]).to.eql(validJsonString)
expect(contents[1]).to.eql('{ \'bar\' }\n')
})
cy.get('#multiple')
.should('include.value', 'valid.json')
.then((input) => {
expect(input[0].files[0].name).to.eq('valid.json')
expect(input[0].files[1].name).to.eq('app.js')
})
})
})
describe('errors', {
defaultCommandTimeout: 50,
}, () => {
it('is a child command', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include('A child command must be chained after a parent because it operates on a previous subject.')
done()
})
cy.selectFile({ contents: '@foo' })
})
it('throws when non dom subject', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` failed because it requires a DOM element.')
done()
})
cy.noop({}).selectFile({ contents: '@foo' })
})
it('throws when non-input subject', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` can only be called on an `<input type="file">` or a `<label for="fileInput">` pointing to or containing one. Your subject is: `<body>...</body>`')
expect(err.docsUrl).to.eq('https://on.cypress.io/selectfile')
done()
})
cy.get('body').selectFile({ contents: '@foo' })
})
it('throws when non-file input', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` can only be called on an `<input type="file">` or a `<label for="fileInput">` pointing to or containing one. Your subject is: `<input type="text" id="text-input">`')
expect(err.docsUrl).to.eq('https://on.cypress.io/selectfile')
done()
})
cy.get('#text-input').selectFile({ contents: '@foo' })
})
it('throws when label for non-file input', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` can only be called on an `<input type="file">` or a `<label for="fileInput">` pointing to or containing one. Your subject is: `<label for="text-input" id="text-label">Text label</label>`')
expect(err.docsUrl).to.eq('https://on.cypress.io/selectfile')
done()
})
cy.get('#text-label').selectFile({ contents: '@foo' })
})
it('throws when label without an attached input', function (done) {
// Even though this label contains a file input, testing on real browsers confirms that clicking it
// does *not* activate the contained input.
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` can only be called on an `<input type="file">` or a `<label for="fileInput">` pointing to or containing one. Your subject is: `<label for="nonexistent" id="nonexistent-label">...</label>`')
expect(err.docsUrl).to.eq('https://on.cypress.io/selectfile')
done()
})
cy.get('#nonexistent-label').selectFile({ contents: '@foo' })
})
it('throws when subject is collection of elements', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` can only be called on a single element. Your subject contained')
expect(err.docsUrl).to.eq('https://on.cypress.io/selectfile')
done()
})
cy.get('input[type="file"]').selectFile({ contents: '@foo' })
})
it('throws when no arguments given', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` must be passed a Buffer or an object with a non-null `contents` property as its 1st argument. You passed: `undefined`.')
expect(err.docsUrl).to.eq('https://on.cypress.io/selectfile')
done()
})
cy.get('#basic').selectFile()
})
it('throws when file is null', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` must be passed a Buffer or an object with a non-null `contents` property as its 1st argument. You passed: `null`.')
expect(err.docsUrl).to.eq('https://on.cypress.io/selectfile')
done()
})
cy.get('#basic').selectFile(null)
})
it('throws when single file.contents is null', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` must be passed a Buffer or an object with a non-null `contents` property as its 1st argument. You passed: `{"contents":null}`.')
expect(err.docsUrl).to.eq('https://on.cypress.io/selectfile')
done()
})
cy.get('#basic').selectFile({ contents: null })
})
it('throws when file is an unknown alias', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` could not find a registered alias for: `@unknown`.')
done()
})
cy.get('#basic').selectFile('@unknown')
})
it('throws when file is an alias for a DOM node', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` can only attach strings, Buffers or objects, while your alias `@body` resolved to: `<body>...</body>`.')
expect(err.docsUrl).to.eq('https://on.cypress.io/selectfile')
done()
})
cy.get('body').as('body')
cy.get('#basic').selectFile('@body')
})
it('throws when file is an alias for null', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` can only attach strings, Buffers or objects, while your alias `@null` resolved to: `null`.')
expect(err.docsUrl).to.eq('https://on.cypress.io/selectfile')
done()
})
cy.wrap(null).as('null')
cy.get('#basic').selectFile('@null')
})
it('throws with aliased intercepts', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` can only attach strings, Buffers or objects, while your alias `@postUser` resolved to: `null`.')
expect(err.docsUrl).to.eq('https://on.cypress.io/selectfile')
done()
})
cy.intercept('POST', '/users', {}).as('postUser')
cy.get('#basic').selectFile('@postUser')
})
it('throws when any path does not exist', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile("this/file/doesNotExist.json")` failed because the file does not exist at the following path:')
expect(err.docsUrl).to.eq('https://on.cypress.io/selectfile')
done()
})
cy.get('#basic').selectFile(['cypress/fixtures/valid.json', 'this/file/doesNotExist.json'])
})
it('throws when any file\'s contents is undefined', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` must be passed an array of Buffers or objects with non-null `contents`. At files[1] you passed: `{}`.')
expect(err.docsUrl).to.eq('https://on.cypress.io/selectfile')
done()
})
cy.get('#basic').selectFile([{ contents: '@foo' }, {}])
})
it('throws on invalid action', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` `action` can only be `select` or `drag-drop`. You passed: `foobar`.')
expect(err.docsUrl).to.eq('https://on.cypress.io/selectfile')
done()
})
cy.get('#basic').selectFile({ contents: '@foo' }, { action: 'foobar' })
})
})
/*
* The tests around actionability are somewhat limited, since the functionality is thoroughly tested in the
* `cy.trigger()` unit tests. We include a few tests directly on `cy.selectFile()` in order to ensure we're
* using $actionability.verify() properly, but don't extensively exercise the logic within it here.
*
* See trigger_spec.js for the full actionability test suite.
*/
describe('actionability', {
defaultCommandTimeout: 50,
}, () => {
it('selects files with a hidden input from a visible label', () => {
cy.get('#hidden-label').selectFile({ contents: '@foo' })
cy.get('#hidden')
.then(getFileContents)
.then((contents) => {
expect(contents[0]).to.eql('foo')
})
})
it('does not work on hidden inputs by default', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` failed because this element is not visible')
done()
})
cy.get('#hidden').selectFile({ contents: '@foo' })
})
it('can force on hidden inputs', () => {
cy.get('#hidden').selectFile({ contents: '@foo' }, { force: true })
})
it('does not work on covered inputs by default', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`\`<input id="covered" type="file">\`
is being covered by another element:
\`<div id="covering"></div>\``)
done()
})
cy.get('#covered').selectFile({ contents: '@foo' })
})
it('can force on covered inputs', () => {
cy.get('#covered').selectFile({ contents: '@foo' }, { force: true })
})
it('does not work on disabled inputs by default', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` failed because this element is `disabled`')
done()
})
cy.get('#disabled-label').selectFile({ contents: '@foo' })
})
it('can force on disabled inputs', () => {
cy.get('#disabled-label').selectFile({ contents: '@foo' }, { force: true })
})
it('does not work on hidden labels by default', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.selectFile()` failed because this element is not visible')
done()
})
cy.get('#hidden-basic-label').selectFile({ contents: '@foo' })
})
it('can force on hidden labels', () => {
cy.get('#hidden-basic-label').selectFile({ contents: '@foo' }, { force: true })
})
it('can scroll to input', () => {
const scrolled = []
cy.on('scrolled', ($el, type) => {
scrolled.push(type)
})
cy.get('#scroll').selectFile({ contents: '@foo' })
.then(() => {
expect(scrolled).not.to.be.empty
})
})
it('can scroll to label', () => {
const scrolled = []
cy.on('scrolled', ($el, type) => {
scrolled.push(type)
})
cy.get('#scroll-label').selectFile({ contents: '@foo' })
.then(() => {
expect(scrolled).not.to.be.empty
})
})
it('does not scroll when forced', () => {
const scrolled = []
cy.on('scrolled', ($el, type) => {
scrolled.push(type)
})
cy.get('#scroll-label').selectFile({ contents: '@foo' }, { force: true })
.then(() => {
expect(scrolled).to.be.empty
})
})
it('waits until input stops animating', {
defaultCommandTimeout: 1000,
}, () => {
let retries = 0
cy.on('command:retry', (obj) => {
retries += 1
})
cy.stub(cy, 'ensureElementIsNotAnimating')
.throws(new Error('animating!'))
.onThirdCall().returns()
cy.get('#basic').selectFile({ contents: '@foo' }).then(() => {
expect(retries).to.eq(3)
expect(cy.ensureElementIsNotAnimating).to.be.calledThrice
})
})
it('can specify scrollBehavior in options', () => {
cy.get('#scroll').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})
cy.get('#scroll').selectFile({ contents: '@foo' }, { scrollBehavior: 'bottom' })
cy.get('#scroll').then((el) => {
expect(el[0].scrollIntoView).to.be.calledWith({ block: 'end' })
})
})
})
describe('drag-drop', () => {
it('attaches a file to an input when targeted', () => {
cy.get('#basic').selectFile({ contents: '@foo' }, { action: 'drag-drop' })
cy.get('#basic')
.then(getFileContents)
.then((contents) => {
expect(contents[0]).to.eql('foo')
})
})
it('invokes change and input events on an input when dropped over', (done) => {
const $input = cy.$$('#basic')
$input.on('input', (e) => {
const obj = _.pick(e.originalEvent, 'bubbles', 'cancelable', 'composed', 'target', 'type')
expect(obj).to.deep.eq({
bubbles: true,
cancelable: false,
composed: true,
target: $input.get(0),
type: 'input',
})
$input.on('change', (e) => {
const obj = _.pick(e.originalEvent, 'bubbles', 'cancelable', 'composed', 'target', 'type')
expect(obj).to.deep.eq({
bubbles: true,
cancelable: false,
composed: false,
target: $input.get(0),
type: 'change',
})
done()
})
})
cy.get('#basic').selectFile({ contents: '@foo' }, { action: 'drag-drop' })
})
it('does not follow labels to their inputs', () => {
cy.get('#basic-label').selectFile({ contents: '@foo' }, { action: 'drag-drop' })
cy.get('#basic').then((input) => {
expect(input[0].files.length).to.eql(0)
})
})
it('does not select multiple files with a single-file input', () => {
cy.get('#basic').selectFile(['@foo', '@foo'], { action: 'drag-drop' })
cy.get('#basic').then((input) => {
expect(input[0].files.length).to.eql(0)
})
})
it('drops files onto any element and triggers events', (done) => {
const $body = cy.$$('body')
let events = []
$body.on('input', (e) => {
throw new Error('should not trigger input')
})
$body.on('change', (e) => {
throw new Error('should not trigger change')
})
$body.on('drag', (e) => events.push(e))
$body.on('dragenter', (e) => events.push(e))
$body.on('dragover', (e) => events.push(e))
$body.on('drop', (e) => {
events.push(e)
expect(_.map(events, 'originalEvent.type')).to.deep.eql(['drag', 'dragenter', 'dragover', 'drop'])
expect(_.every(events, ['originalEvent.bubbles', true])).to.be.true
expect(_.every(events, ['originalEvent.cancelable', true])).to.be.true
expect(_.every(events, ['originalEvent.composed', true])).to.be.true
expect(_.every(events, ['originalEvent.target', $body[0]])).to.be.true
done()
})
cy.get('body').selectFile('@foo', { action: 'drag-drop' })
})
it('includes an entry in `dataTransfer.types`', (done) => {
cy.$$('#multiple').on('drop', (e) => {
expect(e.originalEvent.dataTransfer.types).to.contain('Files')
done()
})
cy.get('#multiple').selectFile({ contents: '@foo' }, { action: 'drag-drop' })
})
})
})
})
@@ -4,24 +4,20 @@ import * as Focus from './focus'
import * as Hover from './hover'
import * as Scroll from './scroll'
import * as Select from './select'
import * as SelectFile from './selectFile'
import * as Submit from './submit'
import * as Type from './type'
import * as Trigger from './trigger'
export { Check }
export { Click }
export { Focus }
export { Hover }
export { Scroll }
export { Select }
export { Submit }
export { Type }
export { Trigger }
export {
Check,
Click,
Focus,
Hover,
Scroll,
Select,
SelectFile,
Submit,
Type,
Trigger,
}
@@ -0,0 +1,315 @@
import { basename } from 'path'
import _ from 'lodash'
import $dom from '../../../dom'
import $errUtils from '../../../cypress/error_utils'
import $actionability from '../../actionability'
import { addEventCoords, dispatch } from './trigger'
/* dropzone.js relies on an experimental, nonstandard API, webkitGetAsEntry().
* https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
* The behavior here varies between browsers, and though unpleasant, we have to attempt
* to replicate it as far as possible.
*
* In webkit browsers, item.webkitGetAsEntry() returns null, because our File()
* instances don't actually correspond with a file on disk. But the property is writeable,
* so we're able to override it with our own implementation that returns a proper object.
*
* In Firefox, the builtin webkitGetAsEntry() returns a useful value, but attempting to
* override webkitGetAsEntry() throws an error.
*
*/
const tryMockWebkit = (item) => {
try {
item.webkitGetAsEntry = () => {
return {
isFile: true,
file: (callback) => callback(item.getAsFile()),
}
}
} catch (e) {
// We're in a browser where webkitGetAsEntry cannot be mocked (probably Firefox).
}
return item
}
const createDataTransfer = (files: Cypress.FileReferenceObject[]): DataTransfer => {
const dataTransfer = new DataTransfer()
files.forEach(({ contents, fileName = '', lastModified = Date.now() }) => {
const file = new File([contents], fileName, { lastModified })
dataTransfer.items.add(file)
})
const oldItems = dataTransfer.items
// dataTransfer.items is a getter, and it - and the items read from its
// returned DataTransferItemList - cannot be assigned to. DataTransferItemLists
// also cannot be constructed, so we have to use an array instead.
Object.defineProperty(dataTransfer, 'items', {
get () {
return _.map(oldItems, tryMockWebkit)
},
})
// When a real user drags file(s) over an element, dataTransfer.types always contains `['Files']`.
// In Firefox however, the 'types' array is not properly populated when we construct our dataTransfer.
if (files.length !== 0 && dataTransfer.types.length === 0) {
Object.defineProperty(dataTransfer, 'types', {
get () {
// This is the value observed from real user events in Firefox 90.
return ['application/x-moz-file', 'Files']
},
})
}
return dataTransfer
}
interface InternalSelectFileOptions extends Cypress.SelectFileOptions {
_log: any
$el: JQuery
}
const ACTIONS = {
select: (element, dataTransfer, coords, state) => {
(element as HTMLInputElement).files = dataTransfer.files
const inputEventOptions = addEventCoords({
bubbles: true,
composed: true,
}, coords)
const changeEventOptions = addEventCoords({
bubbles: true,
}, coords)
dispatch(element, state('window'), 'input', inputEventOptions)
dispatch(element, state('window'), 'change', changeEventOptions)
},
'drag-drop': (element, dataTransfer, coords, state) => {
const dragEventOptions = addEventCoords({
bubbles: true,
composed: true,
cancelable: true,
dataTransfer,
}, coords)
dispatch(element, state('window'), 'drag', dragEventOptions)
dispatch(element, state('window'), 'dragenter', dragEventOptions)
dispatch(element, state('window'), 'dragover', dragEventOptions)
dispatch(element, state('window'), 'drop', dragEventOptions)
// If a user drops file over an <input type="file"> element, browsers attach the file
// to it, exactly as if they'd selected one from a file list.
// If a user drops multiple files over an input without the "multiple" attribute,
// the browser does nothing, and we want to mimic this behavior.
if ($dom.isInputType(element, 'file') && (dataTransfer.files.length === 1 || element.multiple)) {
ACTIONS['select'](element, dataTransfer, coords, state)
}
},
}
export default (Commands, Cypress, cy, state, config) => {
const handleAlias = (file, options) => {
const aliasObj = cy.getAlias(file.contents, 'selectFile', options._log)
if (!aliasObj) {
return
}
if (aliasObj.subject == null) {
$errUtils.throwErrByPath('selectFile.invalid_alias', {
onFail: options._log,
args: { alias: file.contents, subject: aliasObj.subject },
})
}
if ($dom.isElement(aliasObj.subject)) {
const subject = $dom.stringify(aliasObj.subject)
$errUtils.throwErrByPath('selectFile.invalid_alias', {
onFail: options._log,
args: { alias: file.contents, subject },
})
}
return {
...file,
contents: aliasObj.subject,
}
}
// Uses backend read:file rather than cy.readFile because we don't want to retry
// loading a specific file until timeout, but rather retry the selectFile command as a whole
const handlePath = async (file, options) => {
return Cypress.backend('read:file', file.contents, { encoding: null })
.then(({ contents }) => {
return {
// We default to the filename on the path, but allow them to override
fileName: basename(file.contents),
...file,
contents: Buffer.from(contents),
}
})
.catch((err) => {
if (err.code === 'ENOENT') {
$errUtils.throwErrByPath('files.nonexistent', {
args: { cmd: 'selectFile', file: file.contents, filePath: err.filePath },
})
}
$errUtils.throwErrByPath('files.unexpected_error', {
onFail: options._log,
args: { cmd: 'selectFile', action: 'read', file, filePath: err.filePath, error: err.message },
})
})
}
/*
* Turns a user-provided file - a string shorthand, Buffer, or object
* into an object of form {
* contents,
* fileName?,
* lastModified?,
* }.
*
* Or throws an error. Users do many strange things, and this is where
* we warn them and suggest how to fix it.
*/
const parseFile = (options) => {
return async (file: any, index: number, filesArray: any[]): Promise<Cypress.FileReferenceObject> => {
if (typeof file === 'string' || Buffer.isBuffer(file)) {
file = { contents: file }
}
if (!file || file.contents == null) {
// Different error messages if the user passed in one file vs. an array of files
if (filesArray.length > 1) {
$errUtils.throwErrByPath('selectFile.invalid_array_file_reference', {
onFail: options._log,
args: { file: JSON.stringify(file), index },
})
} else {
$errUtils.throwErrByPath('selectFile.invalid_single_file_reference', {
onFail: options._log,
args: { file: JSON.stringify(file) },
})
}
}
if (typeof file.contents === 'string') {
file = handleAlias(file, options) ?? await handlePath(file, options)
}
if (!_.isString(file.contents) && !Buffer.isBuffer(file.contents)) {
file.contents = JSON.stringify(file.contents)
}
return file
}
}
Commands.addAll({ prevSubject: 'element' }, {
async selectFile (subject: JQuery<any>, files: Cypress.FileReference | Cypress.FileReference[], options: Partial<InternalSelectFileOptions>): Promise<JQuery> {
options = _.defaults({}, options, {
action: 'select',
log: true,
$el: subject,
})
if (options.log) {
options._log = Cypress.log({
$el: options.$el,
timeout: options.timeout,
consoleProps () {
return {
'Target': $dom.getElements(options.$el),
Elements: options.$el?.length,
}
},
})
options._log.snapshot('before', { next: 'after' })
}
if (!options.action || !ACTIONS[options.action]) {
$errUtils.throwErrByPath('selectFile.invalid_action', {
onFail: options._log,
args: { action: options.action },
})
}
if (subject.length > 1) {
$errUtils.throwErrByPath('selectFile.multiple_elements', {
onFail: options._log,
args: { num: subject.length },
})
}
let eventTarget = subject
// drag-drop always targets the subject directly, but select
// may switch <label> -> <input> element
if (options.action === 'select') {
if (eventTarget.is('label')) {
eventTarget = $dom.getInputFromLabel(eventTarget)
}
if (eventTarget.length < 1 || !$dom.isInputType(eventTarget, 'file')) {
const node = $dom.stringify(options.$el)
$errUtils.throwErrByPath('selectFile.not_file_input', {
onFail: options._log,
args: { node },
})
}
}
if (!options.force) {
cy.ensureNotDisabled(eventTarget, options._log)
}
// Make sure files is an array even if the user only passed in one
const filesArray = await Promise.all(([] as Cypress.FileReference[]).concat(files).map(parseFile(options)))
// We verify actionability on the subject, rather than the eventTarget,
// in order to allow for a hidden <input> with a visible <label>
// Similarly, this is why we implement something similar, but not identical to,
// cy.trigger() to dispatch our events.
await $actionability.verify(cy, subject, config, options, {
onScroll ($el, type) {
Cypress.action('cy:scrolled', $el, type)
},
onReady ($elToClick, coords) {
// We dispatch the events on the eventTarget element, but target the red dot
// based on $elToClick (the coords $actionability.verify gave us),
// which is the original subject.
if (options._log) {
// display the red dot at these coords
options._log.set({
coords: coords.fromAutWindow,
})
}
const dataTransfer = createDataTransfer(filesArray)
ACTIONS[options.action as string](eventTarget.get(0), dataTransfer, coords, state)
return $elToClick
},
})
const verifyAssertions = () => {
return cy.verifyUpcomingAssertions(options.$el, options, {
onRetry: verifyAssertions,
})
}
return verifyAssertions()
},
})
}
@@ -7,7 +7,7 @@ import $dom from '../../../dom'
import $errUtils from '../../../cypress/error_utils'
import $actionability from '../../actionability'
const dispatch = (target, appWindow, eventName, options) => {
export const dispatch = (target, appWindow, eventName, options) => {
const eventConstructor = options.eventConstructor ?? 'Event'
const ctor = appWindow[eventConstructor]
@@ -38,6 +38,19 @@ const dispatch = (target, appWindow, eventName, options) => {
return target.dispatchEvent(event)
}
export const addEventCoords = (eventOptions, coords) => {
const { fromElWindow, fromElViewport } = coords
return _.extend({
clientX: fromElViewport.x,
clientY: fromElViewport.y,
screenX: fromElViewport.x,
screenY: fromElViewport.y,
pageX: fromElWindow.x,
pageY: fromElWindow.y,
}, eventOptions)
}
export default (Commands, Cypress, cy, state, config) => {
return Commands.addAll({ prevSubject: ['element', 'window', 'document'] }, {
trigger (subject, eventName, positionOrX, y, userOptions = {}) {
@@ -117,23 +130,14 @@ export default (Commands, Cypress, cy, state, config) => {
},
onReady ($elToClick, coords) {
const { fromElWindow, fromElViewport, fromAutWindow } = coords
if (options._log) {
// display the red dot at these coords
options._log.set({
coords: fromAutWindow,
coords: coords.fromAutWindow,
})
}
eventOptions = _.extend({
clientX: fromElViewport.x,
clientY: fromElViewport.y,
screenX: fromElViewport.x,
screenY: fromElViewport.y,
pageX: fromElWindow.x,
pageY: fromElWindow.y,
}, eventOptions)
eventOptions = addEventCoords(eventOptions, coords)
return dispatch($elToClick.get(0), state('window'), eventName, eventOptions)
},
+1 -1
View File
@@ -91,7 +91,7 @@ export default (Commands, Cypress, cy) => {
// file exists but it shouldn't - or - file doesn't exist but it should
const errPath = contents ? 'files.existent' : 'files.nonexistent'
const { message, docsUrl } = $errUtils.cypressErrByPath(errPath, {
args: { file, filePath },
args: { cmd: 'readFile', file, filePath },
})
err.message = message
+38 -18
View File
@@ -121,40 +121,56 @@ export default {
},
as: {
docsUrl: 'https://on.cypress.io/as',
empty_string: {
message: `${cmd('as')} cannot be passed an empty string.`,
docsUrl: 'https://on.cypress.io/as',
},
invalid_type: {
message: `${cmd('as')} can only accept a string.`,
docsUrl: 'https://on.cypress.io/as',
},
invalid_first_token: {
message: '`{{alias}}` cannot be named starting with the `@` symbol. Try renaming the alias to `{{suggestedName}}`, or something else that does not start with the `@` symbol.',
docsUrl: 'https://on.cypress.io/as',
},
reserved_word: {
message: `${cmd('as')} cannot be aliased as: \`{{alias}}\`. This word is reserved.`,
docsUrl: 'https://on.cypress.io/as',
},
},
selectFile: {
docsUrl: 'https://on.cypress.io/selectfile',
invalid_action: {
message: `${cmd('selectFile')} \`action\` can only be \`select\` or \`drag-drop\`. You passed: \`{{action}}\`.`,
},
invalid_array_file_reference: {
message: `${cmd('selectFile')} must be passed an array of Buffers or objects with non-null \`contents\`. At files[{{index}}] you passed: \`{{file}}\`.`,
},
invalid_single_file_reference: {
message: `${cmd('selectFile')} must be passed a Buffer or an object with a non-null \`contents\` property as its 1st argument. You passed: \`{{file}}\`.`,
},
multiple_elements: {
message: `${cmd('selectFile')} can only be called on a single element. Your subject contained {{num}} elements.`,
},
not_file_input: {
message: `${cmd('selectFile')} can only be called on an \`<input type="file">\` or a \`<label for="fileInput">\` pointing to or containing one. Your subject is: \`{{node}}\`.`,
},
invalid_alias: {
message: `${cmd('selectFile')} can only attach strings, Buffers or objects, while your alias \`{{alias}}\` resolved to: \`{{subject}}\`.`,
},
},
blur: {
docsUrl: 'https://on.cypress.io/blur',
multiple_elements: {
message: `${cmd('blur')} can only be called on a single element. Your subject contained {{num}} elements.`,
docsUrl: 'https://on.cypress.io/blur',
},
no_focused_element: {
message: `${cmd('blur')} can only be called when there is a currently focused element.`,
docsUrl: 'https://on.cypress.io/blur',
},
timed_out: {
message: `${cmd('blur')} timed out because your browser did not receive any \`blur\` events. This is a known bug in Chrome when it is not the currently focused window.`,
docsUrl: 'https://on.cypress.io/blur',
},
wrong_focused_element: {
message: `${cmd('blur')} can only be called on the focused element. Currently the focused element is a: \`{{node}}\``,
docsUrl: 'https://on.cypress.io/blur',
},
},
@@ -477,12 +493,14 @@ export default {
docsUrl: `https://on.cypress.io/${_.toLower(obj.cmd)}`,
}
},
existent: {
message: stripIndent`
${cmd('readFile', '"{{file}}"')} failed because the file exists when expected not to exist at the following path:
existent (obj) {
return {
message: stripIndent`
${cmd('{{cmd}}', '"{{file}}"')} failed because the file exists when expected not to exist at the following path:
\`{{filePath}}\``,
docsUrl: 'https://on.cypress.io/readfile',
\`{{filePath}}\``,
docsUrl: `https://on.cypress.io/${_.toLower(obj.cmd)}`,
}
},
invalid_argument (obj) {
return {
@@ -494,12 +512,14 @@ export default {
message: `${cmd('writeFile')} must be passed a non-empty string, an object, or an array as its 2nd argument. You passed: \`{{contents}}\`.`,
docsUrl: 'https://on.cypress.io/writefile',
},
nonexistent: {
message: stripIndent`
${cmd('readFile', '"{{file}}"')} failed because the file does not exist at the following path:
nonexistent (obj) {
return {
message: stripIndent`
${cmd('{{cmd}}', '"{{file}}"')} failed because the file does not exist at the following path:
\`{{filePath}}\``,
docsUrl: 'https://on.cypress.io/readfile',
\`{{filePath}}\``,
docsUrl: `https://on.cypress.io/${_.toLower(obj.cmd)}`,
}
},
timed_out (obj) {
return {
+19
View File
@@ -279,3 +279,22 @@ export const getContainsSelector = (text, filter = '', options: {
return selectors.join()
}
export const getInputFromLabel = ($el) => {
if (!$el.is('label')) {
return $([])
}
// If an element has a "for" attribute, then clicking on it won't
// focus / activate any contained inputs, even if the "for" target doesn't
// exist.
if ($el.attr('for')) {
// The parent().last() is the current document, which is where we want to
// search from.
return $(`#${$el.attr('for')}`, $el.parents().last())
}
// Alternately, if a label contains inputs, clicking it focuses / activates
// the first one.
return $('input', $el).first()
}
+2 -1
View File
@@ -10,7 +10,7 @@ const { isWindow, getWindowByElement } = $window
const { isDocument, getDocumentFromElement } = $document
const { wrap, unwrap, isJquery, query } = $jquery
const { isVisible, isHidden, isStrictlyHidden, isHiddenByAncestors, getReasonIsHidden, isW3CRendered, isW3CFocusable } = $visibility
const { isInputType, isFocusable, isElement, isScrollable, isFocused, stringify, getElements, getContainsSelector, getFirstDeepestElement, isDetached, isAttached, isTextLike, isSelector, isDescendent, getFirstFixedOrStickyPositionParent, getFirstStickyPositionParent, getFirstScrollableParent, isUndefinedOrHTMLBodyDoc, elementFromPoint, getParent, findAllShadowRoots, isWithinShadowRoot, getHostContenteditable } = $elements
const { isInputType, isFocusable, isElement, isScrollable, isFocused, stringify, getElements, getContainsSelector, getFirstDeepestElement, getInputFromLabel, isDetached, isAttached, isTextLike, isSelector, isDescendent, getFirstFixedOrStickyPositionParent, getFirstStickyPositionParent, getFirstScrollableParent, isUndefinedOrHTMLBodyDoc, elementFromPoint, getParent, findAllShadowRoots, isWithinShadowRoot, getHostContenteditable } = $elements
const { getCoordsByPosition, getElementPositioning, getElementCoordinatesByPosition, getElementAtPointFromViewport, getElementCoordinatesByPositionRelativeToXY } = $coordinates
const { getSelectionBounds } = $selection
@@ -69,4 +69,5 @@ export default {
getSelectionBounds,
getDocumentFromElement,
getParent,
getInputFromLabel,
}
@@ -64,6 +64,7 @@ export const e2eProjectDirs = [
'retries-2',
'same-fixtures-integration-folders',
'screen-size',
'selectFile',
'shadow-dom-global-inclusion',
'spec-generation',
'spec-name-special-characters',
@@ -7,7 +7,6 @@ import { print, FragmentDefinitionNode } from 'graphql'
import { testUrqlClient } from './clientTestUrqlClient'
import { Component, computed, watch, defineComponent, h, toRaw } from 'vue'
import { each } from 'lodash'
import 'cypress-file-upload'
import { createI18n } from '@cy/i18n'
/**
-1
View File
@@ -85,7 +85,6 @@
"@urql/vue",
"@vue/test-utils",
"@vueuse/core",
"cypress-file-upload",
"dedent",
"fake-uuid",
"graphql",
+1 -2
View File
@@ -48,7 +48,7 @@
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": ["../driver/src"], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [] /* List of folders to include type definitions from. */
/* Type declaration files to be included in compilation. */
"paths": {
"@cy/i18n": ["../frontend-shared/src/locales/i18n"],
@@ -60,7 +60,6 @@
"./vite-env",
"@intlify/vite-plugin-vue-i18n/client",
"@testing-library/cypress",
"cypress-file-upload",
"cypress"
],
"allowSyntheticDefaultImports": true,
-2
View File
@@ -41,7 +41,6 @@
"classnames": "2.3.1",
"concurrently": "^6.2.0",
"cross-env": "6.0.3",
"cypress-file-upload": "^5.0.8",
"graphql": "^15.5.1",
"graphql-tag": "^2.12.5",
"gravatar": "1.8.0",
@@ -88,7 +87,6 @@
"@urql/vue",
"@vue/test-utils",
"@vueuse/core",
"cypress-file-upload",
"dedent",
"fake-uuid",
"graphql",
@@ -3,6 +3,7 @@ import GlobalPageHeader from './GlobalPageHeader.vue'
import { ref } from 'vue'
const searchLabel = defaultMessages.globalPage.searchPlaceholder
const dropzoneSelector = '[data-testid="dropzone"] > div'
const fileInputSelector = 'input[type=file]'
const addProjectSelector = '[data-testid=addProjectButton]'
@@ -43,8 +44,8 @@ describe('<GlobalPageHeader />', () => {
})
it('handles a file upload', () => {
cy.get(fileInputSelector)
.attachFile('test-project/cypress.config.ts')
cy.get(dropzoneSelector)
.selectFile('cypress/fixtures/test-project/cypress.config.ts', { action: 'drag-drop' })
.get(addProjectSelector)
.click()
.get('@fileUpload').should('have.been.called')
@@ -0,0 +1 @@
module.exports = {}
@@ -0,0 +1,105 @@
import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import Dropzone from 'dropzone'
describe('selectFile', () => {
describe('uppy', () => {
beforeEach(() => {
cy.document().then((doc) => {
doc.body.innerHTML = `
<link rel="stylesheet" href="/node_modules/@uppy/core/dist/style.css" />
<link rel="stylesheet" href="/node_modules/@uppy/dashboard/dist/style.css" />
<div id="uppy"></div>
`
})
cy.get('#uppy').then((div) => {
new Uppy().use(Dashboard, {
inline: true,
target: div[0],
})
})
})
it('can input files', () => {
// Because Uppy triggers file input on clicking a button - via js event
// handler - there's no way for cypress know that a button should trigger
// the file input. We have to target the hidden input and `force` it.
cy.get('input').first().selectFile([
{ contents: Buffer.from('foo'), fileName: 'bar.txt' },
{ contents: Buffer.from('foo2'), fileName: 'baz.txt' },
], { force: true })
cy.get('#uppy')
.should('contain', 'bar.txt')
.should('contain', 'baz.txt')
.should('contain', '3 B')
.should('contain', '4 B')
})
it('can drop files', () => {
cy.get('.uppy-Dashboard-AddFiles').first().selectFile([
{ contents: Buffer.from('foo'), fileName: 'bar.txt' },
{ contents: Buffer.from('foo2'), fileName: 'baz.txt' },
], { action: 'drag-drop' })
cy.get('#uppy')
.should('contain', 'bar.txt')
.should('contain', 'baz.txt')
.should('contain', '3 B')
.should('contain', '4 B')
})
})
describe('dropzone', () => {
beforeEach(() => {
cy.document().then((doc) => {
doc.body.innerHTML = `
<link rel="stylesheet" href="/node_modules/dropzone/dist/basic.css" />
<form class="dropzone"></form>
`
})
cy.get('.dropzone').then((div) => {
new Dropzone(div[0], {
url: 'example.com',
hiddenInputContainer: div[0],
autoProcessQueue: false,
})
})
})
it('can input files via dropzone', () => {
// Because dropzone triggers file input on clicking a button - via js event
// handler - there's no way for cypress know that a button should trigger
// the file input. We have to target the hidden input and `force` it.
cy.get('input').first().selectFile([
{ contents: Buffer.from('foo'), fileName: 'bar.txt' },
{ contents: Buffer.from('foo2'), fileName: 'baz.txt' },
], { force: true })
cy.get('.dz-preview')
.should('contain', 'bar.txt')
.should('contain', 'baz.txt')
.should('contain', '3 b')
.should('contain', '4 b')
})
it('can drop files via dropzone', () => {
// Because dropzone triggers file input on clicking a button - via js event
// handler - there's no way for cypress know that a button should trigger
// the file input. We have to target the hidden input and `force` it.
cy.get('.dropzone').first().selectFile([
{ contents: Buffer.from('foo'), fileName: 'bar.txt' },
{ contents: Buffer.from('foo2'), fileName: 'baz.txt' },
], { action: 'drag-drop' })
cy.get('.dz-preview')
.should('contain', 'bar.txt')
.should('contain', 'baz.txt')
.should('contain', '3 b')
.should('contain', '4 b')
})
})
})
@@ -0,0 +1,9 @@
{
"name": "@test-project/selectFile",
"version": "0.0.0-test",
"devDependencies": {
"@uppy/core": "^2.1.2",
"@uppy/dashboard": "2.1.1",
"dropzone": "6.0.0-beta.1"
}
}
+163
View File
@@ -0,0 +1,163 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@swc/helpers@^0.2.13":
version "0.2.14"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.2.14.tgz#20288c3627442339dd3d743c944f7043ee3590f0"
integrity sha512-wpCQMhf5p5GhNg2MmGKXzUNwxe7zRiCsmqYsamez2beP7mKPCSiu+BjZcdN95yYSzO857kr0VfQewmGpS77nqA==
"@transloadit/prettier-bytes@0.0.7":
version "0.0.7"
resolved "https://registry.yarnpkg.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz#cdb5399f445fdd606ed833872fa0cabdbc51686b"
integrity sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==
"@uppy/core@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@uppy/core/-/core-2.1.2.tgz#a06cf70902fac79d9def21456d845a2e0968b512"
integrity sha512-w5wE2THkGxYxwxfZ/89q1s8umGNswCXmgjwUxB52faOnosvtu3BLp/hJltmWUQ6YNlsRsjrzxA5IyZ+/J3ApNQ==
dependencies:
"@transloadit/prettier-bytes" "0.0.7"
"@uppy/store-default" "^2.0.2"
"@uppy/utils" "^4.0.3"
lodash.throttle "^4.1.1"
mime-match "^1.0.2"
namespace-emitter "^2.0.1"
nanoid "^3.1.25"
preact "^10.5.13"
"@uppy/dashboard@2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@uppy/dashboard/-/dashboard-2.1.1.tgz#ccecf0a50b51a76e81efb7b093be9edb01851bf0"
integrity sha512-o+x381JXDAbl1M/gNeZWePfjfgYf8NPS3EvABH9P488hWGmzJUErZ5BODcrbMK9PovRlwU3SelHrMzfz232NbQ==
dependencies:
"@transloadit/prettier-bytes" "0.0.7"
"@uppy/informer" "^2.0.4"
"@uppy/provider-views" "^2.0.4"
"@uppy/status-bar" "^2.1.1"
"@uppy/thumbnail-generator" "^2.0.5"
"@uppy/utils" "^4.0.3"
classnames "^2.2.6"
is-shallow-equal "^1.0.1"
lodash.debounce "^4.0.8"
memoize-one "^5.0.4"
nanoid "^3.1.25"
preact "^10.5.13"
"@uppy/informer@^2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@uppy/informer/-/informer-2.0.4.tgz#7c6d86e9ec17753b76cfd91fad76b7391c3e72c3"
integrity sha512-S7ooDZPWqfCBZtFct4Kh1oqbJ14t+zNLIRhNAZu/3rQa7AFHGCjU75c0uq+T+lI+es71DUsZukCUufEs+piprg==
dependencies:
"@uppy/utils" "^4.0.3"
preact "^10.5.13"
"@uppy/provider-views@^2.0.4":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@uppy/provider-views/-/provider-views-2.0.5.tgz#6b0fa8481eb5dcd482641c739f6d45b8aa91106d"
integrity sha512-FdxgzwEOM0Q6TeXxwg5MtmpBiJ5AHAX/KciC+TUlpPYCZjf3g1/EHv/fQrLWou3LRV2H02qciJFAXtALCuE5JQ==
dependencies:
"@uppy/utils" "^4.0.3"
classnames "^2.2.6"
preact "^10.5.13"
"@uppy/status-bar@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@uppy/status-bar/-/status-bar-2.1.1.tgz#02e97759a3efd0b3e09f97ca3ac07240b9d5de4a"
integrity sha512-/rAOdpegBocksboMxhmTWOreiJNiYjz3m2UTWWi1X3TOv1uM3R8pxECdCBWKTue+sD+bVe0ACRVRMCNrG/QKYA==
dependencies:
"@transloadit/prettier-bytes" "0.0.7"
"@uppy/utils" "^4.0.3"
classnames "^2.2.6"
lodash.throttle "^4.1.1"
preact "^10.5.13"
"@uppy/store-default@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@uppy/store-default/-/store-default-2.0.2.tgz#c0464e92452fdc7d4cd1548d2c7453017cad7a98"
integrity sha512-D9oz08EYBoc4fDotvaevd2Q7uVldS61HYFOXK20b5M/xXF/uxepapaqQnMu1DfCVsA77rhp7DMemxnWc9y8xTQ==
"@uppy/thumbnail-generator@^2.0.5":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@uppy/thumbnail-generator/-/thumbnail-generator-2.0.5.tgz#2ac27b55f2d1d57c92a5a072796229da08679451"
integrity sha512-z9A3mKnrUyB2tmIya6nbZYQmmM7Af++IojhaBf6hc/v7gVGW4MF+B/GBaf9nt4XuD5gpnx8EnPS2h1KQL4srvw==
dependencies:
"@uppy/utils" "^4.0.3"
exifr "^6.0.0"
"@uppy/utils@^4.0.3":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@uppy/utils/-/utils-4.0.3.tgz#181fdd161e1450d31af0cf7bc97946a99196a8fe"
integrity sha512-LApneC8lNvTonzSJFupxzuEvKhwp/Klc1otq8t+zXpdgjLVVSuW/rJBFfdIDrmDoqSzVLQKYjMy07CmhDAWfKg==
dependencies:
lodash.throttle "^4.1.1"
classnames@^2.2.6:
version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
dropzone@6.0.0-beta.1:
version "6.0.0-beta.1"
resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-6.0.0-beta.1.tgz#4e59a904676e3766f4adc32fed0ab9a445bd470f"
integrity sha512-j6882YI4oa0vxV6nFdW7/0QiWBp0DKdtLC/J2GUz4v6FIz3SNLtZbXl43lUTIs+FrwixnfIsCSjQuTpaGNDeLQ==
dependencies:
"@swc/helpers" "^0.2.13"
just-extend "^5.0.0"
exifr@^6.0.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/exifr/-/exifr-6.3.0.tgz#ba6a49c0a30372a969d109684e1cd8450a05ba43"
integrity sha512-NCSOP15py+4QyvD90etFN0QOVj12ygVE8kfEDG8GDc+SXf9YAOxua2x5kGp6WvxbGjufA5C3r/1ZKHOpHbEWFg==
is-shallow-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-shallow-equal/-/is-shallow-equal-1.0.1.tgz#c410b51eb1c12ee50cd02891d32d1691a132d73c"
integrity sha512-lq5RvK+85Hs5J3p4oA4256M1FEffzmI533ikeDHvJd42nouRRx5wBzt36JuviiGe5dIPyHON/d0/Up+PBo6XkQ==
just-extend@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-5.1.1.tgz#4f33b1fc719964f816df55acc905776694b713ab"
integrity sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
lodash.throttle@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
memoize-one@^5.0.4:
version "5.2.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
mime-match@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/mime-match/-/mime-match-1.0.2.tgz#3f87c31e9af1a5fd485fb9db134428b23bbb7ba8"
integrity sha1-P4fDHprxpf1IX7nbE0Qosju7e6g=
dependencies:
wildcard "^1.1.0"
namespace-emitter@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz#978d51361c61313b4e6b8cf6f3853d08dfa2b17c"
integrity sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==
nanoid@^3.1.25:
version "3.1.30"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
preact@^10.5.13:
version "10.6.2"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.6.2.tgz#c849f91df9ad36bfa64d1a5d5880977f767c69e5"
integrity sha512-ppDjurt75nSxyikpyali+uKwRl8CK9N6ntOPovGIEGQagjMLVzEgVqFEsUUyUrqyE9Ch90KE0jmFc9q2QcPLBA==
wildcard@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-1.1.2.tgz#a7020453084d8cd2efe70ba9d3696263de1710a5"
integrity sha1-pwIEUwhNjNLv5wup02liY94XEKU=
+17
View File
@@ -0,0 +1,17 @@
const systemTests = require('../lib/system-tests').default
// While these tests are fairly simple and could be normal cypress tests,
// they also depend on 3rd party libraries used only for the test that we
// don't want to add to cypress' dependencies. They are therefore e2e tests,
// which can have their own package.json.
describe('selectFile', () => {
systemTests.setup()
it('works with 3rd party libraries', function () {
return systemTests.exec(this, {
project: 'selectFile',
snapshot: false,
expectedExitCode: 0,
})
})
})
+4 -9
View File
@@ -14401,7 +14401,7 @@ buffer@5.6.0:
base64-js "^1.0.2"
ieee754 "^1.1.4"
buffer@^5.0.2, buffer@^5.1.0, buffer@^5.2.0, buffer@^5.5.0, buffer@^5.7.0:
buffer@^5.0.2, buffer@^5.1.0, buffer@^5.2.0, buffer@^5.5.0, buffer@^5.6.0, buffer@^5.7.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
@@ -17466,11 +17466,6 @@ cypress-expect@2.0.0:
arg "4.1.3"
debug "4.2.0"
cypress-file-upload@^5.0.8:
version "5.0.8"
resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1"
integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g==
cypress-image-snapshot@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/cypress-image-snapshot/-/cypress-image-snapshot-3.1.1.tgz#cb7242d8086e0d4cbb7f333f927f71cdb5ff9971"
@@ -28725,9 +28720,9 @@ memfs@^3.1.2, memfs@^3.2.2:
fs-monkey "1.0.3"
"memoize-one@>=3.1.1 <6", memoize-one@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
version "5.2.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
memoizerific@^1.11.3:
version "1.11.3"