diff --git a/packages/utils/src/lang.js b/packages/utils/src/lang.js index 4a3c68ca81..a98f294ea3 100644 --- a/packages/utils/src/lang.js +++ b/packages/utils/src/lang.js @@ -4,7 +4,7 @@ export const encodeHtml = function encodeHtml(str) { export const isString = obj => typeof obj === 'string' || obj instanceof String -export const isNonEmptyString = obj => obj && isString(obj) +export const isNonEmptyString = obj => Boolean(obj && isString(obj)) export const isPureObject = function isPureObject(o) { return !Array.isArray(o) && typeof o === 'object' diff --git a/packages/utils/src/resolve.js b/packages/utils/src/resolve.js index 1d63c5a880..65e0da8bad 100644 --- a/packages/utils/src/resolve.js +++ b/packages/utils/src/resolve.js @@ -11,7 +11,6 @@ export const startsWithRootAlias = startsWithAlias(['@@', '~~']) export const isWindows = /^win/.test(process.platform) export const wp = function wp(p = '') { - /* istanbul ignore if */ if (isWindows) { return p.replace(/\\/g, '\\\\') } @@ -19,7 +18,6 @@ export const wp = function wp(p = '') { } export const wChunk = function wChunk(p = '') { - /* istanbul ignore if */ if (isWindows) { return p.replace(/\//g, '_') } @@ -62,7 +60,7 @@ export const relativeTo = function relativeTo() { // Make correct relative path let rp = path.relative(dir, _path) if (rp[0] !== '.') { - rp = './' + rp + rp = '.' + path.sep + rp } return wp(rp) diff --git a/packages/utils/src/route.js b/packages/utils/src/route.js index e75bbaf09c..5bda20e779 100644 --- a/packages/utils/src/route.js +++ b/packages/utils/src/route.js @@ -9,7 +9,6 @@ export const flatRoutes = function flatRoutes(router, _path = '', routes = []) { if ([':', '*'].some(c => r.path.includes(c))) { return } - /* istanbul ignore if */ if (r.children) { if (_path === '' && r.path === '/') { routes.push('/') diff --git a/packages/utils/src/serialize.js b/packages/utils/src/serialize.js index d04bd31194..7428fb6a81 100644 --- a/packages/utils/src/serialize.js +++ b/packages/utils/src/serialize.js @@ -1,4 +1,3 @@ - import serialize from 'serialize-javascript' export function serializeFunction(func) { diff --git a/packages/utils/src/task.js b/packages/utils/src/task.js index 412096d119..b3186d135a 100644 --- a/packages/utils/src/task.js +++ b/packages/utils/src/task.js @@ -10,7 +10,6 @@ export const parallel = function parallel(tasks, fn) { } export const chainFn = function chainFn(base, fn) { - /* istanbul ignore if */ if (typeof fn !== 'function') { return base } diff --git a/packages/utils/test/__snapshots__/route.test.js.snap b/packages/utils/test/__snapshots__/route.test.js.snap new file mode 100644 index 0000000000..7ac1ccbc04 --- /dev/null +++ b/packages/utils/test/__snapshots__/route.test.js.snap @@ -0,0 +1,121 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`util: route util: route create createRoutes should allow snake case routes in posix system 1`] = ` +Array [ + Object { + "chunkName": "pages/parent/index", + "component": "/some/nuxt/app/pages/parent/index.vue", + "name": "parent", + "path": "/parent", + }, + Object { + "chunkName": "pages/snake_case_route", + "component": "/some/nuxt/app/pages/snake_case_route.vue", + "name": "snake_case_route", + "path": "/snake_case_route", + }, + Object { + "chunkName": "pages/parent/child/index", + "component": "/some/nuxt/app/pages/parent/child/index.vue", + "name": "parent-child", + "path": "/parent/child", + }, + Object { + "chunkName": "pages/parent/child/test", + "component": "/some/nuxt/app/pages/parent/child/test.vue", + "name": "parent-child-test", + "path": "/parent/child/test", + }, + Object { + "children": Array [ + Object { + "chunkName": "pages/another_route/_id", + "component": "/some/nuxt/app/pages/another_route/_id.vue", + "name": "another_route-id", + "path": "", + }, + ], + "chunkName": "pages/another_route/_id", + "component": "/some/nuxt/app/pages/another_route/_id.vue", + "path": "/another_route/:id?", + }, + Object { + "chunkName": "pages/subpage/_param", + "component": "/some/nuxt/app/pages/subpage/_param.vue", + "name": "subpage-param", + "path": "/subpage/:param?", + }, + Object { + "chunkName": "pages/index", + "component": "/some/nuxt/app/pages/index.vue", + "name": "index", + "path": "/", + }, + Object { + "chunkName": "pages/_param", + "component": "/some/nuxt/app/pages/_param.vue", + "name": "param", + "path": "/:param", + }, +] +`; + +exports[`util: route util: route create createRoutes should allow snake case routes in windows system 1`] = ` +Array [ + Object { + "chunkName": "pages/parent/index", + "component": "\\\\\\\\\\\\\\\\some\\\\\\\\nuxt\\\\\\\\app\\\\\\\\pages\\\\\\\\parent\\\\\\\\index.vue", + "name": "parent", + "path": "/parent", + }, + Object { + "chunkName": "pages/snake_case_route", + "component": "\\\\\\\\\\\\\\\\some\\\\\\\\nuxt\\\\\\\\app\\\\\\\\pages\\\\\\\\snake_case_route.vue", + "name": "snake_case_route", + "path": "/snake_case_route", + }, + Object { + "chunkName": "pages/parent/child/index", + "component": "\\\\\\\\\\\\\\\\some\\\\\\\\nuxt\\\\\\\\app\\\\\\\\pages\\\\\\\\parent\\\\\\\\child\\\\\\\\index.vue", + "name": "parent-child", + "path": "/parent/child", + }, + Object { + "chunkName": "pages/parent/child/test", + "component": "\\\\\\\\\\\\\\\\some\\\\\\\\nuxt\\\\\\\\app\\\\\\\\pages\\\\\\\\parent\\\\\\\\child\\\\\\\\test.vue", + "name": "parent-child-test", + "path": "/parent/child/test", + }, + Object { + "children": Array [ + Object { + "chunkName": "pages/another_route/_id", + "component": "\\\\\\\\\\\\\\\\some\\\\\\\\nuxt\\\\\\\\app\\\\\\\\pages\\\\\\\\another_route\\\\\\\\_id.vue", + "name": "another_route-id", + "path": "", + }, + ], + "chunkName": "pages/another_route/_id", + "component": "\\\\\\\\\\\\\\\\some\\\\\\\\nuxt\\\\\\\\app\\\\\\\\pages\\\\\\\\another_route\\\\\\\\_id.vue", + "path": "/another_route/:id?", + }, + Object { + "chunkName": "pages/subpage/_param", + "component": "\\\\\\\\\\\\\\\\some\\\\\\\\nuxt\\\\\\\\app\\\\\\\\pages\\\\\\\\subpage\\\\\\\\_param.vue", + "name": "subpage-param", + "path": "/subpage/:param?", + }, + Object { + "chunkName": "pages/index", + "component": "\\\\\\\\\\\\\\\\some\\\\\\\\nuxt\\\\\\\\app\\\\\\\\pages\\\\\\\\index.vue", + "name": "index", + "path": "/", + }, + Object { + "chunkName": "pages/_param", + "component": "\\\\\\\\\\\\\\\\some\\\\\\\\nuxt\\\\\\\\app\\\\\\\\pages\\\\\\\\_param.vue", + "name": "param", + "path": "/:param", + }, +] +`; diff --git a/packages/utils/test/context.test.js b/packages/utils/test/context.test.js new file mode 100644 index 0000000000..bc2aa7effd --- /dev/null +++ b/packages/utils/test/context.test.js @@ -0,0 +1,24 @@ +import { getContext, determineGlobals } from '../src/context' + +describe('util: context', () => { + test('should get context with req and res', () => { + const ctx = getContext({ a: 1 }, { b: 2 }) + + expect(getContext.length).toBe(2) + expect(typeof ctx.req).toBe('object') + expect(typeof ctx.res).toBe('object') + expect(ctx.req.a).toBe(1) + expect(ctx.res.b).toBe(2) + }) + + test('should get correct globals', () => { + const globals = { + foo: name => `${name}: foo`, + bar: name => `${name}: bar`, + baz: 'baz' + } + const result = determineGlobals('global', globals) + + expect(result).toEqual({ bar: 'global: bar', foo: 'global: foo', baz: 'baz' }) + }) +}) diff --git a/packages/utils/test/index.test.js b/packages/utils/test/index.test.js new file mode 100644 index 0000000000..0ad3710d59 --- /dev/null +++ b/packages/utils/test/index.test.js @@ -0,0 +1,22 @@ +import * as Util from '../src' +import * as context from '../src/context' +import * as lang from '../src/lang' +import * as resolve from '../src/resolve' +import * as route from '../src/route' +import * as serialize from '../src/serialize' +import * as task from '../src/task' +import * as timer from '../src/timer' + +describe('util: entry', () => { + test('should export all methods from utils folder', () => { + expect(Util).toEqual({ + ...context, + ...lang, + ...resolve, + ...route, + ...serialize, + ...task, + ...timer + }) + }) +}) diff --git a/packages/utils/test/lang.test.js b/packages/utils/test/lang.test.js new file mode 100644 index 0000000000..f3132a877c --- /dev/null +++ b/packages/utils/test/lang.test.js @@ -0,0 +1,55 @@ +import { + encodeHtml, isString, isNonEmptyString, + isPureObject, isUrl, urlJoin, wrapArray, stripWhitespace +} from '../src/lang' + +describe('util: lang', () => { + test('should check if given argument is string', () => { + expect(isString('str')).toEqual(true) + expect(isString(String(100))).toEqual(true) + expect(isString(100)).toEqual(false) + expect(isString([])).toEqual(false) + }) + + test('should check if given argument is empty string', () => { + expect(isNonEmptyString('str')).toEqual(true) + expect(isNonEmptyString([])).toEqual(false) + expect(isNonEmptyString('')).toEqual(false) + }) + + test('should check if given argument is pure object', () => { + expect(isPureObject({})).toEqual(true) + expect(isPureObject([])).toEqual(false) + expect(isPureObject(Number('1'))).toEqual(false) + }) + + test('should check if given argument is url', () => { + expect(isUrl('http://localhost')).toEqual(true) + expect(isUrl('https://localhost')).toEqual(true) + expect(isUrl('//localhost')).toEqual(true) + expect(isUrl('localhost')).toEqual(false) + }) + + test('should wrap given argument with array', () => { + expect(wrapArray([ 'array' ])).toEqual([ 'array' ]) + expect(wrapArray('str')).toEqual([ 'str' ]) + }) + + test('should strip white spaces in given argument', () => { + expect(stripWhitespace('foo')).toEqual('foo') + expect(stripWhitespace('foo\t\r\f\n')).toEqual('foo\n') + expect(stripWhitespace('foo{\n\n\n')).toEqual('foo{\n') + expect(stripWhitespace('\n\n\n\f\r\f}')).toEqual('\n\f\r\f}') + expect(stripWhitespace('foo\n\n\nbar')).toEqual('foo\n\nbar') + expect(stripWhitespace('foo\n\n\n')).toEqual('foo\n') + }) + + test('should encode html', () => { + const html = '

Hello

' + expect(encodeHtml(html)).toEqual('<h1>Hello</h1>') + }) + + test('should join url', () => { + expect(urlJoin('test', '/about')).toEqual('test/about') + }) +}) diff --git a/packages/utils/test/resolve.posix.test.js b/packages/utils/test/resolve.posix.test.js new file mode 100644 index 0000000000..650ccd3402 --- /dev/null +++ b/packages/utils/test/resolve.posix.test.js @@ -0,0 +1,85 @@ +import consola from 'consola' + +import { + startsWithAlias, startsWithSrcAlias, wp, wChunk, + relativeTo, defineAlias, isIndexFileAndFolder, getMainModule +} from '../src/resolve' + +describe.posix('util: resolve', () => { + test('should check if path starts with alias', () => { + expect(startsWithAlias(['/var'])('/var/nuxt/src')).toEqual(true) + }) + + test('should check if path starts with root alias', () => { + expect(startsWithSrcAlias('@/assets')).toEqual(true) + expect(startsWithSrcAlias('~/pages')).toEqual(true) + }) + + test('should check if path starts with src alias', () => { + expect(startsWithSrcAlias('@@/src/assets')).toEqual(true) + expect(startsWithSrcAlias('~~/src/pages')).toEqual(true) + }) + + test('should return same path in linux', () => { + expect(wp('/var/nuxt\\ src/')).toEqual('/var/nuxt\\ src/') + }) + + test('should return same path in linux', () => { + expect(wChunk('nuxt/layout/test')).toEqual('nuxt/layout/test') + }) + + test('should define alias', () => { + const nuxt = {} + const server = { + name: 'nuxt', + bound: () => 'bound fn', + test: () => 'test defineAlias' + } + + defineAlias(nuxt, server, ['name', 'bound']) + defineAlias(nuxt, server, ['test'], { bind: false, warn: true }) + + expect(nuxt.name).toEqual(server.name) + expect(nuxt.bound).not.toBe(server.bound) + expect(nuxt.bound()).toEqual('bound fn') + expect(nuxt.test).toBe(server.test) + expect(nuxt.test()).toEqual('test defineAlias') + expect(consola.warn).toBeCalledTimes(1) + expect(consola.warn).toBeCalledWith({ + message: `'test' is deprecated'`, + additional: expect.any(String) + }) + }) + + test('should check if given argument is index file or folder', () => { + expect(isIndexFileAndFolder(['/var/nuxt/plugins/test'])).toEqual(false) + expect(isIndexFileAndFolder(['/var/nuxt/plugins/test/index.js'])).toEqual(false) + expect(isIndexFileAndFolder(['/var/nuxt/plugins/test', '/var/nuxt/plugins/test/index.js'])).toEqual(true) + }) + + test('should return main module', () => { + expect(getMainModule()).toHaveProperty('children', 'exports', 'filename', 'path') + }) + + describe('relativeTo', () => { + const path1 = '@/foo' + const path2 = '@/bar' + + test('should resolve alias path', () => { + expect(relativeTo(path1, path2)).toBe('@/bar') + }) + + test('should keep webpack inline loaders prepended', () => { + expect(relativeTo(path1, `loader1!loader2!${path2}`)) + .toEqual('loader1!loader2!@/bar') + }) + + test('should check path which is not started with alias', () => { + expect(relativeTo('/var/nuxt/foo/bar', '/var/nuxt/foo/baz')).toBe('../baz') + }) + + test('should check path which is not started with alias ', () => { + expect(relativeTo('/var/nuxt/foo', '/var/nuxt/foo/bar')).toBe('./bar') + }) + }) +}) diff --git a/packages/utils/test/resolve.win.test.js b/packages/utils/test/resolve.win.test.js new file mode 100644 index 0000000000..80ca1ea474 --- /dev/null +++ b/packages/utils/test/resolve.win.test.js @@ -0,0 +1,90 @@ +import consola from 'consola' + +import { + wp, wChunk, r, relativeTo, + startsWithAlias, startsWithSrcAlias, + defineAlias, isIndexFileAndFolder, getMainModule +} from '../src/resolve' + +describe.win('util: resolve windows', () => { + test('should format windows separator', () => { + expect(wp('c:\\nuxt\\src')).toEqual('c:\\\\nuxt\\\\src') + }) + + test('should format windows path', () => { + expect(wChunk('nuxt/layout/test')).toEqual('nuxt_layout_test') + }) + + test('should resolve alias path', () => { + expect(r('@\\layout\\test')).toEqual('@\\\\layout\\\\test') + }) + + test('should check if path starts with alias', () => { + expect(startsWithAlias(['#'])('#layout/test')).toEqual(true) + }) + + test('should check if path starts with root alias', () => { + expect(startsWithSrcAlias('@/assets')).toEqual(true) + expect(startsWithSrcAlias('~/pages')).toEqual(true) + }) + + test('should check if path starts with src alias', () => { + expect(startsWithSrcAlias('@@/src/assets')).toEqual(true) + expect(startsWithSrcAlias('~~/src/pages')).toEqual(true) + }) + + test('should define alias', () => { + const nuxt = {} + const server = { + name: 'nuxt', + bound: () => 'bound fn', + test: () => 'test defineAlias' + } + + defineAlias(nuxt, server, ['name', 'bound']) + defineAlias(nuxt, server, ['test'], { bind: false, warn: true }) + + expect(nuxt.name).toEqual(server.name) + expect(nuxt.bound).not.toBe(server.bound) + expect(nuxt.bound()).toEqual('bound fn') + expect(nuxt.test).toBe(server.test) + expect(nuxt.test()).toEqual('test defineAlias') + expect(consola.warn).toBeCalledTimes(1) + expect(consola.warn).toBeCalledWith({ + message: `'test' is deprecated'`, + additional: expect.any(String) + }) + }) + + test('should check if given argument is index file or folder', () => { + expect(isIndexFileAndFolder(['/var/nuxt/plugins/test'])).toEqual(false) + expect(isIndexFileAndFolder(['/var/nuxt/plugins/test/index.js'])).toEqual(false) + expect(isIndexFileAndFolder(['/var/nuxt/plugins/test', '/var/nuxt/plugins/test/index.js'])).toEqual(true) + }) + + test('should return main module', () => { + expect(getMainModule()).toHaveProperty('children', 'exports', 'filename', 'path') + }) + + describe('relativeTo', () => { + const path1 = '@\\foo' + const path2 = '@\\bar' + + test('should resolve alias path', () => { + expect(relativeTo(path1, path2)).toBe('@\\\\bar') + }) + + test('should keep webpack inline loaders prepended', () => { + expect(relativeTo(path1, `loader1!loader2!${path2}`)) + .toBe('loader1!loader2!@\\\\bar') + }) + + test('should check path which is not started with alias', () => { + expect(relativeTo('c:\\foo\\bar', 'c:\\foo\\baz')).toBe('..\\\\baz') + }) + + test('should check path which is not started with alias ', () => { + expect(relativeTo('c:\\foo', 'c:\\foo\\baz')).toBe('.\\\\baz') + }) + }) +}) diff --git a/packages/utils/test/route.test.js b/packages/utils/test/route.test.js new file mode 100644 index 0000000000..e61420d7d4 --- /dev/null +++ b/packages/utils/test/route.test.js @@ -0,0 +1,198 @@ +import { flatRoutes, createRoutes, guardDir, promisifyRoute } from '../src/route' + +describe('util: route', () => { + test('should flat route with path', () => { + const routes = flatRoutes([ + { name: 'login', path: '/login' }, + { name: 'about', path: '/about' }, + { name: 'posts', + path: '', + children: [ + { name: 'posts-list', path: '' }, + { name: 'posts-create', path: 'post' } + ] + } + ]) + expect(routes).toEqual([ '/login', '/about', '', '/post' ]) + }) + + test('should ignore route with * and :', () => { + const routes = flatRoutes([ + { name: 'login', path: '/login' }, + { name: 'foo', path: '/foo/:id' }, + { name: 'bar', path: '/bar/*' } + ]) + expect(routes).toEqual([ '/login' ]) + }) + + test('should resolve route with /', () => { + const routes = flatRoutes([ + { name: 'foo', + path: '/', + children: [ + { name: 'foo-bar', path: 'foo/bar' }, + { name: 'foo-baz', path: 'foo/baz' } + ] + } + ]) + expect(routes).toEqual([ '/', '/foo/bar', '/foo/baz' ]) + }) + + describe('util: route guard', () => { + test('should guard parent dir', () => { + expect(() => { + guardDir({ dir1: '/root/parent', dir2: '/root' }, 'dir1', 'dir2') + }).toThrow() + }) + + test('should guard same dir', () => { + expect(() => { + guardDir({ dir1: '/root/parent', dir2: '/root/parent' }, 'dir1', 'dir2') + }).toThrow() + }) + + test('should not guard same level dir', () => { + expect(() => { + guardDir({ dir1: '/root/parent-next', dir2: '/root/parent' }, 'dir1', 'dir2') + }).not.toThrow() + }) + + test('should not guard same level dir - 2', () => { + expect(() => { + guardDir({ dir1: '/root/parent', dir2: '/root/parent-next' }, 'dir1', 'dir2') + }).not.toThrow() + }) + + test('should not guard child dir', () => { + expect(() => { + guardDir({ dir1: '/root/parent', dir2: '/root/parent/child' }, 'dir1', 'dir2') + }).not.toThrow() + }) + }) + + describe('util: route promisifyRoute', () => { + test('should promisify array routes', () => { + const array = [1] + const promise = promisifyRoute(array) + expect(typeof promise).toBe('object') + return promise.then((res) => { + expect(res).toBe(array) + }) + }) + + test('should promisify functional routes', () => { + const array = [1, 2] + const fn = function () { + return array + } + const promise = promisifyRoute(fn) + expect(typeof promise).toBe('object') + return promise.then((res) => { + expect(res).toBe(array) + }) + }) + + test('should promisify promisable functional routes', () => { + const array = [1, 2, 3] + const fn = function () { + return new Promise((resolve) => { + resolve(array) + }) + } + const promise = promisifyRoute(fn) + expect(typeof promise).toBe('object') + return promise.then((res) => { + expect(res).toBe(array) + }) + }) + + test('should promisify promisable functional routes with arguments', () => { + const fn = function (array) { + return new Promise((resolve) => { + resolve(array) + }) + } + const array = [1, 2, 3] + const promise = promisifyRoute(fn, array) + expect(typeof promise).toBe('object') + return promise.then((res) => { + expect(res).toBe(array) + }) + }) + + test('should promisify functional routes with error', () => { + const fn = function (cb) { + cb(new Error('Error here')) + } + const promise = promisifyRoute(fn) + expect(typeof promise).toBe('object') + return promise.catch((e) => { + expect(e.message).toBe('Error here') + }) + }) + + test('should promisify functional routes with arguments and error', () => { + const fn = function (cb, array) { + cb(new Error('Error here: ' + array.join())) + } + const array = [1, 2, 3, 4] + const promise = promisifyRoute(fn, array) + expect(typeof promise).toBe('object') + return promise.catch((e) => { + expect(e.message).toBe('Error here: ' + array.join()) + }) + }) + + test('should promisify functional routes with result', () => { + const array = [1, 2, 3, 4] + const fn = function (cb) { + cb(null, array) + } + const promise = promisifyRoute(fn) + expect(typeof promise).toBe('object') + return promise.then((res) => { + expect(res).toBe(array) + }) + }) + + test('should promisify functional routes with arguments and result', () => { + const fn = function (cb, array, object) { + cb(null, { array, object }) + } + const array = [1, 2, 3, 4] + const object = { a: 1 } + const promise = promisifyRoute(fn, array, object) + expect(typeof promise).toBe('object') + return promise.then((res) => { + expect(res.array).toBe(array) + expect(res.object).toBe(object) + }) + }) + }) + + describe('util: route create', () => { + const files = [ + 'pages/index.vue', + 'pages/_param.vue', + 'pages/subpage/_param.vue', + 'pages/snake_case_route.vue', + 'pages/another_route/_id.vue', + 'pages/another_route/_id.vue', + 'pages/parent/index.vue', + 'pages/parent/child/index.vue', + 'pages/parent/child/test.vue' + ] + const srcDir = '/some/nuxt/app' + const pagesDir = 'pages' + + test.posix('createRoutes should allow snake case routes in posix system', () => { + const routesResult = createRoutes(files, srcDir, pagesDir) + expect(routesResult).toMatchSnapshot() + }) + + test.win('createRoutes should allow snake case routes in windows system', () => { + const routesResult = createRoutes(files, srcDir, pagesDir) + expect(routesResult).toMatchSnapshot() + }) + }) +}) diff --git a/packages/utils/test/serialize.test.js b/packages/utils/test/serialize.test.js new file mode 100644 index 0000000000..bb9c0df6c4 --- /dev/null +++ b/packages/utils/test/serialize.test.js @@ -0,0 +1,61 @@ +import { serializeFunction } from '../src/serialize' + +describe('util: serialize', () => { + test('should serialize normal function', () => { + const obj = { + fn: function () {} + } + expect(serializeFunction(obj.fn)).toEqual('function () {}') + }) + + test('should serialize shorthand function', () => { + const obj = { + fn() {} + } + expect(serializeFunction(obj.fn)).toEqual('function() {}') + }) + + test('should serialize arrow function', () => { + const obj = { + fn: () => {} + } + expect(serializeFunction(obj.fn)).toEqual('() => {}') + }) + + test('should not replace custom scripts', () => { + const obj = { + fn() { + return 'function xyz(){};a=false?true:xyz();' + } + } + + expect(serializeFunction(obj.fn)).toEqual(`function () { + return 'function xyz(){};a=false?true:xyz();'; + }`) + }) + + test('should serialize internal function', () => { + const obj = { + fn(arg) { + if (arg) { + return { + title() { + return 'test' + } + } + } + } + } + + expect(serializeFunction(obj.fn)).toEqual(`function(arg) { + if (arg) { + return { + title: function () { + return 'test'; + } + + }; + } + }`) + }) +}) diff --git a/packages/utils/test/task.test.js b/packages/utils/test/task.test.js new file mode 100644 index 0000000000..69cbbcdecc --- /dev/null +++ b/packages/utils/test/task.test.js @@ -0,0 +1,106 @@ +import consola from 'consola' +import { sequence, parallel, chainFn } from '../src/task' + +describe('util: task', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test('should call fn in sequence', async () => { + const fn = jest.fn(consola.log) + await sequence([1, 2, 3, 4], fn) + + expect(fn).toBeCalledTimes(4) + expect(consola.log).toBeCalledTimes(4) + expect(consola.log).nthCalledWith(1, 1) + expect(consola.log).nthCalledWith(2, 2) + expect(consola.log).nthCalledWith(3, 3) + expect(consola.log).nthCalledWith(4, 4) + }) + + test('should call fn in parallel', async () => { + jest.spyOn(Promise, 'all') + jest.spyOn(Promise, 'resolve') + + await parallel([1, 2, 3, 4], (num, index) => [num, index]) + + expect(Promise.all).toBeCalledTimes(1) + expect(Promise.resolve).toBeCalledTimes(4) + expect(Promise.resolve).nthCalledWith(1, [1, 0]) + expect(Promise.resolve).nthCalledWith(2, [2, 1]) + expect(Promise.resolve).nthCalledWith(3, [3, 2]) + expect(Promise.resolve).nthCalledWith(4, [4, 3]) + + Promise.all.mockRestore() + Promise.resolve.mockRestore() + }) + + test('chainFn (mutate, mutate)', () => { + // Pass more than one argument to test that they're actually taken into account + const firstFn = function (obj, count) { + obj.foo = count + 1 + } + const secondFn = function (obj, count) { + obj.bar = count + 2 + } + + const chainedFn = chainFn(firstFn, secondFn) + expect(chainedFn({}, 10)).toEqual({ foo: 11, bar: 12 }) + }) + + test('chainFn (mutate, return)', () => { + const firstFn = function (obj, count) { + obj.foo = count + 1 + } + const secondFn = function (obj, count) { + return Object.assign({}, obj, { bar: count + 2 }) + } + + const chainedFn = chainFn(firstFn, secondFn) + expect(chainedFn({}, 10)).toEqual({ foo: 11, bar: 12 }) + }) + + test('chainFn (return, mutate)', () => { + const firstFn = function (obj, count) { + return Object.assign({}, obj, { foo: count + 1 }) + } + const secondFn = function (obj, count) { + obj.bar = count + 2 + } + + const chainedFn = chainFn(firstFn, secondFn) + expect(chainedFn({}, 10)).toEqual({ foo: 11, bar: 12 }) + }) + + test('chainFn (return, return)', () => { + const firstFn = function (obj, count) { + return Object.assign({}, obj, { foo: count + 1 }) + } + const secondFn = function (obj, count) { + return Object.assign({}, obj, { bar: count + 2 }) + } + + const chainedFn = chainFn(firstFn, secondFn) + expect(chainedFn({}, 10)).toEqual({ foo: 11, bar: 12 }) + }) + + test('chainFn (return, non-function)', () => { + const firstFn = function (obj, count) { + return Object.assign({}, obj, { foo: count + 1 }) + } + const secondFn = '' + + const chainedFn = chainFn(firstFn, secondFn) + expect(chainedFn).toBe(firstFn) + }) + + test('chainFn (non-function, return)', () => { + const firstFn = '' + const secondFn = function (obj, count) { + return Object.assign({}, obj, { bar: count + 2 }) + } + + const chainedFn = chainFn(firstFn, secondFn) + expect(chainedFn({}, 10)).toEqual({ bar: 12 }) + }) +}) diff --git a/packages/utils/test/timer.test.js b/packages/utils/test/timer.test.js new file mode 100644 index 0000000000..2a89335e8e --- /dev/null +++ b/packages/utils/test/timer.test.js @@ -0,0 +1,165 @@ +import { timeout, waitFor, Timer } from '../src/timer' + +describe('util: timer', () => { + test('timeout (promise)', async () => { + const result = await timeout(Promise.resolve('time not run out'), 100) + expect(result).toEqual('time not run out') + }) + + test('timeout (async function)', async () => { + const result = await timeout(async () => { + await waitFor(10) + return 'time not run out' + }, 100) + expect(result).toEqual('time not run out') + }) + + test('timeout (timeout in 100ms)', async () => { + const call = timeout(waitFor(200), 100, 'timeout test 100ms') + await expect(call).rejects.toThrow('timeout test 100ms') + }) + + test('timeout (async timeout in 100ms)', async () => { + const call = timeout(async () => { + await waitFor(500) + }, 100, 'timeout test 100ms') + await expect(call).rejects.toThrow('timeout test 100ms') + }) + + test('waitFor', async () => { + const delay = 100 + const s = process.hrtime() + await waitFor(delay) + const t = process.hrtime(s) + // Node.js makes no guarantees about the exact timing of when callbacks will fire + // HTML5 specifies a minimum delay of 4ms for timeouts + // although arbitrary, use this value to determine our lower limit + expect((t[0] * 1e9 + t[1]) / 1e6).not.toBeLessThan(delay - 4) + await waitFor() + }) + + describe('util: timer Timer', () => { + beforeAll(() => { + // jest.spyOn() + }) + + test('should construct Timer', () => { + const timer = new Timer() + expect(timer._times).toBeInstanceOf(Map) + }) + + test('should create new time record', () => { + const timer = new Timer() + timer.hrtime = jest.fn(() => 'hrtime') + + const time = timer.start('test', 'test Timer') + + expect(timer.hrtime).toBeCalledTimes(1) + expect(time).toEqual({ description: 'test Timer', name: 'test', start: 'hrtime' }) + }) + + test('should stop and remove time record', () => { + const timer = new Timer() + timer.hrtime = jest.fn(() => 'hrtime') + timer.start('test', 'test Timer') + + const time = timer.end('test') + + expect(timer._times.size).toEqual(0) + expect(timer.hrtime).toBeCalledTimes(2) + expect(timer.hrtime).nthCalledWith(2, 'hrtime') + expect(time).toEqual({ description: 'test Timer', name: 'test', duration: 'hrtime', start: 'hrtime' }) + }) + + test('should be quiet if end with nonexistent time', () => { + const timer = new Timer() + + const time = timer.end('test') + + expect(time).toBeUndefined() + }) + + test('should use bigint hrtime if supports', () => { + const timer = new Timer() + const hrtime = process.hrtime + process.hrtime = { + bigint: jest.fn(() => 'bingint hrtime') + } + + const time = timer.hrtime() + + expect(time).toEqual('bingint hrtime') + expect(process.hrtime.bigint).toBeCalledTimes(1) + + process.hrtime = hrtime + }) + + if (BigInt) { + test('should calculate duration with bigint hrtime', () => { + const timer = new Timer() + const hrtime = process.hrtime + process.hrtime = { + bigint: jest.fn() + .mockReturnValueOnce(BigInt(100000000)) + .mockReturnValueOnce(BigInt(213000000)) + } + + let time = timer.hrtime() + time = timer.hrtime(time) + + expect(time).toEqual(BigInt(113)) + expect(process.hrtime.bigint).toBeCalledTimes(2) + + process.hrtime = hrtime + }) + } + + test('should use hrtime if bigint it not supported', () => { + const timer = new Timer() + const hrtime = process.hrtime + process.hrtime = jest.fn(() => 'hrtime') + process.hrtime.bigint = undefined + + const time = timer.hrtime() + + expect(time).toEqual('hrtime') + expect(process.hrtime).toBeCalledTimes(1) + + process.hrtime = hrtime + }) + + test('should calculate duration with hrtime', () => { + const timer = new Timer() + const hrtime = process.hrtime + process.hrtime = jest.fn() + .mockReturnValueOnce([1, 500000]) + .mockReturnValueOnce([2, 600000]) + process.hrtime.bigint = undefined + + let time = timer.hrtime() + time = timer.hrtime(time) + + expect(time).toEqual(2000.6) + expect(process.hrtime).toBeCalledTimes(2) + expect(process.hrtime).nthCalledWith(1) + expect(process.hrtime).nthCalledWith(2, [1, 500000]) + + process.hrtime = hrtime + }) + + test('should clear all times', () => { + const timer = new Timer() + timer.hrtime = jest.fn(() => 'hrtime') + + timer.start('time-1', 'test time-1') + timer.start('time-2', 'test time-2') + timer.start('time-3', 'test time-3') + + expect(timer._times.size).toEqual(3) + + timer.clear() + + expect(timer._times.size).toEqual(0) + }) + }) +}) diff --git a/test/unit/utils.test.js b/test/unit/utils.test.js index 84515e4712..140465218b 100644 --- a/test/unit/utils.test.js +++ b/test/unit/utils.test.js @@ -1,332 +1,8 @@ -import path from 'path' import { waitUntil } from '../utils' -import * as Utils from '../../packages/utils/src/index' describe('utils', () => { - test('encodeHtml', () => { - const html = '

Hello

' - expect(Utils.encodeHtml(html)).toBe('<h1>Hello</h1>') - }) - - test('getContext', () => { - const ctx = Utils.getContext({ a: 1 }, { b: 2 }) - expect(Utils.getContext.length).toBe(2) - expect(typeof ctx.req).toBe('object') - expect(typeof ctx.res).toBe('object') - expect(ctx.req.a).toBe(1) - expect(ctx.res.b).toBe(2) - }) - - test('waitFor', async () => { - const delay = 100 - const s = process.hrtime() - await Utils.waitFor(delay) - const t = process.hrtime(s) - // Node.js makes no guarantees about the exact timing of when callbacks will fire - // HTML5 specifies a minimum delay of 4ms for timeouts - // although arbitrary, use this value to determine our lower limit - expect((t[0] * 1e9 + t[1]) / 1e6).not.toBeLessThan(delay - 4) - await Utils.waitFor() - }) - test('waitUntil', async () => { expect(await waitUntil(() => true, 0.1, 100)).toBe(false) expect(await waitUntil(() => false, 0.1, 100)).toBe(true) }) - - test('timeout (promise)', async () => { - const result = await Utils.timeout(Promise.resolve('time not run out'), 100) - expect(result).toBe('time not run out') - }) - - test('timeout (async function)', async () => { - const result = await Utils.timeout(async () => { - await Utils.waitFor(10) - return 'time not run out' - }, 100) - expect(result).toBe('time not run out') - }) - - test('timeout (timeout in 100ms)', async () => { - const timeout = Utils.timeout(Utils.waitFor(200), 100, 'timeout test 100ms') - await expect(timeout).rejects.toThrow('timeout test 100ms') - }) - - test('timeout (async timeout in 100ms)', async () => { - const timeout = Utils.timeout(async () => { - await Utils.waitFor(500) - }, 100, 'timeout test 100ms') - await expect(timeout).rejects.toThrow('timeout test 100ms') - }) - - test('urlJoin', () => { - expect(Utils.urlJoin('test', '/about')).toBe('test/about') - }) - - test('promisifyRoute (array)', () => { - const array = [1] - const promise = Utils.promisifyRoute(array) - expect(typeof promise).toBe('object') - return promise.then((res) => { - expect(res).toBe(array) - }) - }) - - test('promisifyRoute (fn => array)', () => { - const array = [1, 2] - const fn = function () { - return array - } - const promise = Utils.promisifyRoute(fn) - expect(typeof promise).toBe('object') - return promise.then((res) => { - expect(res).toBe(array) - }) - }) - - test('promisifyRoute (fn => promise)', () => { - const array = [1, 2, 3] - const fn = function () { - return new Promise((resolve) => { - resolve(array) - }) - } - const promise = Utils.promisifyRoute(fn) - expect(typeof promise).toBe('object') - return promise.then((res) => { - expect(res).toBe(array) - }) - }) - - test('promisifyRoute ((fn(args) => promise))', () => { - const fn = function (array) { - return new Promise((resolve) => { - resolve(array) - }) - } - const array = [1, 2, 3] - const promise = Utils.promisifyRoute(fn, array) - expect(typeof promise).toBe('object') - return promise.then((res) => { - expect(res).toBe(array) - }) - }) - - test('promisifyRoute (fn(cb) with error)', () => { - const fn = function (cb) { - cb(new Error('Error here')) - } - const promise = Utils.promisifyRoute(fn) - expect(typeof promise).toBe('object') - return promise.catch((e) => { - expect(e.message).toBe('Error here') - }) - }) - - test('promisifyRoute (fn(cb, args) with error)', () => { - const fn = function (cb, array) { - cb(new Error('Error here: ' + array.join())) - } - const array = [1, 2, 3, 4] - const promise = Utils.promisifyRoute(fn, array) - expect(typeof promise).toBe('object') - return promise.catch((e) => { - expect(e.message).toBe('Error here: ' + array.join()) - }) - }) - - test('promisifyRoute (fn(cb) with result)', () => { - const array = [1, 2, 3, 4] - const fn = function (cb) { - cb(null, array) - } - const promise = Utils.promisifyRoute(fn) - expect(typeof promise).toBe('object') - return promise.then((res) => { - expect(res).toBe(array) - }) - }) - - test('promisifyRoute (fn(cb, args) with result)', () => { - const fn = function (cb, array, object) { - cb(null, { array, object }) - } - const array = [1, 2, 3, 4] - const object = { a: 1 } - const promise = Utils.promisifyRoute(fn, array, object) - expect(typeof promise).toBe('object') - return promise.then((res) => { - expect(res.array).toBe(array) - expect(res.object).toBe(object) - }) - }) - - test('chainFn (mutate, mutate)', () => { - // Pass more than one argument to test that they're actually taken into account - const firstFn = function (obj, count) { - obj.foo = count + 1 - } - const secondFn = function (obj, count) { - obj.bar = count + 2 - } - - const chainedFn = Utils.chainFn(firstFn, secondFn) - expect(chainedFn({}, 10)).toEqual({ foo: 11, bar: 12 }) - }) - - test('chainFn (mutate, return)', () => { - const firstFn = function (obj, count) { - obj.foo = count + 1 - } - const secondFn = function (obj, count) { - return Object.assign({}, obj, { bar: count + 2 }) - } - - const chainedFn = Utils.chainFn(firstFn, secondFn) - expect(chainedFn({}, 10)).toEqual({ foo: 11, bar: 12 }) - }) - - test('chainFn (return, mutate)', () => { - const firstFn = function (obj, count) { - return Object.assign({}, obj, { foo: count + 1 }) - } - const secondFn = function (obj, count) { - obj.bar = count + 2 - } - - const chainedFn = Utils.chainFn(firstFn, secondFn) - expect(chainedFn({}, 10)).toEqual({ foo: 11, bar: 12 }) - }) - - test('chainFn (return, return)', () => { - const firstFn = function (obj, count) { - return Object.assign({}, obj, { foo: count + 1 }) - } - const secondFn = function (obj, count) { - return Object.assign({}, obj, { bar: count + 2 }) - } - - const chainedFn = Utils.chainFn(firstFn, secondFn) - expect(chainedFn({}, 10)).toEqual({ foo: 11, bar: 12 }) - }) - - test('flatRoutes', () => { - const routes = Utils.flatRoutes([ - { name: 'login', path: '/login' }, - { name: 'about', path: '/about' }, - { name: 'posts', - path: '', - children: [ - { name: 'posts-list', - path: '' - }, - { name: 'posts-create', - path: 'post' - } - ] - } - ]) - expect(routes).toMatchObject([ '/login', '/about', '', '/post' ]) - }) - - describe('relativeTo', () => { - const path1 = path.join(path.sep, 'foo', 'bar') - const path2 = path.join(path.sep, 'foo', 'baz') - - test('makes path relative to dir', () => { - expect(Utils.relativeTo(path1, path2)).toBe(Utils.wp(`..${path.sep}baz`)) - }) - - test('keeps webpack inline loaders prepended', () => { - expect(Utils.relativeTo(path1, `loader1!loader2!${path2}`)) - .toBe(Utils.wp(`loader1!loader2!..${path.sep}baz`)) - }) - }) - - describe('guardDir', () => { - test('Parent dir is guarded', () => { - expect(() => { - Utils.guardDir({ - dir1: '/root/parent', - dir2: '/root' - }, 'dir1', 'dir2') - }).toThrow() - }) - - test('Same dir is guarded', () => { - expect(() => { - Utils.guardDir({ - dir1: '/root/parent', - dir2: '/root/parent' - }, 'dir1', 'dir2') - }).toThrow() - }) - - test('Same level dir is not guarded', () => { - expect(() => { - Utils.guardDir({ - dir1: '/root/parent-next', - dir2: '/root/parent' - }, 'dir1', 'dir2') - }).not.toThrow() - }) - - test('Same level dir is not guarded 2', () => { - expect(() => { - Utils.guardDir({ - dir1: '/root/parent', - dir2: '/root/parent-next' - }, 'dir1', 'dir2') - }).not.toThrow() - }) - - test('Child dir is not guarded', () => { - expect(() => { - Utils.guardDir({ - dir1: '/root/parent', - dir2: '/root/parent/child' - }, 'dir1', 'dir2') - }).not.toThrow() - }) - }) -}) - -test('createRoutes should allow snake case routes', () => { - const files = [ - 'pages/_param.vue', - 'pages/subpage/_param.vue', - 'pages/snake_case_route.vue', - 'pages/another_route/_id.vue' - ] - const srcDir = '/some/nuxt/app' - const pagesDir = 'pages' - const routesResult = Utils.createRoutes(files, srcDir, pagesDir) - const expectedResult = [ - { - name: 'snake_case_route', - path: '/snake_case_route', - component: Utils.r('/some/nuxt/app/pages/snake_case_route.vue'), - chunkName: 'pages/snake_case_route' - }, - { - name: 'another_route-id', - path: '/another_route/:id?', - component: Utils.r('/some/nuxt/app/pages/another_route/_id.vue'), - chunkName: 'pages/another_route/_id' - }, - { - name: 'subpage-param', - path: '/subpage/:param?', - component: Utils.r('/some/nuxt/app/pages/subpage/_param.vue'), - chunkName: 'pages/subpage/_param' - }, - { - name: 'param', - path: '/:param?', - component: Utils.r('/some/nuxt/app/pages/_param.vue'), - chunkName: 'pages/_param' - } - ] - - expect(routesResult).toEqual(expectedResult) })