vscode-icons/vscode-icons

View on GitHub
test/utils/utils.test.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
/* eslint-disable prefer-arrow-callback */
/* eslint-disable no-unused-expressions */
import { expect } from 'chai';
import { ChildProcess } from 'child_process';
import { Stats } from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as proxyq from 'proxyquire';
import * as sinon from 'sinon';
import * as fsAsync from '../../src/common/fsAsync';
import { FileFormat } from '../../src/models';
import { Utils } from '../../src/utils';

describe('Utils: tests', function () {
  interface IUtils {
    open: (target: string, options?: object) => Promise<ChildProcess>;
  }

  context('ensures that', function () {
    let sandbox: sinon.SinonSandbox;
    let existsAsyncStub: sinon.SinonStub;

    beforeEach(function () {
      sandbox = sinon.createSandbox();
      existsAsyncStub = sandbox.stub(fsAsync, 'existsAsync').resolves();
    });

    afterEach(function () {
      sandbox.restore();
    });

    context(`the 'getAppDataDirPath' function`, function () {
      context(`returns the correct 'vscode' path`, function () {
        context(`when the process platform is`, function () {
          let envStub: sinon.SinonStub;
          let platformStub: sinon.SinonStub;

          beforeEach(function () {
            envStub = sandbox.stub(process, 'env');
            platformStub = sandbox.stub(process, 'platform');
          });

          it('darwin (macOS)', function () {
            const dirPath = `${os.homedir()}/Library/Application Support`;
            platformStub.value('darwin');

            expect(Utils.getAppDataDirPath()).to.be.equal(dirPath);
          });

          it('linux', function () {
            const dirPath = `${os.homedir()}/.config`;
            platformStub.value('linux');

            expect(Utils.getAppDataDirPath()).to.be.equal(dirPath);
          });

          it('win32 (windows)', function () {
            const dirPath = 'C:\\Users\\User\\AppData\\Roaming';
            envStub.value({
              APPDATA: dirPath,
            });
            platformStub.value('win32');

            expect(Utils.getAppDataDirPath()).to.be.equal(dirPath);
          });

          it('NOT implemented', function () {
            const dirPath = '/var/local';
            platformStub.value('freebsd');

            expect(Utils.getAppDataDirPath()).to.be.equal(dirPath);
          });
        });
      });
    });

    context(`the 'pathUnixJoin' function`, function () {
      it('returns a path using Unix separator', function () {
        expect(Utils.pathUnixJoin('path', 'to', 'code')).to.equal(
          'path/to/code',
        );
      });
    });

    context(`the 'tempPath' function`, function () {
      it('returns the path to the OS temporary directory', function () {
        expect(Utils.tempPath()).to.equal(os.tmpdir());
      });
    });

    context(`the 'fileFormatToString' function`, function () {
      it(`returns the string representation of the 'FileFormat'`, function () {
        expect(Utils.fileFormatToString('svg')).to.equal('.svg');
        expect(Utils.fileFormatToString(FileFormat.svg)).to.equal('.svg');
      });
    });

    context(`the 'createDirectoryRecursively' function`, function () {
      context('creates all directories asynchronously', function () {
        context(`when the process platform is`, function () {
          let pathSepStub: sinon.SinonStub;
          let mkdirAsyncStub: sinon.SinonStub;

          beforeEach(function () {
            pathSepStub = sandbox.stub(path, 'sep');
            mkdirAsyncStub = sandbox.stub(fsAsync, 'mkdirAsync').resolves();
          });

          const testCase = async (
            directoryPath: string,
            dirExists: boolean,
            expectedCounts: number,
          ): Promise<void> => {
            const fileCheck = existsAsyncStub.callsFake(
              (dirPath: string) =>
                dirExists || directoryPath.split(path.sep).includes(dirPath),
            );
            const createDirectory = mkdirAsyncStub.resolves();

            fileCheck.resetHistory();
            createDirectory.resetHistory();

            await Utils.createDirectoryRecursively(directoryPath);

            expect(fileCheck.callCount).to.be.equal(
              dirExists ? expectedCounts + 2 : expectedCounts,
            );
            expect(createDirectory.callCount).to.equal(expectedCounts);
          };

          it('win32 (windows)', async function () {
            pathSepStub.value('\\');

            // Directory Exists
            await testCase('path\\to', true, 0);

            // Create Directory
            // - Relative path
            await testCase('.\\path', false, 2);

            // - Absolute path
            await testCase('C:\\path\\to', false, 3);
          });

          it('*nix', async function () {
            pathSepStub.value('/');

            // Directory Exists
            await testCase('path/to', true, 0);

            // Create Directory
            // - Relative path
            await testCase('path/to', false, 2);

            // - Absolute path
            await testCase('/path/to', false, 3);
          });
        });
      });
    });

    context(`the 'deleteDirectoryRecursively' function`, function () {
      let readdirAsyncStub: sinon.SinonStub;

      it('deletes a directory and all subdirectories asynchronously', async function () {
        readdirAsyncStub = sandbox.stub(fsAsync, 'readdirAsync').resolves();
        const directoryPath = '/path/to';
        const lstatsAsyncStub = sandbox.stub(fsAsync, 'lstatAsync').resolves();
        const fileCheck = existsAsyncStub
          .onFirstCall()
          .resolves(true)
          .onSecondCall()
          .resolves(false);
        const readDirectory = readdirAsyncStub.resolves(['dir', 'file.txt']);
        const lstats = lstatsAsyncStub
          .onFirstCall()
          .resolves({
            isDirectory: () => true,
          } as Stats)
          .onSecondCall()
          .resolves({
            isDirectory: () => false,
          } as Stats);
        const deleteFile = sandbox.stub(fsAsync, 'unlinkAsync').resolves();
        const removeDirectory = sandbox.stub(fsAsync, 'rmdirAsync').resolves();

        await Utils.deleteDirectoryRecursively(directoryPath);

        expect(fileCheck.calledTwice).to.be.true;
        expect(readDirectory.calledOnce).to.be.true;
        expect(lstats.calledTwice).to.be.true;
        expect(deleteFile.calledOnce).to.be.true;
        expect(removeDirectory.calledOnce).to.be.true;
      });
    });

    context(`the 'parseJSON' function`, function () {
      it('returns an object when parsing succeeds', function () {
        const json =
          Utils.parseJSONSafe<Record<string, string>>('{"test": "test"}');

        expect(json).to.be.instanceOf(Object);
        expect(Object.getOwnPropertyNames(json)).to.include('test');
        expect(json.test).to.be.equal('test');
      });

      it(`returns an empty object when parsing fails`, function () {
        expect(Utils.parseJSONSafe('test')).to.be.empty;
      });
    });

    context(`the 'getRelativePath' function`, function () {
      context(`does NOT throw an Error`, function () {
        context(`if the destination directory does NOT exists`, function () {
          it('and a directory check should NOT be done', function () {
            const toDirName = 'path/to';

            expect(() =>
              Utils.getRelativePath('path/from', toDirName, false),
            ).to.not.throw(Error, `Directory '${toDirName}' not found.`);
          });
        });
      });

      context('returns a relative path that', function () {
        context('has a trailing path separator', function () {
          const trailingPathSeparatorTest = async (
            toDirName: string,
          ): Promise<void> => {
            const relativePath = await Utils.getRelativePath(
              'path/from',
              toDirName,
              false,
            );

            expect(relativePath.endsWith('/')).to.be.true;
            expect(/\/{2,}$/g.test(relativePath)).to.be.false;
          };

          it(`if it's provided`, async function () {
            await trailingPathSeparatorTest('path/to/');
          });

          it(`if it's NOT provided`, async function () {
            await trailingPathSeparatorTest('path/to');
          });

          it('that is NOT repeated', async function () {
            await trailingPathSeparatorTest('path/to//');
            await trailingPathSeparatorTest('path/to///');
          });
        });
      });

      context('throws an Error', function () {
        it('if the `fromDirPath` parameter is NOT defined', async function () {
          try {
            await Utils.getRelativePath(null, 'path/to');
          } catch (error) {
            expect(error).to.match(/fromDirPath not defined\./);
          }
        });

        it('if the `toDirName` parameter is NOT defined', async function () {
          try {
            await Utils.getRelativePath('path/from', null);
          } catch (error) {
            expect(error).to.match(/toDirName not defined\./);
          }
        });

        it('the destination directory does NOT exists', async function () {
          const toDirName = 'path/to';

          try {
            await Utils.getRelativePath('path/from', toDirName);
          } catch (error) {
            expect(error).to.match(
              new RegExp(`Directory '${toDirName}' not found.`),
            );
          }
        });
      });
    });

    context(`the 'removeFirstDot' function`, function () {
      it('removes the leading dot', function () {
        expect(Utils.removeFirstDot('.test')).to.be.equal('test');
      });

      it('ignores when no leading dot', function () {
        expect(Utils.removeFirstDot('test')).to.be.equal('test');
      });
    });

    context(`the 'belongToSameDrive' function`, function () {
      it(`returns 'false', when paths do NOT belong to the same drive`, function () {
        expect(Utils.belongToSameDrive('C:\\path\\to', 'D:\\path\to')).to.be
          .false;
      });

      it(`returns 'true', when paths do belong to the same drive`, function () {
        expect(Utils.belongToSameDrive('C:\\path\\to', 'C:\\anotherpath\to')).to
          .be.true;
      });
    });

    context(`the 'overwriteDrive' function`, function () {
      it('overwrites the drive', function () {
        const sourcePath = 'C:\\path\\to';
        const destPath = 'D:\\path\\to';

        expect(Utils.overwriteDrive(sourcePath, destPath)).to.be.equal(
          sourcePath,
        );
      });
    });

    context(`the 'getDrives' function returns an`, function () {
      it('Array of the provided drives', function () {
        const drive1 = 'C:';
        const drive2 = 'D:';

        expect(Utils.getDrives(drive1))
          .to.be.an.instanceOf(Array)
          .and.include(drive1);
        expect(Utils.getDrives(drive1, drive2))
          .an.instanceOf(Array)
          .and.include.members([drive1, drive2]);
      });

      it('empty Array, if no drive is provided', function () {
        expect(Utils.getDrives()).to.be.an.instanceOf(Array).and.be.empty;
      });

      it('Array of undefined drives, if provided paths are NOT actual drives', function () {
        expect(Utils.getDrives('/', 'file:///'))
          .to.be.an.instanceOf(Array)
          .and.include.members([undefined]);
      });
    });

    context(`the 'combine' function`, function () {
      it('returns an array combining the elements of the provided arrays', function () {
        const array1 = ['webpack.base.conf', 'webpack.common'];
        const array2 = ['js', 'coffee', 'ts'];
        const combinedArray = [
          'webpack.base.conf.js',
          'webpack.base.conf.coffee',
          'webpack.base.conf.ts',
          'webpack.common.js',
          'webpack.common.coffee',
          'webpack.common.ts',
        ];
        expect(Utils.combine(array1, array2))
          .to.be.an.instanceOf(Array)
          .and.have.deep.members(combinedArray);
      });
    });

    context(`the 'updateFile' function`, function () {
      let readFileAsyncStub: sinon.SinonStub;
      let writeFileAsyncStub: sinon.SinonStub;
      let replacerStub: sinon.SinonStub;

      beforeEach(function () {
        readFileAsyncStub = sandbox.stub(fsAsync, 'readFileAsync');
        writeFileAsyncStub = sandbox.stub(fsAsync, 'writeFileAsync').resolves();
        replacerStub = sandbox.stub();
      });

      it('rejects on file read error', async function () {
        readFileAsyncStub.rejects(new Error('error on read'));
        try {
          await Utils.updateFile('', replacerStub);
        } catch (err) {
          expect(replacerStub.called).to.be.false;
          expect(err)
            .to.be.an.instanceof(Error)
            .that.matches(/error on read/);
        }
      });

      it('rejects on file write error', async function () {
        readFileAsyncStub.resolves('');
        writeFileAsyncStub.rejects(new Error('error on write'));
        replacerStub.returns([]);

        try {
          await Utils.updateFile('', replacerStub);
        } catch (error) {
          expect(replacerStub.calledOnce).to.be.true;
          expect(error)
            .to.be.an.instanceof(Error)
            .that.matches(/error on write/);
        }
      });

      it('correctly detects unix style EOL (LF)', async function () {
        readFileAsyncStub.resolves('\n');
        replacerStub.returns([]);

        const result = await Utils.updateFile('', replacerStub);

        expect(replacerStub.calledOnce).to.be.true;
        expect(result).to.be.undefined;
      });

      it('correctly detects windows style EOL (CRLF)', async function () {
        readFileAsyncStub.resolves('\r\n');
        replacerStub.returns([]);

        const result = await Utils.updateFile('', replacerStub);

        expect(replacerStub.calledOnce).to.be.true;
        expect(result).to.be.undefined;
      });

      it(`updates the file`, async function () {
        readFileAsyncStub.resolves('text\n');
        // Note: it's up to the replacer to provide the correct replaced context
        replacerStub.returns(['replaced\n']);

        const result = await Utils.updateFile('', replacerStub);

        expect(replacerStub.calledOnce).to.be.true;
        expect(result).to.be.undefined;
      });
    });

    context(`the 'unflattenProperties' function`, function () {
      it(`returns an object with individual properties structure`, function () {
        const obj = {
          'vsicons.dontShowNewVersionMessage': {
            default: false,
          },
        };

        expect(Utils.unflattenProperties(obj, 'default'))
          .to.be.an('object')
          .with.ownProperty('vsicons')
          .and.that.to.haveOwnProperty('dontShowNewVersionMessage').and.that.to
          .be.false;
      });
    });

    context(`the 'open' function`, function () {
      it(`to call the external module`, async function () {
        const openStub = sandbox.stub().resolves();
        const target = 'target';
        const utils = (
          proxyq.noCallThru().load('../../src/utils', {
            open: openStub,
          }) as Record<string, IUtils>
        ).Utils;

        await utils.open(target);

        expect(openStub.calledOnceWithExactly(target, undefined)).to.be.true;
      });
    });
  });
});