Repository: warpdesign/react-explorer
Pull request #137 (2020-04-27 13:08:51)

Fix copy subdir win


Patch:
diff --git a/src/components/Downloads.tsx b/src/components/Downloads.tsx
index 4ca3876..f28e6b4 100644
--- a/src/components/Downloads.tsx
+++ b/src/components/Downloads.tsx
@@ -12,6 +12,7 @@ import classnames from 'classnames';
 import { intentClass } from '@blueprintjs/core/lib/esm/common/classes';
 import { AppAlert } from './AppAlert';
 import CONFIG from '../config/appConfig'
+import { isWin } from '../utils/platform';
 
 const TYPE_ICONS: { [key: string]: IconName } = {
     'img': 'media',
@@ -244,7 +245,7 @@ class DownloadsClass extends React.Component<IProps, IState> {
 
             const intent = this.getIntent(transfer);
             const className = intentClass(intent);
-
+            const sep = isWin ? '\\' : '/';
             const node: ITreeNode =
             {
                 id: transfer.id,
@@ -265,7 +266,7 @@ class DownloadsClass extends React.Component<IProps, IState> {
                     node.childNodes.push({
                         id: id,
                         icon: file.file.isDir ? 'folder-close' : this.getFileIcon(filetype),
-                        label: file.subDirectory ? (file.subDirectory + '/' + file.file.fullname) : file.file.fullname,
+                        label: file.subDirectory ? (file.subDirectory + sep + file.file.fullname) : file.file.fullname,
                         secondaryLabel: this.createFileRightLabel(file),
                         nodeData: {
                             transfer: file,
diff --git a/src/components/SideView.tsx b/src/components/SideView.tsx
index 33d2e72..1b7339d 100644
--- a/src/components/SideView.tsx
+++ b/src/components/SideView.tsx
@@ -4,20 +4,17 @@ import { Toolbar } from './Toolbar';
 import { Statusbar } from './Statusbar';
 import { AppState } from "../state/appState";
 import { LoginDialog } from "./dialogs/LoginDialog";
-import { FileState } from "../state/fileState";
 import { Loader } from "./Loader";
 import { FileTable } from "./FileTable";
 import classnames from 'classnames';
 import { DropTargetSpec, DropTargetConnector, DropTargetMonitor, DropTargetCollector, ConnectDropTarget, DropTarget } from "react-dnd";
-import { DraggedObject } from './RowRenderer';
 import { TabList } from "./TabList";
 import { ViewState } from "../state/viewState";
 import { AppToaster } from "./AppToaster";
 import { Intent } from "@blueprintjs/core";
 import { getLocalizedError } from "../locale/error";
 import { withNamespaces, WithNamespaces } from "react-i18next";
-import { FsLocal, LocalApi } from "../services/plugins/FsLocal";
-import { getFS } from "../services/Fs";
+import { LocalApi } from "../services/plugins/FsLocal";
 
 interface SideViewProps extends WithNamespaces {
     hide: boolean;
diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx
index 6fac1f7..5040fbb 100644
--- a/src/components/Toolbar.tsx
+++ b/src/components/Toolbar.tsx
@@ -7,7 +7,7 @@ import { FileMenu } from "./FileMenu";
 import { MakedirDialog } from "./dialogs/MakedirDialog";
 import { AppAlert } from './AppAlert';
 import { Logger } from "./Log";
-import { AppToaster, IToasterOpts } from "./AppToaster";
+import { AppToaster } from "./AppToaster";
 import { withNamespaces, WithNamespaces, Trans } from "react-i18next";
 import { WithMenuAccelerators, Accelerators, Accelerator } from "./WithMenuAccelerators";
 import { throttle } from "../utils/throttle";
@@ -206,8 +206,9 @@ export class ToolbarClass extends React.Component<IProps, PathInputState> {
         try {
             const { viewState } = this.injected;
             const fileCache = viewState.getVisibleCache();
+            const cache = this.cache;
 
-            const deleted = await this.cache.delete(this.state.path, fileCache.selected);
+            const deleted = await cache.delete(this.state.path, fileCache.selected);
 
             // TODO: reset selection ?
             if (!deleted) {
@@ -217,7 +218,9 @@ export class ToolbarClass extends React.Component<IProps, PathInputState> {
                     // show warning
                     this.onDeleteError();
                 }
-                this.cache.reload();
+                if (cache.getFS().options.needsRefresh) {
+                    cache.reload();
+                }
             }
         } catch (err) {
             this.onDeleteError(err);
diff --git a/src/services/Fs.ts b/src/services/Fs.ts
index 09e3135..4b3a0b2 100644
--- a/src/services/Fs.ts
+++ b/src/services/Fs.ts
@@ -92,7 +92,7 @@ export function MakeId(stats: any): FileID {
 }
 
 function isModeExe(mode: number, gid: number, uid: number): Boolean {
-    if (isWin || mode === -1) {
+    if (isWin || mode === -1) {
         return false;
     }
 
@@ -125,7 +125,7 @@ export function filetype(mode: number, gid: number, uid: number, extension: stri
 export interface FsApi {
     // public API
     // async methods that may require server access
-    list(dir: string, transferId?: number): Promise<File[]>;
+    list(dir: string, watchDir?: boolean, transferId?: number): Promise<File[]>;
     cd(path: string, transferId?: number): Promise<string>;
     delete(parent: string, files: File[], transferId?: number): Promise<number>;
     makedir(parent: string, name: string, transferId?: number): Promise<string>;
diff --git a/src/services/plugins/FsGeneric.ts b/src/services/plugins/FsGeneric.ts
index 3d0398b..15395c8 100644
--- a/src/services/plugins/FsGeneric.ts
+++ b/src/services/plugins/FsGeneric.ts
@@ -86,7 +86,7 @@ class GenericApi implements FsApi {
         } as File);
     }
 
-    async list(dir: string, transferId = -1): Promise<File[]> {
+    async list(dir: string, watchDir = false, transferId = -1): Promise<File[]> {
         console.log("FsGeneric.readDirectory");
         const pathExists = await this.isDir(dir);
 
@@ -97,7 +97,7 @@ class GenericApi implements FsApi {
         }
     }
 
-    off() {}
+    off() { }
 
     isRoot(path: string): boolean {
         return path === "/";
@@ -129,7 +129,7 @@ class GenericApi implements FsApi {
     getParentTree(dir: string): Array<{ dir: string; fullname: string }> {
         const numParts = dir.replace(/^\//, "").split("/").length;
         const folders = [];
-        for (let i = 0; i < numParts; ++i) {}
+        for (let i = 0; i < numParts; ++i) { }
         return [];
     }
 
@@ -137,7 +137,7 @@ class GenericApi implements FsApi {
         return path;
     }
 
-    on(event: string, cb: (data: any) => void): void {}
+    on(event: string, cb: (data: any) => void): void { }
 }
 
 export const FsGeneric: Fs = {
diff --git a/src/services/plugins/FsLocal.ts b/src/services/plugins/FsLocal.ts
index 7a76c93..4b07fbc 100644
--- a/src/services/plugins/FsLocal.ts
+++ b/src/services/plugins/FsLocal.ts
@@ -30,7 +30,7 @@ export class LocalApi implements FsApi {
     loginOptions: ICredentials = null;
     onFsChange: (filename: string) => void;
 
-    constructor(path: string, onFsChange: (filename: string) => void) {
+    constructor(_: string, onFsChange: (filename: string) => void) {
         this.path = "";
         this.onFsChange = onFsChange;
     }
@@ -151,14 +151,14 @@ export class LocalApi implements FsApi {
                         });
                     }
                 })
-                .catch((err) => {
-                    reject({
-                        code: err.code,
-                        message: err.message,
-                        newName: newName,
-                        oldName: file.fullname
-                    });
-                })
+                    .catch((err) => {
+                        reject({
+                            code: err.code,
+                            message: err.message,
+                            newName: newName,
+                            oldName: file.fullname
+                        });
+                    })
             });
         } else {
             // reject promise with previous name in case of invalid chars
@@ -249,7 +249,7 @@ export class LocalApi implements FsApi {
         }
     }
 
-    async list(dir: string, transferId = -1): Promise<File[]> {
+    async list(dir: string, watchDir = false, transferId = -1): Promise<File[]> {
         try {
             await this.isDir(dir);
             return new Promise<File[]>((resolve, reject) => {
@@ -258,24 +258,24 @@ export class LocalApi implements FsApi {
                         reject(err);
                     } else {
                         const dirPath = path.resolve(dir);
-    
+
                         const files: File[] = [];
-    
+
                         for (var i = 0; i < items.length; i++) {
                             const file = LocalApi.fileFromPath(
                                 path.join(dirPath, items[i])
                             );
                             files.push(file);
                         }
-    
-                        this.onList(dirPath);
-    
+
+                        watchDir && this.onList(dirPath);
+
                         resolve(files);
                     }
                 });
             });
-        } catch(err) {
-            throw({
+        } catch (err) {
+            throw ({
                 code: err.code,
                 message: `Could not access path: ${dir}`
             });
@@ -310,7 +310,7 @@ export class LocalApi implements FsApi {
                 ctime: new Date(),
                 mtime: new Date(),
                 birthtime: new Date(),
-                size: stats.size,
+                size: stats ? stats.size : 0,
                 isDirectory: () => isDir,
                 mode: -1,
                 isSymbolicLink: () => isSymLink,
@@ -343,7 +343,7 @@ export class LocalApi implements FsApi {
                     filetype(mode, 0, 0, extension)) ||
                 "",
             isSym: stats.isSymbolicLink(),
-            target: stats.isSymbolicLink() && name || null,
+            target: stats.isSymbolicLink() && name || null,
             id: MakeId(stats)
         };
 
@@ -486,7 +486,7 @@ export class LocalApi implements FsApi {
         return isWin ? (path.match(/\\$/) ? path : path + "\\") : path;
     }
 
-    on(event: string, cb: (data: any) => void): void {}
+    on(event: string, cb: (data: any) => void): void { }
 }
 
 export function FolderExists(path: string) {
diff --git a/src/services/plugins/FsSimpleFtp.ts b/src/services/plugins/FsSimpleFtp.ts
index bc2af73..565e515 100644
--- a/src/services/plugins/FsSimpleFtp.ts
+++ b/src/services/plugins/FsSimpleFtp.ts
@@ -345,7 +345,7 @@ class SimpleFtpApi implements FsApi {
         } as File);
     }
 
-    list(path: string, transferId = -1): Promise<File[]> {
+    list(path: string, watchDir = false, transferId = -1): Promise<File[]> {
         return new Promise(async (resolve, reject) => {
             const newpath = this.pathpart(path);
 
diff --git a/src/services/plugins/LocalWatch.ts b/src/services/plugins/LocalWatch.ts
index ae647ce..ba6c72b 100644
--- a/src/services/plugins/LocalWatch.ts
+++ b/src/services/plugins/LocalWatch.ts
@@ -31,7 +31,7 @@ export const LocalWatch = {
     },
     createCallback(path: string) {
         // console.log('LocalWatch.createCallback', path);
-        return debounce((eventType: string, filename: string | Buffer) => {
+        return debounce((_: string, filename: string) => {
             // console.log('changeCallback', eventType, filename);
             const callbacks = this.getCallbacks(path);
             for (let cb of callbacks) {
diff --git a/src/state/appState.ts b/src/state/appState.ts
index dcce0d3..f9de9c7 100644
--- a/src/state/appState.ts
+++ b/src/state/appState.ts
@@ -135,15 +135,17 @@ export class AppState {
             debugger;
             if (
                 options.dstPath === cache.path &&
-                options.dstFsName === cache.getFS().name
+                options.dstFsName === cache.getFS().name &&
+                cache.getFS().options.needsRefresh
             ) {
-                // FIX ME: since watcher has been implemented, there's no need to reload cache
-                // if destination is local fs
                 cache.reload();
             }
 
             return res;
-        });
+        })
+            .catch(err => {
+                debugger;
+            })
     }
 
     /**
@@ -167,7 +169,7 @@ export class AppState {
         if (!srcCache) {
             srcPath = files[0].dir;
             const fs = getFS(srcPath);
-            srcApi = new fs.API(srcPath, () => {});
+            srcApi = new fs.API(srcPath, () => { });
         }
 
         const options = {
@@ -232,7 +234,7 @@ export class AppState {
         } else {
             // first we need to get a FS for local
             const fs = getFS(DOWNLOADS_DIR);
-            const api = new fs.API(DOWNLOADS_DIR, () => {});
+            const api = new fs.API(DOWNLOADS_DIR, () => { });
 
             const options = {
                 files,
@@ -339,7 +341,7 @@ export class AppState {
             options.dstFs,
             options.dstPath
         );
-        debugger;
+
         const batch = new Batch(
             options.srcFs,
             options.dstFs,
diff --git a/src/state/fileState.ts b/src/state/fileState.ts
index f8bb781..3eb6332 100644
--- a/src/state/fileState.ts
+++ b/src/state/fileState.ts
@@ -4,7 +4,6 @@ import { Deferred } from '../utils/deferred';
 import { i18next } from '../locale/i18n';
 import { getLocalizedError } from '../locale/error';
 import { shell, ipcRenderer } from 'electron';
-import * as process from 'process';
 import { AppState } from "./appState";
 import { TSORT_METHOD_NAME, TSORT_ORDER } from "../services/FsSort";
 
@@ -438,7 +437,7 @@ export class FileState {
     @action
     @needsConnection
     async list(path: string): Promise<File[]> {
-        return this.api.list(path)
+        return this.api.list(path, true)
             .catch(this.handleError)
     }
 
@@ -540,7 +539,7 @@ export class FileState {
         }
     }
 
-    shellOpenFile(path: string):boolean {
+    shellOpenFile(path: string): boolean {
         console.log('need to open file', path);
         return shell.openItem(path);
     }
diff --git a/src/transfers/batch.ts b/src/transfers/batch.ts
index d200844..3f2aa7d 100644
--- a/src/transfers/batch.ts
+++ b/src/transfers/batch.ts
@@ -5,6 +5,7 @@ import { Deferred } from "../utils/deferred";
 import { getLocalizedError } from "../locale/error";
 import { Readable } from "stream";
 import { getSelectionRange } from "../utils/fileUtils";
+import { isWin } from "../utils/platform";
 
 const MAX_TRANSFERS = 2;
 const MAX_ERRORS = 5;
@@ -79,7 +80,7 @@ export class Batch {
 
     @action
     start(): Promise<boolean> {
-        console.log('starting batch');
+        console.log('Starting batch');
         if (this.status === 'queued') {
             this.slotsAvailable = MAX_TRANSFERS;
             this.status = 'started';
@@ -102,7 +103,8 @@ export class Batch {
     @action
     updatePendingTransfers(subDir: string, newFilename: string, cancel = false) {
         // TODO: escape '(' & ')' in subDir if needed
-        const regExp = new RegExp('^(' + subDir + ')');
+        const escapedSubDir = isWin ? subDir.replace(/\\/g, '\\\\') : subDir;
+        const regExp = new RegExp(`^(${escapedSubDir})`);
         const files = this.files.filter((file) => file.subDirectory.match(regExp) !== null);
 
         // destination directory for these files could not be created: we cancel these transfers
@@ -190,9 +192,9 @@ export class Batch {
         let stream = null;
 
         try {
-            if (transfer.file.isSym) {
-                debugger;
-            }
+            // if (transfer.file.isSym) {
+            //     debugger;
+            // }
             newFilename = await this.renameOrCreateDir(transfer, fullDstPath);
         } catch (err) {
             console.log('error creating directory', err);
@@ -207,21 +209,19 @@ export class Batch {
             if (isSym) {
                 const linkPath = dstFs.join(fullDstPath, newFilename)
                 try {
-                    debugger;
                     await srcFs.makeSymlink(transfer.file.target, linkPath, this.id)
                     transfer.status = 'done';
-                } catch(err) {
+                } catch (err) {
                     this.onTransferError(transfer, err);
-                    debugger;
                 }
             } else {
                 try {
-                    if (transfer.file.isSym) {
-                        debugger;
-                    }
+                    // if (transfer.file.isSym) {
+                    //     debugger;
+                    // }
                     // console.log('getting stream', srcPath, wantedName);
                     stream = await srcFs.getStream(srcPath, wantedName, this.id);
-    
+
                     this.streams.push(stream);
                     // console.log('sending to stream', dstFs.join(fullDstPath, newFilename));
                     // we have to listen for errors that may appear during the transfer: socket closed, timeout,...
@@ -233,11 +233,12 @@ export class Batch {
                     //     // destroy stream so that put stream resolves ?
                     //     stream.destroy();
                     // });
-    
+
                     await dstFs.putStream(stream, dstFs.join(fullDstPath, newFilename), (bytesRead: number) => {
                         this.onData(transfer, bytesRead);
                     }, this.id);
-    
+
+                    console.log('endPutStream', fullDstPath);
                     this.removeStream(stream);
                     transfer.status = 'done';
                 } catch (err) {
@@ -253,13 +254,14 @@ export class Batch {
                     //}
                     this.onTransferError(transfer, err);
                     if (this.errors > MAX_ERRORS) {
-                        console.warn('maximum errors occurred: cancelling upcoming file transfers');
+                        console.warn('Maximum errors occurred: cancelling upcoming file transfers');
                         this.status = 'error';
                         this.cancelFiles();
                     }
                 }
             }
         } else {
+            // console.log('isDir', fullDstPath);
             transfer.status = 'done';
             // make transfers with this directory ready
             this.updatePendingTransfers(srcFs.join(transfer.subDirectory, wantedName), newFilename, (transfer as FileTransfer).status !== 'done');
@@ -275,23 +277,11 @@ export class Batch {
         if (this.status !== 'error' && this.transfersDone < this.files.length) {
             this.queueNextTransfers();
         } else {
-            for (let transfer of this.files) {
-                console.log(transfer.status, transfer);
-            }
-            // console.log('no more transfers !!');
             if (this.numErrors === this.files.length) {
-                for (let transfer of this.files) {
-                    console.log(transfer.status, transfer.file.fullname, transfer);
-                }
-
                 this.transferDef.reject({
                     code: ''
                 });
             } else {
-                for (let transfer of this.files) {
-                    console.log(transfer.status, transfer.file.fullname, transfer);
-                }
-
                 this.transferDef.resolve();
             }
         }
@@ -480,6 +470,7 @@ export class Batch {
     async setFileList(files: File[]) {
         return this.getFileList(files).then((transfers) => {
             console.log('got files', transfers);
+            // transfers.forEach(t => console.log(`[${t.status}] ${t.file.dir}/${t.file.fullname}:${t.file.isDir}, ${t.subDirectory} (${t.newSub})`))
             this.files.replace(transfers);
         }).catch((err) => {
             return Promise.reject(err);


Test patch:
diff --git a/src/services/plugins/__tests__/FsLocal.test.ts b/src/services/plugins/__tests__/FsLocal.test.ts
index 255a31f..b2ae08d 100644
--- a/src/services/plugins/__tests__/FsLocal.test.ts
+++ b/src/services/plugins/__tests__/FsLocal.test.ts
@@ -1,9 +1,11 @@
 import { FsLocal } from "../FsLocal";
 import * as fs from 'fs';
+import { sep } from 'path';
 
 import {
     describeUnix,
     describeWin,
+    itUnix,
     getPath,
 } from "../../../utils/test/helpers";
 
@@ -21,122 +23,122 @@ const TESTS_DIR = getPath("tests_dir");
 console.log('');
 
 describe('FsLocal', () => {
-    describeUnix("isDirectoryNameValid", function() {
+    describeUnix("isDirectoryNameValid", function () {
         beforeAll(() => {
-            localAPI = new FsLocal.API("/", () => {});
+            localAPI = new FsLocal.API("/", () => { });
         });
-    
-        it("unix: dir name cannot start with  ../", function() {
+
+        it("unix: dir name cannot start with  ../", function () {
             const isValid = localAPI.isDirectoryNameValid("../");
-    
+
             expect(isValid).toBe(false);
         });
-    
-        it("unix: dir name cannot start with  ./", function() {
+
+        it("unix: dir name cannot start with  ./", function () {
             const isValid = localAPI.isDirectoryNameValid("./");
-    
+
             expect(isValid).toBe(false);
         });
-    
-        it("unix: dir name cannot start with  ../.", function() {
+
+        it("unix: dir name cannot start with  ../.", function () {
             const isValid = localAPI.isDirectoryNameValid("../.");
-    
+
             expect(isValid).toBe(false);
         });
     });
-    
+
     // @todo add Windows specific tests for isDirectoryNameValid
-    describe("resolve", function() {
+    describe("resolve", function () {
         beforeAll(() => {
-            localAPI = new FsLocal.API("/", () => {});
+            localAPI = new FsLocal.API("/", () => { });
         });
-    
-        it("~ resolves to homedir", function() {
+
+        it("~ resolves to homedir", function () {
             const dir = localAPI.resolve("~");
-    
+
             expect(dir).toBe(HOMEDIR);
         });
     });
-    
-    describe("cd", function() {
+
+    describe("cd", function () {
         const filePath = `${TESTS_DIR}/file`
 
         beforeAll(() => {
             mock(TmpDir);
-            localAPI = new FsLocal.API("/", () => {});
+            localAPI = new FsLocal.API("/", () => { });
         });
 
         afterAll(() => mock.restore());
-    
-        it("should throw ENOTDIR if is not a directory", async function() {
+
+        it("should throw ENOTDIR if is not a directory", async function () {
             expect.assertions(1);
-    
+
             try {
                 await localAPI.cd(filePath);
             } catch (err) {
                 expect(err.code).toBe("ENOTDIR");
             }
         });
-    
-        it("should throw if does not exist", async function() {
+
+        it("should throw if does not exist", async function () {
             expect.assertions(1);
-    
+
             try {
                 await localAPI.cd(`${filePath}__`);
             } catch (err) {
                 expect(err.code).toBe("ENOENT");
             }
         });
-    
-        it("should return resolved path if exists and is a dir", function() {
+
+        it("should return resolved path if exists and is a dir", function () {
             const dir = localAPI.resolve(TESTS_DIR);
-    
+
             expect(dir).toBe(TESTS_DIR);
         });
-    
-        it("should return resolved homedir path for ~", function() {
+
+        it("should return resolved homedir path for ~", function () {
             expect.assertions(1);
-    
+
             const dir = localAPI.resolve("~");
-    
+
             expect(dir).toBe(HOMEDIR);
         });
     });
-    
-    describe("size", function() {
+
+    describe("size", function () {
         const SIZE_PATH = `${TESTS_DIR}/sizeTest`;
-    
+
         beforeAll(() => {
             mock(TmpDir);
-            localAPI = new FsLocal.API("/", () => {});
+            localAPI = new FsLocal.API("/", () => { });
         });
 
         afterAll(() => mock.restore());
-    
+
         it("should return 0 if no files", async () => {
             expect.assertions(1);
-    
+
             const size = await localAPI.size(__dirname, []);
             expect(size).toBe(0);
         });
-    
+
         it("should return correct size for 14 bytes file", async () => {
             expect.assertions(1);
-    
+
             const size = await localAPI.size(SIZE_PATH, ["14bytes"]);
             expect(size).toBe(14);
         });
-    
+
         it("should return correct size for several 14 + 1024 files", async () => {
             expect.assertions(1);
-    
+
             const size = await localAPI.size(SIZE_PATH, ["14bytes", "1024bytes"]);
             expect(size).toBe(1038);
         });
-    
+
         it("should throw ENOENT if file does not exist", async () => {
             expect.assertions(1);
-    
+
             try {
                 await localAPI.size(SIZE_PATH, ["15bytes"]);
             } catch (err) {
@@ -144,20 +146,20 @@ describe('FsLocal', () => {
             }
         });
     });
-    
-    describe("makedir", function() {
-        const MAKEDIR_PATH = `${TESTS_DIR}/makedirTest`;
-    
+
+    describe("makedir", function () {
+        const MAKEDIR_PATH = `${TESTS_DIR}${sep}makedirTest`;
+
         beforeAll(() => {
-            localAPI = new FsLocal.API("/", () => {});
+            localAPI = new FsLocal.API("/", () => { });
             mock(TmpDir);
         });
-    
+
         afterAll(() => mock.restore());
 
         it("should return success if folder already exists", async () => {
             expect.assertions(1);
-    
+
             const resolved = await localAPI.makedir(MAKEDIR_PATH, "");
             expect(resolved).toBe(MAKEDIR_PATH);
         });
@@ -173,39 +175,39 @@ describe('FsLocal', () => {
         //         expect(err.code).toMatch(/EACCES|EROFS/);
         //     }
         // });
-    
+
         it("should throw EACCES if source is not readable", async () => {
             expect.assertions(1);
-    
+
             try {
                 await localAPI.makedir(`${MAKEDIR_PATH}/denied`, "foo");
             } catch (err) {
                 expect(err.code).toBe("EACCES");
             }
         });
-    
+
         it("should create a single folder", async () => {
             expect.assertions(1);
-    
+
             const resolved = await localAPI.makedir(MAKEDIR_PATH, "dir1");
-    
-            expect(resolved).toBe(`${MAKEDIR_PATH}/dir1`);
+
+            expect(resolved).toBe(`${MAKEDIR_PATH}${sep}dir1`);
         });
-    
+
         it("should create several folders", async () => {
             expect.assertions(1);
-    
+
             const resolved = await localAPI.makedir(MAKEDIR_PATH, "dir2/dir3/dir4");
-    
-            expect(resolved).toBe(`${MAKEDIR_PATH}/dir2/dir3/dir4`);
+
+            expect(resolved).toBe(`${MAKEDIR_PATH}${sep}dir2${sep}dir3${sep}dir4`);
         });
     });
-    
+
     describe("makeSymLink", () => {
         const filePath = `${TESTS_DIR}/file`
 
         beforeAll(() => {
-            localAPI = new FsLocal.API("/", () => {});
+            localAPI = new FsLocal.API("/", () => { });
             mock(TmpDir);
         });
 
@@ -230,7 +232,7 @@ describe('FsLocal', () => {
 
             try {
                 await localAPI.makeSymlink(filePath, '/non/valid');
-            } catch(err) {
+            } catch (err) {
                 expect(err.code).toBe('ENOENT');
             }
         });
@@ -240,7 +242,7 @@ describe('FsLocal', () => {
 
             try {
                 await localAPI.makeSymlink(filePath, `${TESTS_DIR}/makedirTest/denied/link`);
-            } catch(err) {
+            } catch (err) {
                 expect(err.code).toBe('EACCES');
             }
         });
@@ -250,7 +252,7 @@ describe('FsLocal', () => {
 
             try {
                 const res = await localAPI.makeSymlink(filePath, `${TESTS_DIR}/file`);
-            } catch(err) {
+            } catch (err) {
                 expect(err.code).toBe('EEXIST');
             }
         });
@@ -258,18 +260,18 @@ describe('FsLocal', () => {
 
     describe('rename', () => {
         beforeAll(() => {
-            localAPI = new FsLocal.API("/", () => {});
+            localAPI = new FsLocal.API("/", () => { });
             mock(TmpDir);
         });
 
         afterAll(() => mock.restore());
-        
+
         it('should throw EEXIST if destination already exists', async () => {
             expect.assertions(1);
 
             try {
                 await localAPI.rename(`${TESTS_DIR}/sizeTest`, { fullname: '14bytes' }, '1024bytes');
-            } catch(err) {
+            } catch (err) {
                 expect(err.code).toBe('EEXIST');
             }
         });
@@ -279,7 +281,7 @@ describe('FsLocal', () => {
 
             try {
                 await localAPI.rename(`${TESTS_DIR}/sizeTest`, { fullname: 'nothing_here' }, 'new_file');
-            } catch(err) {
+            } catch (err) {
                 expect(err.code).toBe('ENOENT');
             }
         });
@@ -289,7 +291,7 @@ describe('FsLocal', () => {
 
             try {
                 await localAPI.rename(`${TESTS_DIR}/deleteTest/folder_denied`, { fullname: 'file2' }, 'new_file');
-            } catch(err) {
+            } catch (err) {
                 expect(err.code).toBe('EACCES');
             }
         });
@@ -311,12 +313,12 @@ describe('FsLocal', () => {
 
     describe('isDir', () => {
         beforeAll(() => {
-            localAPI = new FsLocal.API("/", () => {});
+            localAPI = new FsLocal.API("/", () => { });
             mock(TmpDir);
         });
 
         afterAll(() => mock.restore());
-        
+
         it('should return true if file is a directory', async () => {
             expect.assertions(1);
             const res = await localAPI.isDir(TESTS_DIR);
@@ -344,7 +346,7 @@ describe('FsLocal', () => {
             expect.assertions(1);
             try {
                 await localAPI.isDir(`${TESTS_DIR}/not_here`);
-            } catch(err) {
+            } catch (err) {
                 expect(err.code).toBe('ENOENT');
             }
         });
@@ -364,7 +366,7 @@ describe('FsLocal', () => {
 
     describe('exists', () => {
         beforeAll(() => {
-            localAPI = new FsLocal.API("/", () => {});
+            localAPI = new FsLocal.API("/", () => { });
             mock(TmpDir);
         });
 
@@ -385,7 +387,7 @@ describe('FsLocal', () => {
 
     describe('stat', () => {
         beforeAll(() => {
-            localAPI = new FsLocal.API("/", () => {});
+            localAPI = new FsLocal.API("/", () => { });
             mock(TmpDir);
         });
 
@@ -449,7 +451,7 @@ describe('FsLocal', () => {
 
             try {
                 await localAPI.stat(path);
-            } catch(err) {
+            } catch (err) {
                 expect(err.code).toBe('ENOENT');
             }
         });
@@ -469,8 +471,8 @@ describe('FsLocal', () => {
 
     describe('list', () => {
         beforeAll(() => {
-            localAPI = new FsLocal.API("/", () => {});
-            jest.spyOn(localAPI, 'onList').mockImplementation(() => {});
+            localAPI = new FsLocal.API("/", () => { });
+            jest.spyOn(localAPI, 'onList').mockImplementation(() => { });
             mock(TmpDir);
         });
 
@@ -484,17 +486,16 @@ describe('FsLocal', () => {
             expect.assertions(1);
             try {
                 await localAPI.list('/nothing');
-            } catch(err) {
+            } catch (err) {
                 expect(err.code).toBe('ENOENT');
             }
         });
 
-        // TODO: should have called onList(dir)
         it('should return every files found in path if it exists', async () => {
             expect.assertions(1);
-            
+
             const files = await localAPI.list(TESTS_DIR);
-            
+
             expect(files.length).toBe(6);
         });
 
@@ -502,33 +503,42 @@ describe('FsLocal', () => {
             expect.assertions(1);
             try {
                 await localAPI.list(`${TESTS_DIR}/deleteTest/file`);
-            } catch(err) {
+            } catch (err) {
                 expect(err.code).toBe('ENOTDIR');
             }
         });
 
-        it('should throw EACCES if cannot access folder', async () => {
+        // mock-fs doesn't check for mode access on Windows
+        itUnix('should throw EACCES if cannot access folder', async () => {
             expect.assertions(1);
             try {
                 await localAPI.list(`${TESTS_DIR}/deleteTest/folder_denied`);
-            } catch(err) {
+            } catch (err) {
                 expect(err.code).toBe('EACCES');
             }
         });
 
-        it('should call onList with path when listing a valid directory', async () => {
+        it('should call onList with path when listing a valid directory and watchDir is true', async () => {
             expect.assertions(2);
-            
-            await localAPI.list(TESTS_DIR);
-            
+
+            await localAPI.list(TESTS_DIR, true);
+
             expect(localAPI.onList).toHaveBeenCalledTimes(1);
             expect(localAPI.onList).toHaveBeenCalledWith(TESTS_DIR);
         });
+
+        it('should not call onList with path when listing a valid directory and watchDir is false', async () => {
+            expect.assertions(1);
+
+            await localAPI.list(TESTS_DIR, false);
+
+            expect(localAPI.onList).not.toHaveBeenCalled();
+        });
     });
 
     describeUnix('isRoot', () => {
         beforeAll(() => {
-            localAPI = new FsLocal.API("/", () => {});
+            localAPI = new FsLocal.API("/", () => { });
         });
 
         it('should return false for empty path', () => {
@@ -546,7 +556,7 @@ describe('FsLocal', () => {
 
     describeWin('isRoot', () => {
         beforeAll(() => {
-            localAPI = new FsLocal.API("C:\\", () => {});
+            localAPI = new FsLocal.API("C:\\", () => { });
         });
 
         it('should return false for empty path', () => {
@@ -566,12 +576,12 @@ describe('FsLocal', () => {
         });
 
         it('should return false if path === "\\\\foo"', () => {
-            expect(localAPI.isRoot('\\\\foo')).toBe(true);
+            expect(localAPI.isRoot('\\\\foo')).toBe(false);
         });
     });
     // // describe("delete", function() {
     //     const DELETE_PATH = `${TESTS_DIR}/deleteTest`;
-    
+
     //     beforeAll(() => {
     //         localAPI = new FsLocal.API("/", () => {});
     //         return prepareTmpTestFiles();
@@ -579,14 +589,14 @@ describe('FsLocal', () => {
 
     //     it('should resolve if file/folder does not exist', async () => {
     //         expect.assertions(1);
-    
+
     //         const deleted = await localAPI.delete(`${DELETE_PATH}`, [{
     //             fullname: 'not_here'
     //         }]);
-    
+
     //         expect(deleted).toBe(1);
     //     });
-    
+
     //     it('should throw EACCESS if file/folder is write protected', async () => {
     //         expect.assertions(1);
     //         process.stdout.write('ro');
@@ -594,17 +604,17 @@ describe('FsLocal', () => {
     //         // const deleted = await localAPI.delete(`${DELETE_PATH}`, [{
     //         //     fullname: 'file_denied'
     //         // }]);
-    
+
     //         // expect(deleted).toBe(2);
     //         expect(1).toBe(2);
     //     });
-    
+
     //     it("should delete single file and resolve if success", () => {});
-    
+
     //     it("should delete empty folder and resolve if success", () => {});
-    
+
     //     it("should delete not empty folder and resolve if success", () => {});
-    
+
     //     it("should delete not empty folder + files and resolve", () => {});
     // });
 })
diff --git a/src/utils/test/helpers.ts b/src/utils/test/helpers.ts
index 776b0ad..84c7005 100644
--- a/src/utils/test/helpers.ts
+++ b/src/utils/test/helpers.ts
@@ -1,14 +1,14 @@
 import { platform } from 'process';
 import { homedir, tmpdir } from 'os';
-import { mkdirSync, writeFileSync, existsSync } from 'fs';
-import { execSync } from 'child_process';
+import { sep } from 'path';
 
 // expects describe to be globally defined: should only be used from test environment
 declare var describe: any;
+declare var it: any;
 
 // assume Unix in that case: may need some tweaks for some exotic platform
 const isUnix = platform !== 'win32';
-const TEST_FILES_DIR = tmpdir() + '/react-explorer-tests';
+const TEST_FILES_DIR = tmpdir() + sep + 'react-explorer-tests';
 
 // call this to perform unix specific tests
 export const describeUnix = (...args: any) => {
@@ -30,6 +30,16 @@ export const describeWin = (...args: any) => {
     }
 };
 
+// call this to perform unix specific tests
+export const itUnix = (...args: any) => {
+    args[0] += ' (Unix)';
+    if (isUnix) {
+        it(...args);
+    } else {
+        it.skip(...args);
+    }
+};
+
 export const getPath = (id: string) => {
     switch (id) {
         case 'home':
@@ -45,65 +55,3 @@ export const getPath = (id: string) => {
             return tmpdir();
     }
 }
-
-function createEmptyFile(path: string, bytes = 0, mode = 0o777) {
-    // console.log('creating file', path, bytes);
-    writeFileSync(path, Buffer.alloc(bytes), {
-        mode
-    });
-}
-
-// create some test files/directories for testing local fs functions
-// @todo handle windows style separators
-export const prepareTmpTestFiles = () => {
-    return new Promise((resolve, reject) => {
-        // console.log('tmpdir', TEST_FILES_DIR);
-        const tmpSizeDir = `${TEST_FILES_DIR}/sizeTest`;
-        const tmpMakedirDir = `${TEST_FILES_DIR}/makedirTest`
-        const tmpDeleteDir = `${TEST_FILES_DIR}/deleteTest`
-
-        // delete tmpdir if it exits
-        // console.log('deleting dir', TEST_FILES_DIR);
-        // first make this folder readable so that it can be removed
-        if (existsSync(`${tmpMakedirDir}/denied`)) {
-            execSync(`chmod 777 ${tmpMakedirDir}/denied`);
-        }
-
-        if (existsSync(`${tmpDeleteDir}/folder_denied`)) {
-            execSync(`chmod 777 ${tmpDeleteDir}/folder_denied`);
-        }
-
-        if (existsSync(`${tmpDeleteDir}/file_denied`)) {
-            execSync(`chmod 777 ${tmpDeleteDir}/file_denied`);
-        }
-
-        if (existsSync(TEST_FILES_DIR)) {
-            execSync(`rm -rf ${TEST_FILES_DIR}`);
-        }
-
-        // console.log('creating dir', TEST_FILES_DIR);
-        // recreate directory
-        mkdirSync(TEST_FILES_DIR);
-
-        // size test
-        mkdirSync(tmpSizeDir);
-        createEmptyFile(`${tmpSizeDir}/14bytes`, 14);
-        createEmptyFile(`${tmpSizeDir}/1024bytes`, 1024);
-
-        // makedir test
-        mkdirSync(tmpMakedirDir);
-        mkdirSync(`${tmpMakedirDir}/denied`, 0o000);
-
-        // delete test
-        mkdirSync(tmpDeleteDir);
-        mkdirSync(`${tmpDeleteDir}/folder_denied`, 0o000);
-
-        mkdirSync(`${tmpDeleteDir}/single_folder`);
-        mkdirSync(`${tmpDeleteDir}/multiple_folder`);
-
-        createEmptyFile(`${tmpDeleteDir}/file`, 1024);
-        createEmptyFile(`${tmpDeleteDir}/file_denied`, 1024, 0o000);
-
-        resolve();
-    });
-}
\ No newline at end of file