Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Cross-platform Desktop Application Development: Electron, Node, NW.js, and React

You're reading from   Cross-platform Desktop Application Development: Electron, Node, NW.js, and React Build desktop applications with web technologies

Arrow left icon
Product type Paperback
Published in Jul 2017
Publisher Packt
ISBN-13 9781788295697
Length 300 pages
Edition 1st Edition
Languages
Arrow right icon
Author (1):
Arrow left icon
Dmitry Sheiko Dmitry Sheiko
Author Profile Icon Dmitry Sheiko
Dmitry Sheiko
Arrow right icon
View More author details
Toc

Table of Contents (9) Chapters Close

Preface 1. Creating a File Explorer with NW.js-Planning, Designing, and Development FREE CHAPTER 2. Creating a File Explorer with NW.js – Enhancement and Delivery 3. Creating a Chat System with Electron and React – Planning, Designing, and Development 4. Creating a Chat System with Electron and React – Enhancement, Testing, and Delivery 5. Creating a Screen Capturer with NW.js, React, and Redux – Planning, Design, and Development 6. Creating a Screen Capturer with NW.js: Enhancement, Tooling, and Testing 7. Creating RSS Aggregator with Electron, TypeScript , React, and Redux: Planning, Design, and Development 8. Creating RSS Aggregator with Electron, TypeScript, React, and Redux: Development

Writing a service to navigate through directories

Other modules, such as FileListView, DirListView, and TitleBarPath, consume the data from the filesystem, such as directory list, file list, and the current path. So we need to create a service that will provide this data:

./js/Service/Dir.js

const fs = require( "fs" ), 
{ join, parse } = require( "path" );

class DirService {

constructor( dir = null ){
this.dir = dir || process.cwd();
}

static readDir( dir ) {
const fInfoArr = fs.readdirSync( dir, "utf-8" ).map(( fileName ) => {
const filePath = join( dir, fileName ),
stats = DirService.getStats( filePath );
if ( stats === false ) {
return false;
}
return {
fileName,
stats
};
});
return fInfoArr.filter( item => item !== false );
}

getDirList() {
const collection = DirService.readDir( this.dir ).filter(( fInfo )
=> fInfo.stats.isDirectory() );
if ( !this.isRoot() ) {
collection.unshift({ fileName: ".." });
}
return collection;
}

getFileList() {
return DirService.readDir( this.dir ).filter(( fInfo ) =>
fInfo.stats.isFile() );
}

isRoot(){
const { root } = parse( this.dir );
return ( root === this.dir );
}

static getStats( filePath ) {
try {
return fs.statSync( filePath );
} catch( e ) {
return false;
}
}

};

exports.DirService = DirService;

First of all, we import Node.js core module fs that provides us access to the filesystem. We also extract functions--join and parse--from the path module. We will need them for manipulations in the file/directory path.

