feat(icons): add and test icon generation for tauri (#55)

This commit is contained in:
nothingismagick
2019-11-17 22:30:14 +01:00
committed by GitHub
parent 94ead187dc
commit 596f6218e6
19 changed files with 2132 additions and 85 deletions

9
.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

1
.gitignore vendored
View File

@@ -62,3 +62,4 @@ package-lock.json
src-tauri src-tauri
test/jest/tmp

View File

@@ -1,7 +1,7 @@
# tauri [WIP] # tauri [WIP]
## A fresh take on creating cross-platform apps. ## A fresh take on creating cross-platform apps.
[![status](https://img.shields.io/badge/Status-Internal%20Review-yellow.svg)](https://github.com/quasarframework/quasar/tree/tauri) [![status](https://img.shields.io/badge/Status-Internal%20Review-yellow.svg)](https://github.com/quasarframework/quasar/tree/tauri)
[![version](https://img.shields.io/badge/Version-unreleased-yellow.svg)](https://github.com/tauri-apps/tauri/tree/dev) <img align="right" src="/tauri-logo.png" height="240" width="240"> [![version](https://img.shields.io/badge/Version-unreleased-yellow.svg)](https://github.com/tauri-apps/tauri/tree/dev) <img align="right" src="/app-icon.png" height="240" width="240">
[![Chat Server](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/SpmNs4S) [![Chat Server](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/SpmNs4S)
[![devto](https://img.shields.io/badge/dev.to-blog-black.svg)](https://dev.to/tauri) [![devto](https://img.shields.io/badge/dev.to-blog-black.svg)](https://dev.to/tauri)

View File

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View File

@@ -11,7 +11,7 @@ module.exports = {
collectCoverage: true, collectCoverage: true,
coverageDirectory: '<rootDir>/test/jest/coverage', coverageDirectory: '<rootDir>/test/jest/coverage',
collectCoverageFrom: [ collectCoverageFrom: [
'<rootDir>/mode/**/*.js', '<rootDir>/mode/**/*.js'
], ],
coverageReporters: ['json-summary', 'text', 'lcov'], coverageReporters: ['json-summary', 'text', 'lcov'],
coverageThreshold: { coverageThreshold: {
@@ -29,7 +29,8 @@ module.exports = {
moduleFileExtensions: ['js', 'json'], moduleFileExtensions: ['js', 'json'],
moduleNameMapper: { moduleNameMapper: {
'^~/(.*)$': '<rootDir>/$1', '^~/(.*)$': '<rootDir>/$1',
'^mode/(.*)$': '<rootDir>/mode/$1' '^mode/(.*)$': '<rootDir>/mode/$1',
'^test/(.*)$': '<rootDir>/test/$1'
}, },
transform: {} transform: {}
} }

View File

@@ -1,5 +1,4 @@
const const parseArgs = require('minimist')
parseArgs = require('minimist')
const argv = parseArgs(process.argv.slice(2), { const argv = parseArgs(process.argv.slice(2), {
alias: { alias: {

61
mode/bin/tauri-icon.js Normal file
View File

@@ -0,0 +1,61 @@
const parseArgs = require('minimist')
const { appDir, tauriDir } = require('../helpers/app-paths')
const logger = require('../helpers/logger')
const log = logger('app:tauri')
const warn = logger('app:tauri (icon)', 'red')
const { tauricon } = require('../helpers/tauricon')
const { resolve } = require('path')
/**
* @type {object}
* @property {boolean} h
* @property {boolean} help
* @property {string|boolean} f
* @property {string|boolean} force
* @property {boolean} l
* @property {boolean} log
* @property {boolean} c
* @property {boolean} config
* @property {boolean} s
* @property {boolean} source
* @property {boolean} t
* @property {boolean} target
*/
const argv = parseArgs(process.argv.slice(2), {
alias: {
h: 'help',
l: 'log',
c: 'config',
s: 'source',
t: 'target'
},
boolean: ['h', 'l']
})
if (argv.help) {
console.log(`
Description
Create all the icons you need for your Tauri app.
Usage
$ tauri icon
Options
--help, -h Displays this message
--log, l Logging [boolean]
--icon, i Source icon (png, 1240x1240 with transparency)
--target, t Target folder (default: 'src-tauri/icons')
--compression, c Compression type [pngquant|optipng|zopfli]
`)
process.exit(0)
}
tauricon.make(
argv.i || resolve(appDir, 'app-icon.png'),
argv.t || resolve(tauriDir, 'icons'),
argv.c || 'optipng'
).then(() => {
log('(tauricon) Completed')
}).catch(e => {
warn(e)
})

View File

@@ -1,5 +1,4 @@
const const parseArgs = require('minimist')
parseArgs = require('minimist')
const appPaths = require('../helpers/app-paths') const appPaths = require('../helpers/app-paths')
const logger = require('../helpers/logger') const logger = require('../helpers/logger')
const log = logger('app:tauri') const log = logger('app:tauri')
@@ -13,6 +12,8 @@ const warn = logger('app:tauri (init)', 'red')
* @property {string|boolean} force * @property {string|boolean} force
* @property {boolean} l * @property {boolean} l
* @property {boolean} log * @property {boolean} log
* @property {boolean} d
* @property {boolean} directory
*/ */
const argv = parseArgs(process.argv.slice(2), { const argv = parseArgs(process.argv.slice(2), {
alias: { alias: {
@@ -44,7 +45,7 @@ const { inject } = require('../template')
const target = appPaths.tauriDir const target = appPaths.tauriDir
if (inject(target, 'all', argv.f, argv.l, argv.d)) { if (inject(target, 'all', argv.f || null, argv.l || null, argv.d || null)) {
log('tauri init successful') log('tauri init successful')
} else { } else {
warn('tauri init unsuccessful') warn('tauri init unsuccessful')

View File

@@ -1,10 +1,18 @@
#!/usr/bin/env node #!/usr/bin/env node
const cmds = ['init', 'dev', 'build', 'help'] const cmds = ['init', 'dev', 'build', 'help', 'icon']
const cmd = process.argv[2] const cmd = process.argv[2]
/**
* @description This is the bootstrapper that in turn calls subsequent
* Tauri Commands
*
* @param {string|array} command
*/
const tauri = function (command) { const tauri = function (command) {
if (typeof command === 'object') { // technically we just care about an array
command = command[0]
}
if (!command || command === '-h' || command === '--help' || command === 'help') { if (!command || command === '-h' || command === '--help' || command === 'help') {
console.log(` console.log(`
Description Description

View File

@@ -1,5 +1,4 @@
const const { existsSync } = require('fs')
{ existsSync } = require('fs')
const { resolve, join, normalize, sep } = require('path') const { resolve, join, normalize, sep } = require('path')
/** /**

View File

@@ -0,0 +1,92 @@
exports.options = {
// folder determines in which path to drop the generated file
// prefix is the first part of the generated file's name
// infix adds e.g. '44x44' based on the size in sizes to the generated file's name
// suffix adds a file-ending to the generated file's name
// sizes determines the pixel width and height to use
background_color: '#000074',
theme_color: '#02aa9b',
sharp: 'kernel: sharp.kernel.lanczos3', // one of [nearest|cubic|lanczos2|lanczos3]
minify: {
batch: false,
overwrite: true,
available: ['pngquant', 'optipng', 'zopfli'],
type: 'pngquant',
pngcrushOptions: {
reduce: true
},
pngquantOptions: {
quality: [0.6, 0.8],
floyd: 0.1, // 0.1 - 1
speed: 10 // 1 - 10
},
optipngOptions: {
optimizationLevel: 4,
bitDepthReduction: true,
colorTypeReduction: true,
paletteReduction: true
},
zopfliOptions: {
transparent: true,
more: true
}
},
splash_type: 'generate',
tauri: {
linux: {
folder: '.',
prefix: '',
infix: true,
suffix: '.png',
sizes: [
32, 128
]
},
linux_2x: {
folder: '.',
prefix: '128x128@2x',
infix: false,
suffix: '.png',
sizes: [
256
]
},
defaults: {
folder: '.',
prefix: 'icon',
infix: false,
suffix: '.png',
sizes: [
512
]
},
appx_logo: {
folder: '.',
prefix: 'StoreLogo',
infix: false,
suffix: '.png',
sizes: [
50
]
},
appx_square: {
folder: '.',
prefix: 'Square',
infix: true,
suffix: 'Logo.png',
sizes: [
30,
44,
71,
89,
107,
142,
150,
284,
310
]
}
// todo: look at capacitor and cordova for insight into what icons
// we need for those distribution targets
}
}

420
mode/helpers/tauricon.js Normal file
View File

@@ -0,0 +1,420 @@
'use strict'
/**
* This is a module that takes an original image and resizes
* it to common icon sizes and will put them in a folder.
* It will retain transparency and can make special file
* types. You can control the settings.
*
* @module tauricon
* @exports tauricon
* @author Daniel Thompson-Yvetot
* @license MIT
*/
const path = require('path')
const sharp = require('sharp')
const imagemin = require('imagemin')
const pngquant = require('imagemin-pngquant')
const optipng = require('imagemin-optipng')
const zopfli = require('imagemin-zopfli')
const png2icons = require('png2icons')
const readChunk = require('read-chunk')
const isPng = require('is-png')
const settings = require('./tauricon.config')
let image = false
const {
access,
writeFileSync,
ensureDir,
ensureFileSync
} = require('fs-extra')
const exists = async function (file) {
try {
await access(file)
return true
} catch (err) {
return false
}
}
/**
* This is the first call that attempts to memoize the sharp(src).
* If the source image cannot be found or if it is not a png, it
* is a failsafe that will exit or throw.
*
* @param {string} src - a folder to target
* @exits {error} if not a png, if not an image
*/
const checkSrc = async function (src) {
if (image !== false) {
return image
} else {
const srcExists = await exists(src)
if (!srcExists) {
image = false
throw new Error('[ERROR] Source image for tauricon not found')
} else {
const buffer = await readChunk(src, 0, 8)
if (isPng(buffer) === true) {
return (image = sharp(src))
} else {
image = false
throw new Error('[ERROR] Source image for tauricon is not a png')
// exit because this is BAD!
// Developers should catch () { } this as it is
// the last chance to stop bad things happening.
}
}
}
}
/**
* Sort the folders in the current job for unique folders.
*
* @param {object} options - a subset of the settings
* @returns {array} folders
*/
const uniqueFolders = function (options) {
let folders = []
for (const type in options) {
if (options[type].folder) {
folders.push(options[type].folder)
}
}
folders = folders.sort().filter((x, i, a) => !i || x !== a[i - 1])
return folders
}
/**
* Turn a hex color (like #212342) into r,g,b values
*
* @param {string} hex - hex colour
* @returns {array} r,g,b
*/
const hexToRgb = function (hex) {
// https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
hex = hex.replace(shorthandRegex, function (m, r, g, b) {
return r + r + g + g + b + b
})
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
}
: null
}
/**
* validate image and directory
* @param {string} src
* @param {string} target
* @returns {Promise<void>}
*/
const validate = async function (src, target) {
if (target !== undefined) {
await ensureDir(target)
}
return checkSrc(src)
}
/**
* Log progress in the command line
*
* @param {string} msg
* @param {boolean} end
*/
const progress = function (msg) {
console.log(msg)
// process.stdout.write(` ${msg} \r`)
}
/**
* Create a spinner on the command line
*
* @example
*
* const spinnerInterval = spinner()
* // later
* clearInterval(spinnerInterval)
* @returns {function} - the interval object
*/
const spinner = function () {
return setInterval(() => {
process.stdout.write('/ \r')
setTimeout(() => {
process.stdout.write('- \r')
setTimeout(() => {
process.stdout.write('\\ \r')
setTimeout(() => {
process.stdout.write('| \r')
}, 100)
}, 100)
}, 100)
}, 500)
}
const tauricon = exports.tauricon = {
validate: async function (src, target) {
await validate(src, target)
return typeof image === 'object'
},
version: function () {
return require('../../package.json').version
},
/**
*
* @param {string} src
* @param {string} target
* @param {string} strategy
* @param {object} options
*/
make: async function (src, target, strategy, options) {
const spinnerInterval = spinner()
options = options || settings.options.tauri
await this.validate(src, target)
progress('Building Tauri icns and ico')
await this.icns(src, target, options, strategy)
progress('Building Tauri png icons')
await this.build(src, target, options)
if (strategy) {
progress(`Minifying assets with ${strategy}`)
await this.minify(target, options, strategy, 'batch')
} else {
console.log('no minify strategy')
}
progress('Tauricon Finished')
clearInterval(spinnerInterval)
return true
},
/**
* Creates a set of images according to the subset of options it knows about.
*
* @param {string} src - image location
* @param {string} target - where to drop the images
* @param {object} options - js object that defines path and sizes
*/
build: async function (src, target, options) {
await this.validate(src, target) // creates the image object
const buildify2 = async function (pvar) {
try {
const pngImage = image.resize(pvar[1], pvar[1])
if (pvar[2]) {
const rgb = hexToRgb(options.background_color)
pngImage.flatten({
background: { r: rgb.r, g: rgb.g, b: rgb.b, alpha: 1 }
})
}
pngImage.png()
await pngImage.toFile(pvar[0])
} catch (err) {
console.log(err)
}
}
let output
const folders = uniqueFolders(options)
for (const n in folders) {
// make the folders first
ensureDir(`${target}${path.sep}${folders[n]}`)
}
for (const optionKey in options) {
const option = options[optionKey]
// chain up the transforms
for (const sizeKey in option.sizes) {
const size = option.sizes[sizeKey]
if (!option.splash) {
const dest = `${target}/${option.folder}`
if (option.infix === true) {
output = `${dest}${path.sep}${option.prefix}${size}x${size}${option.suffix}`
} else {
output = `${dest}${path.sep}${option.prefix}${option.suffix}`
}
const pvar = [output, size, option.background]
await buildify2(pvar)
}
}
}
},
/**
* Creates a set of splash images (COMING SOON!!!)
*
* @param {string} src - icon location
* @param {string} splashSrc - splashscreen location
* @param {string} target - where to drop the images
* @param {object} options - js object that defines path and sizes
*/
splash: async function (src, splashSrc, target, options) {
let output
let block = false
const rgb = hexToRgb(options.background_color)
// three options
// options: splashscreen_type [generate | overlay | pure]
// - generate (icon + background color) DEFAULT
// - overlay (icon + splashscreen)
// - pure (only splashscreen)
let sharpSrc
if (splashSrc === src) {
// prevent overlay or pure
block = true
}
if (block === true || options.splashscreen_type === 'generate') {
await this.validate(src, target)
if (!image) {
process.exit(1)
}
sharpSrc = sharp(src)
sharpSrc.extend({
top: 726,
bottom: 726,
left: 726,
right: 726,
background: {
r: rgb.r,
g: rgb.g,
b: rgb.b,
alpha: 1
}
})
.flatten({ background: { r: rgb.r, g: rgb.g, b: rgb.b, alpha: 1 } })
} else if (options.splashscreen_type === 'overlay') {
sharpSrc = sharp(splashSrc)
.flatten({ background: { r: rgb.r, g: rgb.g, b: rgb.b, alpha: 1 } })
.composite([{
input: src
// blend: 'multiply' <= future work, maybe just a gag
}])
} else if (options.splashscreen_type === 'pure') {
sharpSrc = sharp(splashSrc)
.flatten({ background: { r: rgb.r, g: rgb.g, b: rgb.b, alpha: 1 } })
}
const data = await sharpSrc.toBuffer()
for (const optionKey in options) {
const option = options[optionKey]
for (const sizeKey in option.sizes) {
const size = option.sizes[sizeKey]
if (option.splash) {
const dest = `${target}${path.sep}${option.folder}`
await ensureDir(dest)
if (option.infix === true) {
output = `${dest}${path.sep}${option.prefix}${size}x${size}${option.suffix}`
} else {
output = `${dest}${path.sep}${option.prefix}${option.suffix}`
}
// console.log('p1', output, size)
const pvar = [output, size]
let sharpData = sharp(data)
sharpData = sharpData.resize(pvar[1][0], pvar[1][1])
await sharpData.toFile(pvar[0])
}
}
}
},
/**
* Minifies a set of images
*
* @param {string} target - image location
* @param {object} options - where to drop the images
* @param {string} strategy - which minify strategy to use
* @param {string} mode - singlefile or batch
*/
minify: async function (target, options, strategy, mode) {
let cmd
const minify = settings.options.minify
if (!minify.available.find(x => x === strategy)) {
strategy = minify.type
}
switch (strategy) {
case 'pngquant':
cmd = pngquant(minify.pngquantOptions)
break
case 'optipng':
cmd = optipng(minify.optipngOptions)
break
case 'zopfli':
cmd = zopfli(minify.zopfliOptions)
break
}
const __minifier = async (pvar) => {
await imagemin([pvar[0]], {
destination: pvar[1],
plugins: [cmd]
}).catch(err => {
console.log(err)
})
}
switch (mode) {
case 'singlefile':
await __minifier([target, path.dirname(target)], cmd)
break
case 'batch':
// eslint-disable-next-line no-case-declarations
const folders = uniqueFolders(options)
for (const n in folders) {
console.log('batch minify:', folders[n])
await __minifier([
`${target}${path.sep}${folders[n]}${path.sep}*.png`,
`${target}${path.sep}${folders[n]}`
], cmd)
}
break
default:
console.error('* [ERROR] Minify mode must be one of [ singlefile | batch]')
process.exit(1)
}
return 'minified'
},
/**
* Creates special icns and ico filetypes
*
* @param {string} src - image location
* @param {string} target - where to drop the images
* @param {object} options
* @param {string} strategy
*/
icns: async function (src, target, options, strategy) {
try {
if (!image) {
process.exit(1)
}
await this.validate(src, target)
const sharpSrc = sharp(src)
const buf = await sharpSrc.toBuffer()
const out = await png2icons.createICNS(buf, png2icons.BICUBIC, 0)
ensureFileSync(path.join(target, '/icon.icns'))
writeFileSync(path.join(target, '/icon.icns'), out)
const out2 = await png2icons.createICO(buf, png2icons.BICUBIC, 0, true)
ensureFileSync(path.join(target, '/icon.ico'))
writeFileSync(path.join(target, '/icon.ico'), out2)
} catch (err) {
console.error(err)
}
}
}
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = tauricon
}
exports.tauricon = tauricon
}

View File

@@ -36,10 +36,18 @@
"fast-glob": "^3.0.4", "fast-glob": "^3.0.4",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"html-webpack-inline-source-plugin": "^0.0.10", "html-webpack-inline-source-plugin": "^0.0.10",
"imagemin": "^7.0.1",
"imagemin-optipng": "^7.1.0",
"imagemin-pngquant": "^8.0.0",
"imagemin-zopfli": "^6.0.0",
"is-png": "^2.0.0",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.template": "^4.5.0", "lodash.template": "^4.5.0",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"ms": "^2.1.2", "ms": "^2.1.2",
"png2icons": "^2.0.1",
"read-chunk": "^3.2.0",
"sharp": "^0.23.2",
"webpack-merge": "^4.2.1" "webpack-merge": "^4.2.1"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,34 +1,32 @@
const { tauri } = require('mode/bin/tauri') const { tauri } = require('mode/bin/tauri')
// const mockProcess = require('jest-mock-process')
describe('[CLI] tauri.js', () => { describe('[CLI] tauri.js', () => {
it('displays a help message', async () => { it('displays a help message', async () => {
jest.spyOn(console, 'log') jest.spyOn(console, 'log')
jest.spyOn(process, 'exit').mockImplementation(() => true) jest.spyOn(process, 'exit').mockImplementation(() => true)
let result = tauri('help') tauri('help')
console.log(process.exit.mock.calls[0][0]) console.log(process.exit.mock.calls[0][0])
expect(process.exit.mock.calls[0][0]).toBe(0) expect(process.exit.mock.calls[0][0]).toBe(0)
// console.log(console.log.mock.calls[0][0])
expect(!!console.log.mock.calls[0][0]).toBe(true) expect(!!console.log.mock.calls[0][0]).toBe(true)
result = tauri('--help') tauri('--help')
// console.log(console.log.mock.calls[2][0])
expect(!!console.log.mock.calls[2][0]).toBe(true) expect(!!console.log.mock.calls[2][0]).toBe(true)
result = tauri('-h') tauri('-h')
expect(!!console.log.mock.calls[3][0]).toBe(true) expect(!!console.log.mock.calls[3][0]).toBe(true)
tauri(['help'])
expect(!!console.log.mock.calls[4][0]).toBe(true)
jest.clearAllMocks() jest.clearAllMocks()
}) })
it('will not run an unavailable command', async () => { it('will not run an unavailable command', async () => {
jest.spyOn(console, 'log') jest.spyOn(console, 'log')
let result = tauri('foo') tauri('foo')
expect(console.log.mock.calls[0][0].split('.')[0]).toBe('Invalid command foo') expect(console.log.mock.calls[0][0].split('.')[0]).toBe('Invalid command foo')
jest.clearAllMocks() jest.clearAllMocks()
}) })
it('will pass on an available command', async () => { it('will pass on an available command', async () => {
jest.spyOn(console, 'log') jest.spyOn(console, 'log')
let result = tauri('init') tauri('init')
expect(console.log.mock.calls[0][0].split('.')[0]).toBe('[tauri]: running init') expect(console.log.mock.calls[0][0].split('.')[0]).toBe('[tauri]: running init')
jest.clearAllMocks() jest.clearAllMocks()
}) })

View File

@@ -0,0 +1,56 @@
const { tauricon } = require('mode/helpers/tauricon')
const { tauri } = require('mode/bin/tauri')
describe('[CLI] tauri-icon internals', () => {
it('tells you the version', () => {
const version = tauricon.version()
expect(!!version).toBe(true)
})
it('gets you help', async () => {
jest.spyOn(console, 'log')
tauri(['icon', 'help'])
expect(!!console.log.mock.calls[0][0]).toBe(true)
jest.clearAllMocks()
})
it('will not validate a non-file', async () => {
try {
await tauricon.validate('test/jest/fixtures/doesnotexist.png', 'test/jest/fixtures/')
} catch (e) {
expect(e.message).toBe('[ERROR] Source image for tauricon not found')
}
})
it('will not validate a non-png', async () => {
try {
await tauricon.validate('test/jest/fixtures/notAMeme.jpg', 'test/jest/fixtures/')
} catch (e) {
expect(e.message).toBe('[ERROR] Source image for tauricon is not a png')
}
})
it('can validate an image as PNG', async () => {
const valid = await tauricon.validate('test/jest/fixtures/tauri-logo.png', 'test/jest/fixtures/')
expect(valid).toBe(true)
})
})
/**
* This test suite takes A LOT of time. Maybe 5 minutes...? You may blame
* Zopfli, but don't blame us for trying to help you get the smallest
* possible binaries!
*/
describe('[CLI] tauri-icon builder', () => {
it('makes a set of icons with pngquant', async () => {
const valid = await tauricon.make('test/jest/fixtures/tauri-logo.png', 'test/jest/tmp/pngquant', 'pngquant')
expect(valid).toBe(true)
})
it('makes a set of icons with optipng', async () => {
const valid = await tauricon.make('test/jest/fixtures/tauri-logo.png', 'test/jest/tmp/optipng', 'optipng')
expect(valid).toBe(true)
})
it('makes a set of icons with zopfli', async () => {
const valid = await tauricon.make('test/jest/fixtures/tauri-logo.png', 'test/jest/tmp/zopfli', 'zopfli')
expect(valid).toBe(true)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

View File

@@ -1,4 +1,4 @@
jest.setTimeout(1000) jest.setTimeout(50000)
global.Promise = require('promise') global.Promise = require('promise')
@@ -6,5 +6,4 @@ setTimeout(() => {
// do nothing // do nothing
}, 1) }, 1)
require('dotenv').config({ path: '.env.jest' }) require('dotenv').config({ path: '.env.jest' })

1457
yarn.lock

File diff suppressed because it is too large Load Diff