mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-20 22:19:46 -05:00
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:
+2
-1
@@ -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",
|
||||
|
||||
Vendored
+5
@@ -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
|
||||
|
||||
Vendored
+35
@@ -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
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
/**
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
"@urql/vue",
|
||||
"@vue/test-utils",
|
||||
"@vueuse/core",
|
||||
"cypress-file-upload",
|
||||
"dedent",
|
||||
"fake-uuid",
|
||||
"graphql",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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=
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user