Then, we declare the DirService class. On construction, it creates a dir property, which takes either a passed-in value or the current working directory (process.cwd()). We add a static method--readDir--to the class that reads the directory content on a given location. The fs.readdirSync method retrieves the content of a directory, but we extend the payload with file/directory stats (https://nodejs.org/api/fs.html#fs_class_fs_stats). In case the stats cannot be obtained, we replace its array element with false. To avoid such gaps in the output array, we will run the array filter method. Thus, on the exit point, we have a clean array of filenames and file stats.

The getFileList method requests readDir for the current directory content and filters the list to leave only files in there.

The getDirList method filters, evidently, the list for directories only. Besides, it prepends the list with a .. directory for upward navigation, but only if we are not in the system root.

So, we can get both lists from the modules consuming them. When the location changes and new directory and file lists get available, each of these modules have to update. To implement it, we will use the observe pattern:

./js/Service/Dir.js

//.... 
const EventEmitter = require( "events" );

class DirService extends EventEmitter {

constructor( dir = null ){
super();
this.dir = dir || process.cwd();
}
setDir( dir = "" ){
let newDir = path.join( this.dir, dir );
// Early exit
if ( DirService.getStats( newDir ) === false ) {
return;
}
this.dir = newDir;
this.notify();
}

notify(){
this.emit( "update" );
}
//...
}

We export from events, core module the EventEmitter class (https://nodejs.org/api/events.html). By extending it with DirService, we make the service an event emitter. It gives us the possibility to fire service events and to subscribe on them:

dirService.on( "customEvent", () => console.log( "fired customEvent" )); 
dirService.emit( "customEvent" );

So whenever the setDir method is called to change the current location, it fires an event of type "update". Given the consuming modules are subscribed, they respond to the event by updating their views.

Unit-testing a service

We've written a service and assume that it fulfills the functional requirements, but we do not know it for sure, yet. To check it, we will create a unit-test.

We do not have any test environment so far. I would suggest going with the Jasmine test framework (https://jasmine.github.io/). We will create in our tests/unit-tests subfolder a dedicated NW.js project, which will be used for the testing. This way, we get the runtime environment for tests, identical to what we have in the application.

So we create the test project manifest:

./tests/unit-tests/package.json

{ 
"name": "file-explorer",
"main": "specs.html",
"chromium-args": "--mixed-context"
}

It points at the Jasmine test runner page, the one we placed next to package.json:

./tests/unit-tests/specs.html

<!doctype html> 
<html>
<head>
<meta charset="utf-8">
<title>Jasmine Spec Runner</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine-
html.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/boot.js"></script>
</head>
<body>
<div id="sandbox" style="display: none"></div>
<script>
// Catch exception and report them to the console.
process.on( "uncaughtException", ( err ) => console.error( err ) );
const path = require( "path" ),
jetpack = require( "fs-jetpack" ),
matchingSpecs = jetpack.find( "../../js", {
matching: [
"*.spec.js",
"!node_modules/**"
]
}, "relativePath" );

matchingSpecs.forEach(( file ) => {
require( path.join( __dirname, file ) );
});
</script>
</body>
</html>

What does this runner do? It loads Jasmine, and with help of the fs-jetpack npm module (https://www.npmjs.com/package/fs-jetpack), it traverses the source directory recursively for all the files matching "*.spec.js" pattern. All these files get added to the test suite. Thus, it assumes that we keep our test specifications next to the target source modules.

fs-jetpack is an external module, and we need to install the package and add it to the development dependencies list:

npm i -D fs-jetpack

Jasmine implements a wide-spread, frontend development testing paradigm Behavior-driven Development (BDD) that can be described with the following pattern:

 
describe( "a context e.g. class or module", () => {
describe( "a context e.g. method or function", () => {
it( "does what expected", () => {
expect( returnValue ).toBe( expectedValue );
});
});
});

As it is generally accepted in unit testing, a suite may have setup and teardown:

beforeEach(() => { 
// something to run before to every test
});
afterEach(() => {
// something to run after to every test
});

When testing a service that touches the filesystem or communicates across the network or talks to databases, we have to be careful. A good unit test is independent from the environment. So, to test our DirService, we have to mock the filesystem. Let's test the getFileList method of the service class to see it in action:

./js/Service/Dir.spec.js

const { DirService } = require( "./Dir" ), 
CWD = process.cwd(),
mock = require( "mock-fs" ),
{ join } = require( "path" );

describe( "Service/Dir", () => {

beforeEach(() => {
mock({
foo: {
bar: {
baz: "baz", // file contains text baz
qux: "qux"
}
}
});
});
afterEach( mock.restore );

describe( "#getFileList", () => {
it( "receives intended file list", () => {
const service = new DirService( join( "foo", "bar" ) );
service.setDir( "bar" );
let files = service.getFileList();
expect( files.length ).toBe( 2 );
});
it( "every file has expected properties", () => {
const service = new DirService( join( "foo", "bar" ) );
const files = service.getFileList();
console.log( files );
const [ file ] = files;
expect( file.fileName ).toBe( "baz" );
expect( file.stats.size ).toBe( 3 );
expect( file.stats.isFile() ).toBe( true );
expect( file.stats.isDirectory() ).toBe( false );
expect( file.stats.mtime ).toBeTruthy();
});
});
});

Before running a test, we point the fs method to a virtual filesystem with the folder /foo/bar/ that contains the baz and qux files. After every test, we restore access to the original filesystem. In the first test, we instantiate the service on the foo/bar location and read the content with the getFileList() method. We assert the number of found files as 2 (as we defined in beforeEach). In the second test, we take the first element of the list and assert that it contains the intended filename and stats.

As we use an external npm package (https://www.npmjs.com/package/mock-fs) for mocking, we need to install it:

npm i -D mock-fs

As we came up with the first test suite, we can modify our project manifest file for a proper test runner script. The ./package.json file contains the following code:

{ 
...
"scripts": {
...
"test": "nw tests/unit-tests"
},
...
}

Now, we can run the tests:

npm test

NW.js will load and render the following screen:

Ideally, unit tests cover all the available functions/methods in the context. I believe that from the preceding code you will get an idea of how to write the tests. However, you may stumble over testing the EventEmitter interface; consider this example:

 
describe( "#setDir", () => {
it( "fires update event", ( done ) => {
const service = new DirService( "foo" );
service.on( "update", () => {
expect( true ).toBe( true );
done();
});
service.notify();
});
});

EventEmitter works asynchronously. When we have asynchronous calls in the test body, we shall explicitly inform Jasmin when the test is ready so that the framework could proceed to the next one. That happens when we invoke the callback passed to its function. In the preceding sample, we subscribe the "update" event on the service and call notify to make it fire the event. As soon as the event is captured, we invoke the done callback.

You have been reading a chapter from
Cross-platform Desktop Application Development: Electron, Node, NW.js, and React
Published in: Jul 2017
Publisher: Packt
ISBN-13: 9781788295697
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image