import React from 'react'; import sinon from 'sinon'; import { expect } from 'chai'; import { setImmediate as tick } from 'timers/promises'; import { render, screen, cleanup, fireEvent, waitFor, } from '@testing-library/react'; import FileInput, { FileInputBackendProvider, createElectronFileInputBackend, } from './file-input'; describe('FileInput', function () { let spy; beforeEach(function () { spy = sinon.spy(); }); afterEach(function () { cleanup(); }); it('renders "Select a file..." if values is falsy and multi is false', function () { render( ); const button = screen.getByTestId('file-input-button'); expect(button.textContent).to.equal('Select a file...'); }); it('renders "Select a file..." if values is empty and multi is false', function () { render( ); const button = screen.getByTestId('file-input-button'); expect(button.textContent).to.equal('Select a file...'); }); it('renders "Select files..." if values is falsy and multi is true', function () { render( ); const button = screen.getByTestId('file-input-button'); expect(button.textContent).to.equal('Select files...'); }); it('renders "a.png" if values is [a.png]', function () { render( ); const button = screen.getByTestId('file-input-button'); expect(button.textContent).to.equal('a.png'); }); it('renders "a.png, b.png" if values is [a.png, b.png]', function () { render( ); const button = screen.getByTestId('file-input-button'); expect(button.textContent).to.equal('a.png, b.png'); }); it('supports variant vertical', function () { render( ); // how do we test this since it is just different css? }); it('supports variant default', function () { render( ); // how do we test this since it is just different css? }); it('adds styling when error=true', function () { render( ); // how do we test this since it is just different css? }); it('adds a link if link is specified', function () { render( ); const link = screen.getByTestId('file-input-link'); expect(link).to.exist; }); it('adds description if description is specified', function () { render( ); const description = screen.getByTestId('file-input-description'); expect(description).to.exist; }); it('adds link and description if specified', function () { render( ); const linkDescription = screen.getByTestId('file-input-link'); expect(linkDescription).to.exist; }); it('adds error message if specified', function () { render( ); const errorMessage = screen.getByTestId('file-input-error'); expect(errorMessage).to.exist; }); it('does not show optional if not specified', function () { render( ); expect(screen.queryByText('Optional')).to.equal(null); }); it('renders the optional when specified', function () { render( ); expect(screen.getByText('Optional')).to.be.visible; }); it('renders the optional message when specified', function () { render( ); expect(screen.getByText('pineapples')).to.be.visible; }); describe('when a file is chosen', function () { beforeEach(async function () { render( ); const fileInput = screen.getByTestId('test-file-input'); await waitFor(() => fireEvent.change(fileInput, { target: { files: [ { path: 'new/file/path', }, ], }, }) ); }); it('calls onChange with the chosen file', function () { expect(spy.callCount).to.equal(1); expect(spy.firstCall.args[0]).to.deep.equal(['new/file/path']); }); }); it('renders the file name with close button when showFileOnNewLine=true', function () { render( ); expect(screen.getByText('new/file/nice')).to.be.visible; expect(screen.getByText('another/file/path')).to.be.visible; expect(screen.getAllByLabelText('Remove file').length).to.equal(2); }); describe('when a file is clicked to remove on multi line', function () { beforeEach(async function () { render( ); const removeButton = screen.getAllByLabelText('Remove file')[0]; await waitFor(() => fireEvent.click(removeButton)); }); it('calls onChange with the file removed', function () { expect(spy.callCount).to.equal(1); expect(spy.firstCall.args[0]).to.deep.equal(['another/file/path']); }); }); describe('createElectronFileInputBackend', function () { function createFakeElectron() { const fakeWindow = {}; const fakeElectron = { getCurrentWindow: sinon.stub().returns(fakeWindow), dialog: { showSaveDialog: sinon.stub(), showOpenDialog: sinon.stub(), }, }; return { fakeElectron, fakeWindow }; } it('allows using electron-style APIs for file updates', async function () { const { fakeElectron, fakeWindow } = createFakeElectron(); const backend = createElectronFileInputBackend(fakeElectron)(); const listener = sinon.stub(); const unsubscribe = backend.onFilesChosen(listener); fakeElectron.dialog.showSaveDialog.resolves({ canceled: false, filePath: 'filepath', }); backend.openFileChooser({ mode: 'save', multi: false, accept: '.json' }); expect(fakeElectron.dialog.showOpenDialog).to.not.have.been.called; expect(fakeElectron.dialog.showSaveDialog).to.have.been.calledOnceWith( fakeWindow, { properties: ['openFile'], filters: [{ name: '.json file', extensions: ['json'] }], } ); expect(listener).to.not.have.been.called; await tick(); expect(listener).to.been.calledOnceWith(['filepath']); unsubscribe(); backend.openFileChooser({ mode: 'save', multi: false, accept: '.json' }); await tick(); expect(listener).to.have.been.calledOnce; expect(fakeElectron.dialog.showSaveDialog).to.have.been.calledTwice; }); it('can partially handle browser-compatible accept values', function () { const { fakeElectron, fakeWindow } = createFakeElectron(); const backend = createElectronFileInputBackend(fakeElectron)(); fakeElectron.dialog.showSaveDialog.resolves({ canceled: true, }); backend.openFileChooser({ mode: 'save', multi: false, accept: '.json, .csv, application/octet-stream', }); expect(fakeElectron.dialog.showSaveDialog).to.have.been.calledWith( fakeWindow, { properties: ['openFile'], filters: [ { name: '.json file', extensions: ['json'] }, { name: '.csv file', extensions: ['csv'] }, ], } ); }); it('does not override existing file filters', function () { const { fakeElectron, fakeWindow } = createFakeElectron(); const backend = createElectronFileInputBackend(fakeElectron)(); fakeElectron.dialog.showSaveDialog.resolves({ canceled: true, }); backend.openFileChooser({ mode: 'save', filters: [{ name: 'CSV file', extensions: ['csv'] }], multi: false, accept: '.json, .csv, application/octet-stream', }); expect(fakeElectron.dialog.showSaveDialog).to.have.been.calledWith( fakeWindow, { properties: ['openFile'], filters: [ { name: 'CSV file', extensions: ['csv'] }, { name: '.json file', extensions: ['json'] }, ], } ); }); it('handles multi:false', function () { const { fakeElectron, fakeWindow } = createFakeElectron(); const backend = createElectronFileInputBackend(fakeElectron)(); fakeElectron.dialog.showSaveDialog.resolves({ canceled: true, }); backend.openFileChooser({ mode: 'save', properties: ['multiSelect'], multi: false, }); expect(fakeElectron.dialog.showSaveDialog).to.have.been.calledWith( fakeWindow, { properties: ['openFile'], filters: [], } ); }); it('handles multi:true', function () { const { fakeElectron, fakeWindow } = createFakeElectron(); const backend = createElectronFileInputBackend(fakeElectron)(); fakeElectron.dialog.showSaveDialog.resolves({ canceled: true, }); backend.openFileChooser({ mode: 'save', multi: true, }); expect(fakeElectron.dialog.showSaveDialog).to.have.been.calledWith( fakeWindow, { properties: ['openFile', 'multiSelect'], filters: [], } ); }); it('can call showOpenDialog if requested', async function () { const { fakeElectron, fakeWindow } = createFakeElectron(); const backend = createElectronFileInputBackend(fakeElectron)(); const listener = sinon.stub(); backend.onFilesChosen(listener); fakeElectron.dialog.showOpenDialog.resolves({ canceled: false, filePaths: ['a', 'b'], }); backend.openFileChooser({ mode: 'open', multi: false, }); expect(fakeElectron.dialog.showSaveDialog).to.not.have.been.called; expect(fakeElectron.dialog.showOpenDialog).to.have.been.calledWith( fakeWindow, { properties: ['openFile'], filters: [], } ); expect(listener).to.not.have.been.called; await tick(); expect(listener).to.been.calledOnceWith(['a', 'b']); }); it('calls the listener with an empty array if the user canceled the request', async function () { const { fakeElectron } = createFakeElectron(); const backend = createElectronFileInputBackend(fakeElectron)(); const listener = sinon.stub(); backend.onFilesChosen(listener); fakeElectron.dialog.showSaveDialog.resolves({ canceled: true, filePaths: ['a', 'b'], }); backend.openFileChooser({ mode: 'save', multi: false, }); expect(listener).to.not.have.been.called; await tick(); expect(listener).to.been.calledOnceWith([]); }); it('handles autoOpen:true', async function () { const { fakeElectron } = createFakeElectron(); const backend = createElectronFileInputBackend(fakeElectron)(); const listener = sinon.stub(); backend.onFilesChosen(listener); const openFileChooserSpy = sinon.spy(backend, 'openFileChooser'); fakeElectron.dialog.showSaveDialog.resolves({ canceled: false, filePaths: ['a'], }); expect(openFileChooserSpy).to.not.have.been.called; expect(spy).to.not.have.been.called; render( backend}> ); await tick(); expect(spy).to.been.calledOnce; expect(openFileChooserSpy).to.been.calledOnceWithExactly({ multi: false, mode: 'save', accept: undefined, title: undefined, defaultPath: undefined, filters: undefined, buttonLabel: undefined, properties: undefined, }); expect(fakeElectron.dialog.showSaveDialog).to.been.calledOnceWithExactly( {}, { properties: ['openFile'], filters: [], } ); expect(listener).to.been.calledOnceWith(['a']); }); describe('when the elector backend is provided by FileInputBackendProvider', function () { let listener: sinon.SinonStub; let showOpenDialogStub: sinon.SinonStub; let openFileChooserSpy: sinon.SinonSpy; beforeEach(async function () { const { fakeElectron } = createFakeElectron(); const backend = createElectronFileInputBackend(fakeElectron)(); listener = sinon.stub(); backend.onFilesChosen(listener); openFileChooserSpy = sinon.spy(backend, 'openFileChooser'); showOpenDialogStub = fakeElectron.dialog.showOpenDialog; fakeElectron.dialog.showOpenDialog.resolves({ canceled: false, filePaths: ['a.json', 'b.json'], }); render( backend}> ); await tick(); expect(spy).to.not.have.been.called; expect(openFileChooserSpy).to.not.have.been.called; expect(showOpenDialogStub).to.not.have.been.called; }); it('calls to open a file input', async function () { const selectFileButton = screen.getByTestId('file-input-button'); await waitFor(() => fireEvent.click(selectFileButton)); expect(spy).to.been.calledOnce; expect(openFileChooserSpy).to.been.calledOnceWithExactly({ multi: true, mode: 'open', title: 'file open title', defaultPath: '~/', accept: '.json, .csv', filters: undefined, buttonLabel: 'Open Sesame', properties: undefined, }); expect(showOpenDialogStub).to.been.calledOnceWithExactly( {}, { properties: ['openFile', 'multiSelect'], filters: [ { name: '.json file', extensions: ['json'] }, { name: '.csv file', extensions: ['csv'] }, ], title: 'file open title', defaultPath: '~/', buttonLabel: 'Open Sesame', } ); expect(listener).to.been.calledOnceWith(['a.json', 'b.json']); }); }); }); });