Compare commits
176 Commits
master
...
gatsby-hoo
Author | SHA1 | Date | |
---|---|---|---|
|
3b07ed2ab5 | ||
|
97878c3881 | ||
|
14c1e14f51 | ||
|
1621e0e16a | ||
|
8d78f41ed6 | ||
|
42b3d0a9d8 | ||
|
3105371954 | ||
|
18427f9fc6 | ||
|
f9b3d5dbd7 | ||
|
c6020a225f | ||
|
e01b6d424e | ||
|
94d7f3d3ce | ||
|
8186e1cf87 | ||
|
461d8aa7ac | ||
|
110543a537 | ||
|
aa10ecf18c | ||
|
280412e2db | ||
|
dd8c405482 | ||
|
ead8927b02 | ||
|
d46ce46b93 | ||
|
07cd0a4799 | ||
|
0473c27e39 | ||
|
26e0776f1d | ||
|
c8068762b5 | ||
|
f63e9b2a47 | ||
|
e449eade9d | ||
|
84fd88f1d0 | ||
|
f11c41b05e | ||
|
0d512a1a4d | ||
|
a1281543e2 | ||
|
53def33627 | ||
|
791142005e | ||
|
4d31079ca2 | ||
|
e29b40990c | ||
|
092fd39da6 | ||
|
ace907779f | ||
|
383197a4c1 | ||
|
ced6c42c3d | ||
|
9f15287fcc | ||
|
5d700f47aa | ||
|
58dc36b0c6 | ||
|
d813fdf742 | ||
|
babab418c7 | ||
|
143a18807e | ||
|
4fd02d661d | ||
|
728a7fbb14 | ||
|
2f7c67c953 | ||
|
b5cde63e4e | ||
|
274fdf038d | ||
|
51c21881cc | ||
|
59ebe433b6 | ||
|
5a782b439e | ||
|
b3b7480358 | ||
|
b7393b3a4b | ||
|
a13f26286d | ||
|
8830fad923 | ||
|
47ee62d387 | ||
|
ee915c39dc | ||
|
f4e7bc0e76 | ||
|
c1716570a8 | ||
|
3920c716e4 | ||
|
91ab1dbd05 | ||
|
67ca83ff16 | ||
|
97509773af | ||
|
cdb77255a7 | ||
|
b47d03cb31 | ||
|
3fcf31bc48 | ||
|
9d7da52ee3 | ||
|
4e27c4ef87 | ||
|
4f1ad26635 | ||
|
6c603b7958 | ||
|
f71d707e23 | ||
|
67d970c837 | ||
|
35efa7cdb0 | ||
|
fe714f2363 | ||
|
a118519c3a | ||
|
f16a51abcb | ||
|
1ee3055f37 | ||
|
e70705be5f | ||
|
754868b9d5 | ||
|
d4aa207f75 | ||
|
b299d32fc3 | ||
|
21c392752e | ||
|
3378c68aed | ||
|
57dbea8c40 | ||
|
fcf9a354f4 | ||
|
f9b34ebd94 | ||
|
ef8b3a4bde | ||
|
d57a4c1147 | ||
|
9e0cf951d2 | ||
|
86552860f6 | ||
|
1bb01ab8eb | ||
|
2c8b779793 | ||
|
83de8ebcbc | ||
|
f776d19404 | ||
|
cfd7e1ab02 | ||
|
d41dad14a1 | ||
|
a23e72d633 | ||
|
67771e07b0 | ||
|
f0233ee030 | ||
|
c7ea0659f4 | ||
|
325f01f034 | ||
|
d48b48bffc | ||
|
c4a74ad244 | ||
|
3f692fc20b | ||
|
bbdc5a3b12 | ||
|
8c312a450c | ||
|
e77763d0b0 | ||
|
9200c1a8e3 | ||
|
46c956e3da | ||
|
4b7f55382f | ||
|
eab20afe1c | ||
|
7261b0b526 | ||
|
bf44bce954 | ||
|
60449249d0 | ||
|
c14aa078b1 | ||
|
f1a2dfdd34 | ||
|
3eb0689ff3 | ||
|
5de72ffb97 | ||
|
c3116bf5b6 | ||
|
152cf7f7b3 | ||
|
89bac8953b | ||
|
42a1788c52 | ||
|
f41518bd92 | ||
|
13cfcca85e | ||
|
2d754227b1 | ||
|
d8ceec1c07 | ||
|
abe7879b08 | ||
|
8187865f1f | ||
|
1336862bce | ||
|
024eb57603 | ||
|
d589329883 | ||
|
786cd06cd9 | ||
|
1f5da0c690 | ||
|
9a4f669c2d | ||
|
a4450b34b3 | ||
|
0606325d6d | ||
|
50200ae72f | ||
|
befcac2087 | ||
|
ea8e4fba08 | ||
|
b774babfb9 | ||
|
cf0ec81730 | ||
|
297bb650ac | ||
|
70b489f2a6 | ||
|
aca30c8df3 | ||
|
1e4e5d82d1 | ||
|
f14e018518 | ||
|
2a77792165 | ||
|
ba8461c281 | ||
|
02f6f2d252 | ||
|
8426eaa433 | ||
|
3b11fcb0b6 | ||
|
bf35f26d5b | ||
|
618b21bb93 | ||
|
e1c4cb9068 | ||
|
7d7916baf0 | ||
|
837b8d77df | ||
|
8a3471b916 | ||
|
6cff032efb | ||
|
c26bf26bd1 | ||
|
a5babf8965 | ||
|
1655a7898e | ||
|
533475e613 | ||
|
a7ebcd92bf | ||
|
3ce3a886ed | ||
|
fb4a130b3c | ||
|
c572501d51 | ||
|
6f391264be | ||
|
6fd1035cf6 | ||
|
9519afa75a | ||
|
819d4be1a5 | ||
|
bad4b4be73 | ||
|
49f3c16c2c | ||
|
6fc3062146 | ||
|
10bd2c7e36 | ||
|
02f33c6ae2 |
88
.eslintrc.json
Normal file
88
.eslintrc.json
Normal file
@ -0,0 +1,88 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"jest/globals": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:jest/recommended"
|
||||
],
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react",
|
||||
"jest"
|
||||
],
|
||||
"rules": {
|
||||
"arrow-parens": [
|
||||
"error",
|
||||
"as-needed"
|
||||
],
|
||||
"arrow-spacing": [
|
||||
"error",
|
||||
{
|
||||
"before": true,
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
{
|
||||
"objects": "never",
|
||||
"arrays": "never",
|
||||
"imports": "never",
|
||||
"exports": "never",
|
||||
"functions": "never"
|
||||
}
|
||||
],
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"max-len": [
|
||||
"warn",
|
||||
{
|
||||
"code": 80
|
||||
}
|
||||
],
|
||||
"no-var": "error",
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
{
|
||||
"named": "never",
|
||||
"anonymous": "never",
|
||||
"asyncArrow": "always"
|
||||
}
|
||||
],
|
||||
"template-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "16.7"
|
||||
}
|
||||
}
|
||||
}
|
33
.gitignore
vendored
33
.gitignore
vendored
@ -1,7 +1,30 @@
|
||||
node_modules
|
||||
.sass-cache
|
||||
build
|
||||
docs
|
||||
tmp
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# Gatsby build files
|
||||
.cache/
|
||||
public/
|
||||
|
||||
# Test coverage
|
||||
coverage/
|
||||
|
||||
# Firebase
|
||||
.firebase/
|
||||
firebase-debug.log
|
||||
|
@ -1,12 +1,13 @@
|
||||
image: node:8
|
||||
image: node:latest
|
||||
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- deploy
|
||||
|
||||
.preview_job: &preview_job
|
||||
only:
|
||||
- master
|
||||
- gatsby # TODO: Change to master once merged
|
||||
|
||||
.production_job: &production_job
|
||||
only:
|
||||
@ -17,11 +18,9 @@ stages:
|
||||
dependencies: []
|
||||
artifacts:
|
||||
paths:
|
||||
- build/
|
||||
- public/
|
||||
script:
|
||||
- yarn gulp build
|
||||
- rm -r build/__discard__
|
||||
- gzip -k -6 $(find build/ -type f)
|
||||
- yarn build
|
||||
|
||||
.deploy_template: &deploy_template
|
||||
stage: deploy
|
||||
@ -36,19 +35,34 @@ cache:
|
||||
before_script:
|
||||
- yarn install
|
||||
|
||||
test-lint:
|
||||
stage: test
|
||||
script:
|
||||
- yarn test:lint
|
||||
|
||||
test-unit:
|
||||
stage: test
|
||||
coverage: '/^Statements\s*:\s*([^%]+)/'
|
||||
script:
|
||||
- yarn test:unit
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage/
|
||||
|
||||
build-preview:
|
||||
<<: *build_template
|
||||
<<: *preview_job
|
||||
variables:
|
||||
BANNER: preview
|
||||
DEPLOY_ENV: preview
|
||||
GA_PROP: $PREVIEW_GA_PROPERTY
|
||||
GA_PROPERTY: $PREVIEW_GA_PROPERTY
|
||||
|
||||
build-production:
|
||||
<<: *build_template
|
||||
<<: *production_job
|
||||
variables:
|
||||
DEPLOY_ENV: production
|
||||
GA_PROP: $PROD_GA_PROPERTY
|
||||
GA_PROPERTY: $PROD_GA_PROPERTY
|
||||
|
||||
deploy-preview:
|
||||
<<: *deploy_template
|
||||
|
68
.jscsrc
68
.jscsrc
@ -1,68 +0,0 @@
|
||||
{
|
||||
"requireCurlyBraces": [
|
||||
"if",
|
||||
"else",
|
||||
"for",
|
||||
"while",
|
||||
"do",
|
||||
"try",
|
||||
"catch"
|
||||
],
|
||||
"requireSpaceAfterKeywords": [
|
||||
"if",
|
||||
"else",
|
||||
"for",
|
||||
"while",
|
||||
"do",
|
||||
"switch",
|
||||
"case",
|
||||
"return",
|
||||
"try",
|
||||
"typeof"
|
||||
],
|
||||
"requireSpaceBeforeBlockStatements": true,
|
||||
"requireParenthesesAroundIIFE": true,
|
||||
"requireSpacesInConditionalExpression": true,
|
||||
"disallowSpacesInNamedFunctionExpression": {
|
||||
"beforeOpeningRoundBrace": true
|
||||
},
|
||||
"disallowSpacesInFunctionDeclaration": {
|
||||
"beforeOpeningRoundBrace": true
|
||||
},
|
||||
"requireSpaceBetweenArguments": true,
|
||||
"requireMultipleVarDecl": "onevar",
|
||||
"requireVarDeclFirst": true,
|
||||
"requireBlocksOnNewline": true,
|
||||
"disallowEmptyBlocks": true,
|
||||
"disallowSpacesInsideArrayBrackets": true,
|
||||
"disallowSpacesInsideParentheses": true,
|
||||
"requireCommaBeforeLineBreak": true,
|
||||
"disallowSpaceAfterPrefixUnaryOperators": true,
|
||||
"disallowSpaceBeforePostfixUnaryOperators": true,
|
||||
"disallowSpaceBeforeBinaryOperators": [
|
||||
","
|
||||
],
|
||||
"requireSpacesInForStatement": true,
|
||||
"requireSpacesInAnonymousFunctionExpression": {
|
||||
"beforeOpeningCurlyBrace": true
|
||||
},
|
||||
"requireSpaceBeforeBinaryOperators": true,
|
||||
"requireSpaceAfterBinaryOperators": true,
|
||||
"disallowKeywords": [
|
||||
"with",
|
||||
"continue"
|
||||
],
|
||||
"validateIndentation": 2,
|
||||
"disallowMixedSpacesAndTabs": true,
|
||||
"disallowTrailingWhitespace": true,
|
||||
"disallowTrailingComma": true,
|
||||
"disallowKeywordsOnNewLine": [
|
||||
"else"
|
||||
],
|
||||
"requireLineFeedAtFileEnd": true,
|
||||
"requireCapitalizedConstructors": true,
|
||||
"requireDotNotation": true,
|
||||
"disallowNewlineBeforeBlockStatements": true,
|
||||
"disallowMultipleLineStrings": true,
|
||||
"requireSpaceBeforeObjectValues": true
|
||||
}
|
10
.travis.yml
10
.travis.yml
@ -1,10 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "node"
|
||||
addons:
|
||||
firefox: "latest"
|
||||
before_script:
|
||||
- "export DISPLAY=:99.0"
|
||||
- "sh -e /etc/init.d/xvfb start"
|
||||
- sleep 3
|
||||
script: yarn test
|
@ -20,14 +20,6 @@ To start a development server, run:
|
||||
|
||||
$ yarn start
|
||||
|
||||
This will build the site into the ./build directory, start a local start on port 8080, and begin watching the source files for modifications. The site will automatically be rebuilt when files are changed. Also, if you browser has the LiveReload extension, then the page will be reloaded.
|
||||
|
||||
These other gulp tasks are available:
|
||||
|
||||
$ gulp docs # Build documentation into the ./docs directory
|
||||
$ gulp build # Build the site into the ./build directory
|
||||
$ yarn test # Run JSCS lint and Karma tests
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE.txt](/LICENSE.txt) file for licensing details.
|
||||
|
31
config.js
31
config.js
@ -1,31 +0,0 @@
|
||||
var path = require('path'),
|
||||
_ = require('lodash'),
|
||||
buildRoot = process.env.BUILD_PATH || path.join(__dirname, './build'),
|
||||
buildPath = _.bind(path.join, path, buildRoot);
|
||||
|
||||
module.exports = {
|
||||
buildRoot: buildRoot,
|
||||
buildPath: buildPath,
|
||||
globs: {
|
||||
other: './src/**/*.!(hbs|scss|js|peg)',
|
||||
templates: './src/**/*.hbs',
|
||||
data: ['./lib/data/**/*.json', './lib/data/**/*.js'],
|
||||
helpers: './lib/helpers/**/*.js',
|
||||
partials: './lib/partials/**/*.hbs',
|
||||
sass: './src/**/*.scss',
|
||||
svg_sass: './src/sass/svg.scss',
|
||||
js: ['./src/**/*.js', './src/**/*.peg'],
|
||||
spec: './spec/**/*_spec.js',
|
||||
lint: [
|
||||
'./lib/**/*.js',
|
||||
'./src/**/*.js',
|
||||
'./spec/**/*.js',
|
||||
'./*.js'
|
||||
]
|
||||
},
|
||||
lintRoots: ['lib', 'src', 'spec'],
|
||||
browserify: {
|
||||
debug: true,
|
||||
fullPaths: false
|
||||
}
|
||||
};
|
@ -2,11 +2,11 @@
|
||||
"hosting": [
|
||||
{
|
||||
"target": "production",
|
||||
"public": "build"
|
||||
"public": "public"
|
||||
},
|
||||
{
|
||||
"target": "preview",
|
||||
"public": "build"
|
||||
"public": "public"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
33
gatsby-browser.js
Normal file
33
gatsby-browser.js
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
|
||||
import i18n from 'i18n';
|
||||
import Layout from 'components/Layout';
|
||||
|
||||
import 'site.css';
|
||||
import style from 'globals.module.css';
|
||||
|
||||
Modal.setAppElement('#___gatsby');
|
||||
|
||||
Modal.defaultProps = {
|
||||
...Modal.defaultProps,
|
||||
className: style.modal,
|
||||
overlayClassName: style.modalOverlay
|
||||
};
|
||||
|
||||
export const onClientEntry = () => {
|
||||
Sentry.getCurrentHub().getClient().getOptions().enabled =
|
||||
(navigator.doNotTrack !== '1' && window.doNotTrack !== '1');
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
export const wrapPageElement = ({ element }) => {
|
||||
return <Layout>{ element }</Layout>;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
export const wrapRootElement = ({ element }) => {
|
||||
return <I18nextProvider i18n={ i18n }>{ element }</I18nextProvider>;
|
||||
};
|
58
gatsby-config.js
Normal file
58
gatsby-config.js
Normal file
@ -0,0 +1,58 @@
|
||||
const pkg = require('./package.json');
|
||||
|
||||
const buildId = [
|
||||
process.env.CI_COMMIT_REF_SLUG || 'prerelese',
|
||||
(process.env.CI_COMMIT_SHA || 'gitsha').slice(0, 7)
|
||||
].join('-');
|
||||
const banner = process.env.BANNER || (process.env.NODE_ENV === 'production'
|
||||
? false
|
||||
: (process.env.NODE_ENV || 'development'));
|
||||
|
||||
module.exports = {
|
||||
siteMetadata: {
|
||||
description: pkg.description,
|
||||
buildId,
|
||||
banner,
|
||||
defaultSyntax: 'js',
|
||||
syntaxList: [
|
||||
{ id: 'js', label: 'JavaScript' },
|
||||
{ id: 'pcre', label: 'PCRE' }
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
'gatsby-plugin-react-helmet',
|
||||
'gatsby-plugin-postcss',
|
||||
{
|
||||
resolve: 'gatsby-plugin-google-analytics',
|
||||
options: {
|
||||
trackingId: process.env.GA_PROPERTY,
|
||||
anonymize: true,
|
||||
respectDNT: true,
|
||||
storeGac: false,
|
||||
cookieExpires: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: 'gatsby-plugin-sentry',
|
||||
options: {
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
environment: process.env.DEPLOY_ENV || process.env.NODE_ENV,
|
||||
debug: (process.env.NODE_ENV !== 'production'),
|
||||
release: buildId
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: 'gatsby-plugin-manifest',
|
||||
options: {
|
||||
name: 'Regexper',
|
||||
short_name: 'Regexper',
|
||||
start_url: '/',
|
||||
background_color: '#6b6659',
|
||||
theme_color: '#bada55',
|
||||
display: 'standalone',
|
||||
icon: 'src/icon.svg'
|
||||
}
|
||||
},
|
||||
'gatsby-plugin-offline'
|
||||
]
|
||||
};
|
9
gatsby-node.js
Normal file
9
gatsby-node.js
Normal file
@ -0,0 +1,9 @@
|
||||
const path = require('path');
|
||||
|
||||
exports.onCreateWebpackConfig = ({ actions }) => {
|
||||
actions.setWebpackConfig({
|
||||
resolve: {
|
||||
modules: [path.resolve(__dirname, 'src'), 'node_modules']
|
||||
}
|
||||
});
|
||||
};
|
15
gatsby-ssr.js
Normal file
15
gatsby-ssr.js
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
|
||||
import i18n from 'i18n';
|
||||
import Layout from 'components/Layout';
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
export const wrapPageElement = ({ element }) => {
|
||||
return <Layout>{ element }</Layout>;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
export const wrapRootElement = ({ element }) => {
|
||||
return <I18nextProvider i18n={ i18n }>{ element }</I18nextProvider>;
|
||||
};
|
101
gulpfile.js
101
gulpfile.js
@ -1,101 +0,0 @@
|
||||
const gulp = require('gulp-help')(require('gulp')),
|
||||
_ = require('lodash'),
|
||||
notify = require('gulp-notify'),
|
||||
folderToc = require('folder-toc'),
|
||||
docco = require('gulp-docco'),
|
||||
connect = require('gulp-connect'),
|
||||
hb = require('gulp-hb'),
|
||||
frontMatter = require('gulp-front-matter'),
|
||||
rename = require('gulp-rename'),
|
||||
config = require('./config'),
|
||||
gutil = require('gulp-util'),
|
||||
webpack = require('webpack')
|
||||
webpackConfig = require('./webpack.config'),
|
||||
fs = require('fs');
|
||||
|
||||
gulp.task('default', 'Auto-rebuild site on changes.', ['server', 'docs'], function() {
|
||||
gulp.watch(config.globs.other, ['static']);
|
||||
gulp.watch(_.flatten([
|
||||
config.globs.templates,
|
||||
config.globs.data,
|
||||
config.globs.helpers,
|
||||
config.globs.partials,
|
||||
config.globs.svg_sass
|
||||
]), ['markup']);
|
||||
gulp.watch(_.flatten([
|
||||
config.globs.sass,
|
||||
config.globs.js
|
||||
]), ['webpack']);
|
||||
gulp.watch(config.globs.js, ['docs']);
|
||||
});
|
||||
|
||||
gulp.task('docs', 'Build documentation into ./docs directory.', ['docs:files'], function() {
|
||||
folderToc('./docs', {
|
||||
filter: '*.html'
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('docs:files', false, function() {
|
||||
return gulp.src(config.globs.js)
|
||||
.pipe(docco())
|
||||
.pipe(gulp.dest('./docs'));
|
||||
});
|
||||
|
||||
gulp.task('server', 'Start development server.', ['build'], function() {
|
||||
gulp.watch(config.buildPath('**/*'), function(file) {
|
||||
return gulp.src(file.path).pipe(connect.reload());
|
||||
});
|
||||
|
||||
return connect.server({
|
||||
root: config.buildRoot,
|
||||
livereload: true
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('build', 'Build site into ./build directory.', ['static', 'webpack', 'markup']);
|
||||
|
||||
gulp.task('static', 'Build static files into ./build directory.', function() {
|
||||
return gulp.src(config.globs.other, { base: './src' })
|
||||
.pipe(gulp.dest(config.buildRoot));
|
||||
});
|
||||
|
||||
gulp.task('markup', 'Build markup into ./build directory.', ['webpack'], function() {
|
||||
var hbStream = hb({
|
||||
data: config.globs.data,
|
||||
helpers: config.globs.helpers,
|
||||
partials: config.globs.partials,
|
||||
parsePartialName: function(option, file) {
|
||||
return _.last(file.path.split(/\\|\//)).replace('.hbs', '');
|
||||
},
|
||||
bustCache: true
|
||||
});
|
||||
hbStream.partials({
|
||||
svg_styles: fs.readFileSync(config.buildRoot + '/css/svg.css').toString()
|
||||
});
|
||||
if (process.env.GA_PROP) {
|
||||
hbStream.data({
|
||||
'gaPropertyId': process.env.GA_PROP
|
||||
});
|
||||
}
|
||||
if (process.env.SENTRY_KEY) {
|
||||
hbStream.data({
|
||||
'sentryKey': process.env.SENTRY_KEY
|
||||
});
|
||||
}
|
||||
return gulp.src(config.globs.templates)
|
||||
.pipe(frontMatter())
|
||||
.pipe(hbStream)
|
||||
.on('error', notify.onError())
|
||||
.pipe(rename({ extname: '.html' }))
|
||||
.pipe(gulp.dest(config.buildRoot));
|
||||
});
|
||||
|
||||
gulp.task('webpack', 'Build JS & CSS into ./build directory.', function(callback) {
|
||||
webpack(webpackConfig, function(err, stats) {
|
||||
if (err) {
|
||||
throw new gutil.PluginError('webpack', err);
|
||||
}
|
||||
gutil.log('[webpack]', stats.toString());
|
||||
callback();
|
||||
});
|
||||
});
|
6
jest/preprocess.js
Normal file
6
jest/preprocess.js
Normal file
@ -0,0 +1,6 @@
|
||||
const babelOptions = {
|
||||
presets: ['babel-preset-gatsby'],
|
||||
plugins: ['dynamic-import-node']
|
||||
};
|
||||
|
||||
module.exports = require('babel-jest').createTransformer(babelOptions);
|
5
jest/setup.js
Normal file
5
jest/setup.js
Normal file
@ -0,0 +1,5 @@
|
||||
global.___loader = {
|
||||
enqueue: jest.fn()
|
||||
};
|
||||
|
||||
global.Element.prototype.getBBox = jest.fn();
|
7
jest/static-file-transform.js
Normal file
7
jest/static-file-transform.js
Normal file
@ -0,0 +1,7 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
process(src, filename) {
|
||||
return `module.exports = ${ JSON.stringify(path.basename(filename)) };`;
|
||||
}
|
||||
};
|
7
jest/yaml.js
Normal file
7
jest/yaml.js
Normal file
@ -0,0 +1,7 @@
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
module.exports = {
|
||||
process(src) {
|
||||
return `module.exports = ${ JSON.stringify(yaml.safeLoad(src)) };`;
|
||||
}
|
||||
};
|
@ -1,35 +0,0 @@
|
||||
module.exports = function(karma) {
|
||||
karma.set({
|
||||
frameworks: ['jasmine'],
|
||||
files: [ 'spec/test_index.js' ],
|
||||
preprocessors: {
|
||||
'spec/test_index.js': ['webpack', 'sourcemap']
|
||||
},
|
||||
reporters: ['progress', 'notify'],
|
||||
colors: true,
|
||||
logLevel: karma.LOG_INFO,
|
||||
browsers: ['Firefox'],
|
||||
autoWatch: true,
|
||||
singleRun: false,
|
||||
webpack: {
|
||||
devtool: 'inline-source-map',
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader'
|
||||
},
|
||||
{
|
||||
test: require.resolve('snapsvg'),
|
||||
loader: 'imports-loader?this=>window,fix=>module.exports=0'
|
||||
},
|
||||
{
|
||||
test: /\.peg$/,
|
||||
loader: require.resolve('./lib/canopy-loader')
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
@ -1,6 +0,0 @@
|
||||
var canopy = require('canopy');
|
||||
|
||||
module.exports = function(source) {
|
||||
this.cacheable();
|
||||
return canopy.compile(source);
|
||||
};
|
@ -1,186 +0,0 @@
|
||||
[
|
||||
{
|
||||
"label": "June 4, 2018 Release",
|
||||
"changes": [
|
||||
"Moving source to GitLab and updating some links on the site."
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "May 24, 2018 Release",
|
||||
"changes": [
|
||||
"Supporting browser \"Do Not Track\" setting. When enabled, this will prevent use of Google Analytics and Sentry error reporting."
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "February 10, 2018 Release",
|
||||
"changes": [
|
||||
"Adding 'sticky' and 'unicode' flag support",
|
||||
"Encoding parenthesis in the permalink and browser URLs",
|
||||
"Adding PNG download support"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "July 31, 2016 Release",
|
||||
"changes": [
|
||||
"Merged code to enable automated testing with Travis CI from <a href=\"https://github.com/Byron\">Sebastian Thiel</a>",
|
||||
"Merged feature to show an informational tooltip on loop labels from <a href=\"https://github.com/ThibWeb\">Thibaud Colas</a>",
|
||||
"Fixed issue with '^' and '$' not being allowed in the middle of a fragment (see <a href=\"https://github.com/javallone/regexper-static/issues/29\">GitHub issue</a>)",
|
||||
"Updating several dependencies",
|
||||
"Some stylistic code cleanup"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "May 31, 2016 Release",
|
||||
"changes": [
|
||||
"Putting separate CSS for generated SVG images back into the build. Downloaded images have been broken since the March 10 release because the SVG styles were merged into the page styles."
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "May 23, 2016 Release",
|
||||
"changes": [
|
||||
"Refactored tracking code to support latest Google Analytics setup"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "March 10, 2016 Release",
|
||||
"changes": [
|
||||
"Embedding SVG icon images into markup",
|
||||
"Some changes for minor performance improvements",
|
||||
"Updating several dependencies"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "March 8, 2016 Release",
|
||||
"changes": [
|
||||
"Replaced icon font with individual SVG images"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "March 3, 2016 Release",
|
||||
"changes": [
|
||||
"Merged some code cleanup and a bugfix from <a href=\"https://github.com/Byron\">Sebastian Thiel</a>",
|
||||
"Updated notice for IE8 users to no longer include link to legacy site"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "December 21, 2015 Release",
|
||||
"changes": [
|
||||
"Updating NPM dependencies to fix JS error that only appeared when running site from a local development environment (see <a href=\"https://github.com/javallone/regexper-static/issues/21\">GitHub issue</a>)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "November 10, 2015 Release",
|
||||
"changes": [
|
||||
"Fixing Babel integration to include polyfills"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "November 1, 2015 Release",
|
||||
"changes": [
|
||||
"Switching from Compass to node-sass and Bourbon (no more need for Ruby)",
|
||||
"Switching to Babel instead of es6ify",
|
||||
"Improving sourcemap generation",
|
||||
"Cleanup of the build process"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "October 31, 2015 Release",
|
||||
"changes": [
|
||||
"Reducing file size for title font",
|
||||
"Cleaning up gulpfile",
|
||||
"Upgrading most dependencies",
|
||||
"Switching to Handlebars for template rendering"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "September 17, 2015 Release",
|
||||
"changes": [
|
||||
"Fixing styling of labels on repetitions",
|
||||
"Fixing issue with vertical centering of alternation expressions that include empty expressions (see <a href=\"https://github.com/javallone/regexper-static/pull/16\">GitHub issue</a>)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "September 2, 2015 Release",
|
||||
"changes": [
|
||||
"Merging fix for error reporting from (see <a href=\"https://github.com/javallone/regexper-static/pull/15\">GitHub pull request</a>)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "July 5, 2015 Release",
|
||||
"changes": [
|
||||
"Updating Creative Commons license badge URL so it isn't pointing to a redirecting URL anymore"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "June 22, 2015 Release",
|
||||
"changes": [
|
||||
"Tweaking buggy Firefox hash detection code based on JavaScript errors that were logged"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "June 16, 2015 Release",
|
||||
"changes": [
|
||||
"Fixes issue with expressions containing a \"%\" not rendering in Firefox (see <a href=\"https://github.com/javallone/regexper-static/issues/12\">GitHub issue</a>)",
|
||||
"Fixed rendering in IE that was causing \"-->\" to display at the top of the page."
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "April 14, 2015 Release",
|
||||
"changes": [
|
||||
"Rendering speed improved. Most users will probably not see much improvement since logging data indicates that expressing rendering time is typically less than 1 second. Using the <a href=\"http://www.ex-parrot.com/pdw/Mail-RFC822-Address.html\">RFC822 email regular expression</a> though shows a rendering speed improvement from ~120 seconds down to ~80 seconds.",
|
||||
"Fixing a bug that would only occur when attempting to render an expression while another is in the process of rendering"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "March 14, 2015 Release",
|
||||
"changes": [
|
||||
"Removing use of Q for promises in favor of \"native\" ES6 promises (even though they aren't quite native everywhere yet)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "March 13, 2015 Release",
|
||||
"changes": [
|
||||
"Fixes bug with numbering of nested subexpressions (see <a href=\"https://github.com/javallone/regexper-static/issues/7\">GitHub issue</a>)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "February 11, 2015 Release",
|
||||
"changes": [
|
||||
"Various adjustments to analytics: tracking expression rendering time and JS errors",
|
||||
"Escape sequences that match to a specific character now display their hexadecimal code (actually done on January 25, but I forgot to update the changelog)",
|
||||
"Fixing styling issue with header links (see <a href=\"https://github.com/javallone/regexper-static/issues/5\">GitHub issue</a>)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "December 30, 2014 Release",
|
||||
"changes": [
|
||||
"Fixing bug that prevented rendering empty subexpressions",
|
||||
"Fixing minor styling bug when permalink is disabled",
|
||||
"Cleaning up some duplicated styles and JS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "December 29, 2014 Release",
|
||||
"changes": [
|
||||
"Tweaking analytics data to help with addressing issues in deployed code (work will likely continue on this)",
|
||||
"Added progress bars on the documentation page",
|
||||
"Removed the loading spinner everywhere",
|
||||
"Animated the progress bars"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "December 26, 2014 Release",
|
||||
"changes": [
|
||||
"Freshened up design",
|
||||
"Multiline regular expression input field (press Shift-Enter to render)",
|
||||
"Added a changelog",
|
||||
"Added documentation",
|
||||
"All parsing and rendering happens client-side (using <a href=\"http://canopy.jcoglan.com/\">Canopy</a> and <a href=\"http://snapsvg.io/\">Snap.svg</a>)",
|
||||
"Added Download link (not available in older browsers)",
|
||||
"Added display of regular expression flags (ignore case, global, multiline)",
|
||||
"Added indicator of quantifier greedy-ness",
|
||||
"Various improvements to parsing of regular expression",
|
||||
"Rendering of a regular expression can be canceled by pressing Escape"
|
||||
]
|
||||
}
|
||||
]
|
@ -1 +0,0 @@
|
||||
module.exports = new Date().toISOString();
|
@ -1,5 +0,0 @@
|
||||
module.exports.register = function(handlebars) {
|
||||
handlebars.registerHelper('icon', function(selector, context) {
|
||||
return new handlebars.SafeString(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 8 8"><use xlink:href="${selector}" /></svg>`);
|
||||
});
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
var layouts = require('handlebars-layouts');
|
||||
|
||||
module.exports.register = function(handlebars) {
|
||||
layouts.register(handlebars);
|
||||
};
|
@ -1,15 +0,0 @@
|
||||
{{#if gaPropertyId}}
|
||||
<script>
|
||||
if (navigator.doNotTrack !== '1' && window.doNotTrack !== '1') {
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', '{{{gaPropertyId}}}', 'auto');
|
||||
ga('send', 'pageview');
|
||||
} else {
|
||||
console.log('Google Analytics disabled by "Do Not Track"');
|
||||
}
|
||||
</script>
|
||||
{{/if}}
|
@ -1,68 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
|
||||
<title>Regexper{{#if file.frontMatter.title}} - {{file.frontMatter.title}}{{/if}}</title>
|
||||
|
||||
<meta name="description" content="Regular expression visualizer using railroad diagrams" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="theme-color" content="#bada55" />
|
||||
|
||||
{{> "google_analytics"}}
|
||||
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="author" href="humans.txt" />
|
||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Bangers&text=Regxpr" />
|
||||
<link rel="stylesheet" href="/css/main.css" />
|
||||
<!-- Built: {{date}} -->
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<h1><a href="/">Regexper</a></h1>
|
||||
<!-- n. One who regexpes -->
|
||||
<span>You thought you only had two problems…</span>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a class="inline-icon" href="/changelog.html">{{icon "#list-rich"}}Changelog</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="inline-icon" href="/documentation.html">{{icon "#document"}}Documentation</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="inline-icon" href="https://gitlab.com/javallone/regexper-static">{{icon "#code"}}Source on GitLab</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main id="content">
|
||||
{{#block "body"}}{{/block}}
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
{{#block "footer"}}
|
||||
<ul class="inline-list">
|
||||
<li>Created by <a href="mailto:jeff.avallone@gmail.com">Jeff Avallone</a></li>
|
||||
<li>
|
||||
Generated images licensed:
|
||||
<a rel="license" href="http://creativecommons.org/licenses/by/3.0/"><img alt="Creative Commons License" src="https://licensebuttons.net/l/by/3.0/80x15.png" /></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<script type="text/html" id="svg-container-base">
|
||||
{{> "svg_template"}}
|
||||
</script>
|
||||
|
||||
{{> "sentry"}}
|
||||
{{/block}}
|
||||
</footer>
|
||||
|
||||
{{> "open_iconic"}}
|
||||
</body>
|
||||
</html>
|
@ -1,23 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="open-iconic">
|
||||
<!-- These icon are from the Open Iconic project https://useiconic.com/open/ -->
|
||||
<defs>
|
||||
<g id="code">
|
||||
<path d="M5 0l-3 6h1l3-6h-1zm-4 1l-1 2 1 2h1l-1-2 1-2h-1zm5 0l1 2-1 2h1l1-2-1-2h-1z" transform="translate(0 1)" />
|
||||
</g>
|
||||
<g id="data-transfer-download">
|
||||
<path d="M3 0v3h-2l3 3 3-3h-2v-3h-2zm-3 7v1h8v-1h-8z" />
|
||||
</g>
|
||||
<g id="document">
|
||||
<path d="M0 0v8h7v-4h-4v-4h-3zm4 0v3h3l-3-3zm-3 2h1v1h-1v-1zm0 2h1v1h-1v-1zm0 2h4v1h-4v-1z" />
|
||||
</g>
|
||||
<g id="link-intact">
|
||||
<path d="M5.88.03c-.18.01-.36.03-.53.09-.27.1-.53.25-.75.47a.5.5 0 1 0 .69.69c.11-.11.24-.17.38-.22.35-.12.78-.07 1.06.22.39.39.39 1.04 0 1.44l-1.5 1.5c-.44.44-.8.48-1.06.47-.26-.01-.41-.13-.41-.13a.5.5 0 1 0-.5.88s.34.22.84.25c.5.03 1.2-.16 1.81-.78l1.5-1.5c.78-.78.78-2.04 0-2.81-.28-.28-.61-.45-.97-.53-.18-.04-.38-.04-.56-.03zm-2 2.31c-.5-.02-1.19.15-1.78.75l-1.5 1.5c-.78.78-.78 2.04 0 2.81.56.56 1.36.72 2.06.47.27-.1.53-.25.75-.47a.5.5 0 1 0-.69-.69c-.11.11-.24.17-.38.22-.35.12-.78.07-1.06-.22-.39-.39-.39-1.04 0-1.44l1.5-1.5c.4-.4.75-.45 1.03-.44.28.01.47.09.47.09a.5.5 0 1 0 .44-.88s-.34-.2-.84-.22z" />
|
||||
</g>
|
||||
<g id="list-rich">
|
||||
<path d="M0 0v3h3v-3h-3zm4 0v1h4v-1h-4zm0 2v1h3v-1h-3zm-4 2v3h3v-3h-3zm4 0v1h4v-1h-4zm0 2v1h3v-1h-3z" />
|
||||
</g>
|
||||
<g id="warning">
|
||||
<path d="M3.09 0c-.06 0-.1.04-.13.09l-2.94 6.81c-.02.05-.03.13-.03.19v.81c0 .05.04.09.09.09h6.81c.05 0 .09-.04.09-.09v-.81c0-.05-.01-.14-.03-.19l-2.94-6.81c-.02-.05-.07-.09-.13-.09h-.81zm-.09 3h1v2h-1v-2zm0 3h1v1h-1v-1z" />
|
||||
</g>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,17 +0,0 @@
|
||||
{{#if sentryKey}}
|
||||
<script>
|
||||
if (navigator.doNotTrack !== '1' && window.doNotTrack !== '1') {
|
||||
window.SENTRY_SDK = {
|
||||
url: 'https://cdn.ravenjs.com/3.25.2/raven.min.js',
|
||||
dsn: '{{{sentryKey}}}',
|
||||
options: {
|
||||
whitelistUrls: [/https:\/\/(.*\.)?regexper\.com/]
|
||||
}
|
||||
};
|
||||
|
||||
(function(a,b,g,e,h){var k=a.SENTRY_SDK,f=function(a){f.data.push(a)};f.data=[];var l=a[e];a[e]=function(c,b,e,d,h){f({e:[].slice.call(arguments)});l&&l.apply(a,arguments)};var m=a[h];a[h]=function(c){f({p:c.reason});m&&m.apply(a,arguments)};var n=b.getElementsByTagName(g)[0];b=b.createElement(g);b.src=k.url;b.crossorigin="anonymous";b.addEventListener("load",function(){try{a[e]=l;a[h]=m;var c=f.data,b=a.Raven;b.config(k.dsn,k.options).install();var g=a[e];if(c.length)for(var d=0;d<c.length;d++)c[d].e?g.apply(b.TraceKit,c[d].e):c[d].p&&b.captureException(c[d].p)}catch(p){console.log(p)}});n.parentNode.insertBefore(b,n)})(window,document,"script","onerror","onunhandledrejection");
|
||||
} else {
|
||||
console.log('Sentry error logging disabled by "Do Not Track"');
|
||||
}
|
||||
</script>
|
||||
{{/if}}
|
@ -1,25 +0,0 @@
|
||||
<div class="svg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
version="1.1">
|
||||
<defs>
|
||||
<style type="text/css">{{> svg_styles}}</style>
|
||||
</defs>
|
||||
<metadata>
|
||||
<rdf:RDF>
|
||||
<cc:License rdf:about="http://creativecommons.org/licenses/by/3.0/">
|
||||
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction" />
|
||||
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution" />
|
||||
<cc:requires rdf:resource="http://creativecommons.org/ns#Notice" />
|
||||
<cc:requires rdf:resource="http://creativecommons.org/ns#Attribution" />
|
||||
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
|
||||
</cc:License>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div style="width:0;"></div>
|
||||
</div>
|
143
package.json
143
package.json
@ -10,49 +10,106 @@
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"pretest": "jscs lib/ src/ spec/",
|
||||
"test": "karma start --single-run",
|
||||
"build": "gulp build",
|
||||
"start": "gulp"
|
||||
"start": "gatsby develop",
|
||||
"build": "gatsby build",
|
||||
"test:lint": "eslint --ignore-path .gitignore .",
|
||||
"test:unit": "jest --coverage",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.17.0",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-polyfill": "^6.3.14",
|
||||
"babel-preset-es2015": "^6.16.0",
|
||||
"babel-runtime": "^6.3.19",
|
||||
"canopy": "^0.2.0",
|
||||
"css-loader": "^0.28.4",
|
||||
"docco": "^0.7.0",
|
||||
"extract-loader": "^1.0.0",
|
||||
"file-loader": "^0.11.2",
|
||||
"firebase-tools": "^6.2.2",
|
||||
"folder-toc": "^0.1.0",
|
||||
"gulp": "^3.8.10",
|
||||
"gulp-connect": "^5.0.0",
|
||||
"gulp-docco": "0.0.4",
|
||||
"gulp-front-matter": "^1.3.0",
|
||||
"gulp-hb": "^6.0.2",
|
||||
"gulp-help": "^1.6.1",
|
||||
"gulp-notify": "^3.0.0",
|
||||
"gulp-rename": "^1.2.2",
|
||||
"gulp-util": "^3.0.7",
|
||||
"handlebars-layouts": "^3.1.2",
|
||||
"imports-loader": "^0.7.1",
|
||||
"jasmine-core": "^2.4.1",
|
||||
"jscs": "^3.0.7",
|
||||
"karma": "^1.1.2",
|
||||
"karma-firefox-launcher": "^1.0.0",
|
||||
"karma-jasmine": "^1.0.2",
|
||||
"karma-notify-reporter": "^1.0.1",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-webpack": "^2.0.4",
|
||||
"lodash": "^4.6.1",
|
||||
"node-bourbon": "^4.2.3",
|
||||
"node-sass": "^4.5.3",
|
||||
"sass-loader": "^6.0.6",
|
||||
"snapsvg": "^0.5.1",
|
||||
"watchify": "^3.7.0",
|
||||
"webpack": "^3.4.1"
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "yarn test:lint"
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
">1%",
|
||||
"not ie < 11"
|
||||
],
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"postcss-import": {},
|
||||
"postcss-cssnext": {
|
||||
"features": {
|
||||
"rem": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
"clearMocks": true,
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.js",
|
||||
"!src/i18n.js"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text-summary",
|
||||
"html"
|
||||
],
|
||||
"globals": {
|
||||
"__PATH_PREFIX__": ""
|
||||
},
|
||||
"moduleNameMapper": {
|
||||
"\\.css$": "identity-obj-proxy"
|
||||
},
|
||||
"modulePaths": [
|
||||
"src",
|
||||
"node_modules"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"react-testing-library/cleanup-after-each",
|
||||
"<rootDir>/jest/setup.js"
|
||||
],
|
||||
"testPathIgnorePatterns": [
|
||||
"node_modules",
|
||||
".cache"
|
||||
],
|
||||
"transform": {
|
||||
"\\.yaml$": "<rootDir>/jest/yaml.js",
|
||||
"\\.js$": "<rootDir>/jest/preprocess.js",
|
||||
"\\.svg$": "<rootDir>/jest/static-file-transform.js"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(gatsby)/)"
|
||||
],
|
||||
"watchPathIgnorePatterns": [
|
||||
"<rootDir>/coverage",
|
||||
"<rootDir>/public"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.2.2",
|
||||
"@ungap/url-search-params": "^0.1.2",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^24.5.0",
|
||||
"babel-plugin-dynamic-import-node": "^2.2.0",
|
||||
"babel-preset-gatsby": "^0.1.6",
|
||||
"eslint": "^5.11.1",
|
||||
"eslint-plugin-jest": "^22.1.2",
|
||||
"eslint-plugin-react": "^7.12.1",
|
||||
"firebase-tools": "^6.3.0",
|
||||
"gatsby": "^2.0.81",
|
||||
"gatsby-plugin-google-analytics": "^2.0.8",
|
||||
"gatsby-plugin-manifest": "^2.0.13",
|
||||
"gatsby-plugin-offline": "^2.0.21",
|
||||
"gatsby-plugin-postcss": "^2.0.2",
|
||||
"gatsby-plugin-react-helmet": "^3.0.5",
|
||||
"gatsby-plugin-sentry": "^1.0.0",
|
||||
"husky": "^1.3.1",
|
||||
"i18next": "^15.0.7",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"i18next-xhr-backend": "^2.0.1",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^24.5.0",
|
||||
"js-yaml": "^3.13.0",
|
||||
"postcss-cssnext": "^3.1.0",
|
||||
"postcss-import": "^12.0.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"react-feather": "^1.1.5",
|
||||
"react-helmet": "^5.2.0",
|
||||
"react-i18next": "^10.5.3",
|
||||
"react-modal": "^3.8.1",
|
||||
"react-testing-library": "^6.0.2"
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
describe('parser/javascript/anchor.js', function() {
|
||||
|
||||
_.forIn({
|
||||
'^': {
|
||||
label: 'Start of line'
|
||||
},
|
||||
'$': {
|
||||
label: 'End of line'
|
||||
}
|
||||
}, (content, str) => {
|
||||
it(`parses "${str}" as an Anchor`, function() {
|
||||
var parser = new javascript.Parser(str);
|
||||
expect(parser.__consume__anchor()).toEqual(jasmine.objectContaining(content));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@ -1,27 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
|
||||
describe('parser/javascript/any_character.js', function() {
|
||||
|
||||
it('parses "." as an AnyCharacter', function() {
|
||||
var parser = new javascript.Parser('.');
|
||||
expect(parser.__consume__terminal()).toEqual(jasmine.objectContaining({
|
||||
type: 'any-character'
|
||||
}));
|
||||
});
|
||||
|
||||
describe('#_render', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
var parser = new javascript.Parser('.');
|
||||
this.node = parser.__consume__terminal();
|
||||
});
|
||||
|
||||
it('renders a label', function() {
|
||||
spyOn(this.node, 'renderLabel').and.returnValue('rendered label');
|
||||
expect(this.node._render()).toEqual('rendered label');
|
||||
expect(this.node.renderLabel).toHaveBeenCalledWith('any character');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,32 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
import _ from 'lodash';
|
||||
import Snap from 'snapsvg';
|
||||
|
||||
describe('parser/javascript/charset_escape.js', function() {
|
||||
|
||||
_.forIn({
|
||||
'\\b': { label: 'backspace (0x08)', ordinal: 0x08 },
|
||||
'\\d': { label: 'digit', ordinal: -1 },
|
||||
'\\D': { label: 'non-digit', ordinal: -1 },
|
||||
'\\f': { label: 'form feed (0x0C)', ordinal: 0x0c },
|
||||
'\\n': { label: 'line feed (0x0A)', ordinal: 0x0a },
|
||||
'\\r': { label: 'carriage return (0x0D)', ordinal: 0x0d },
|
||||
'\\s': { label: 'white space', ordinal: -1 },
|
||||
'\\S': { label: 'non-white space', ordinal: -1 },
|
||||
'\\t': { label: 'tab (0x09)', ordinal: 0x09 },
|
||||
'\\v': { label: 'vertical tab (0x0B)', ordinal: 0x0b },
|
||||
'\\w': { label: 'word', ordinal: -1 },
|
||||
'\\W': { label: 'non-word', ordinal: -1 },
|
||||
'\\0': { label: 'null (0x00)', ordinal: 0 },
|
||||
'\\012': { label: 'octal: 12 (0x0A)', ordinal: 10 },
|
||||
'\\cx': { label: 'ctrl-X (0x18)', ordinal: 24 },
|
||||
'\\xab': { label: '0xAB', ordinal: 0xab },
|
||||
'\\uabcd': { label: 'U+ABCD', ordinal: 0xabcd }
|
||||
}, (content, str) => {
|
||||
it(`parses "${str}" as a CharsetEscape`, function() {
|
||||
var parser = new javascript.Parser(str);
|
||||
expect(parser.__consume__charset_terminal()).toEqual(jasmine.objectContaining(content));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@ -1,103 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
import util from '../../../src/js/util.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
describe('parser/javascript/charset_range.js', function() {
|
||||
|
||||
_.forIn({
|
||||
'a-z': {
|
||||
first: jasmine.objectContaining({ textValue: 'a' }),
|
||||
last: jasmine.objectContaining({ textValue: 'z' })
|
||||
},
|
||||
'\\b-z': {
|
||||
first: jasmine.objectContaining({ textValue: '\\b' }),
|
||||
last: jasmine.objectContaining({ textValue: 'z' })
|
||||
},
|
||||
'\\f-z': {
|
||||
first: jasmine.objectContaining({ textValue: '\\f' }),
|
||||
last: jasmine.objectContaining({ textValue: 'z' })
|
||||
},
|
||||
'\\n-z': {
|
||||
first: jasmine.objectContaining({ textValue: '\\n' }),
|
||||
last: jasmine.objectContaining({ textValue: 'z' })
|
||||
},
|
||||
'\\r-z': {
|
||||
first: jasmine.objectContaining({ textValue: '\\r' }),
|
||||
last: jasmine.objectContaining({ textValue: 'z' })
|
||||
},
|
||||
'\\t-z': {
|
||||
first: jasmine.objectContaining({ textValue: '\\t' }),
|
||||
last: jasmine.objectContaining({ textValue: 'z' })
|
||||
},
|
||||
'\\v-z': {
|
||||
first: jasmine.objectContaining({ textValue: '\\v' }),
|
||||
last: jasmine.objectContaining({ textValue: 'z' })
|
||||
}
|
||||
}, (content, str) => {
|
||||
it(`parses "${str}" as a CharsetRange`, function() {
|
||||
var parser = new javascript.Parser(str);
|
||||
expect(parser.__consume__charset_range()).toEqual(jasmine.objectContaining(content));
|
||||
});
|
||||
});
|
||||
|
||||
_.each([
|
||||
'\\d-a',
|
||||
'\\D-a',
|
||||
'\\s-a',
|
||||
'\\S-a',
|
||||
'\\w-a',
|
||||
'\\W-a'
|
||||
], str => {
|
||||
it(`does not parse "${str}" as a CharsetRange`, function() {
|
||||
var parser = new javascript.Parser(str);
|
||||
expect(parser.__consume__charset_range()).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an exception when the range is out of order', function() {
|
||||
var parser = new javascript.Parser('z-a');
|
||||
expect(() => {
|
||||
parser.__consume__charset_range();
|
||||
}).toThrow('Range out of order in character class: z-a');
|
||||
});
|
||||
|
||||
describe('#_render', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
var parser = new javascript.Parser('a-z');
|
||||
this.node = parser.__consume__charset_range();
|
||||
|
||||
this.node.container = jasmine.createSpyObj('cotnainer', ['addClass', 'text', 'group']);
|
||||
this.node.container.text.and.returnValue('hyphen');
|
||||
|
||||
this.firstDeferred = this.testablePromise();
|
||||
this.lastDeferred = this.testablePromise();
|
||||
|
||||
spyOn(this.node.first, 'render').and.returnValue(this.firstDeferred.promise);
|
||||
spyOn(this.node.last, 'render').and.returnValue(this.lastDeferred.promise);
|
||||
spyOn(util, 'spaceHorizontally');
|
||||
});
|
||||
|
||||
it('renders a hyphen', function() {
|
||||
this.node._render();
|
||||
expect(this.node.container.text).toHaveBeenCalledWith(0, 0, '-');
|
||||
});
|
||||
|
||||
it('spaces the items horizontally', function(done) {
|
||||
this.firstDeferred.resolve();
|
||||
this.lastDeferred.resolve();
|
||||
|
||||
this.node._render()
|
||||
.then(() => {
|
||||
expect(util.spaceHorizontally).toHaveBeenCalledWith([
|
||||
this.node.first,
|
||||
'hyphen',
|
||||
this.node.last
|
||||
], { padding: 5 });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,154 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
import Node from '../../../src/js/parser/javascript/node.js';
|
||||
import util from '../../../src/js/util.js';
|
||||
import _ from 'lodash';
|
||||
import Snap from 'snapsvg';
|
||||
|
||||
describe('parser/javascript/charset.js', function() {
|
||||
|
||||
_.forIn({
|
||||
'[abc]': {
|
||||
label: 'One of:',
|
||||
elements: [
|
||||
jasmine.objectContaining({ type: 'literal', textValue: 'a' }),
|
||||
jasmine.objectContaining({ type: 'literal', textValue: 'b' }),
|
||||
jasmine.objectContaining({ type: 'literal', textValue: 'c' })
|
||||
]
|
||||
},
|
||||
'[^abc]': {
|
||||
label: 'None of:',
|
||||
elements: [
|
||||
jasmine.objectContaining({ type: 'literal', textValue: 'a' }),
|
||||
jasmine.objectContaining({ type: 'literal', textValue: 'b' }),
|
||||
jasmine.objectContaining({ type: 'literal', textValue: 'c' })
|
||||
]
|
||||
},
|
||||
'[aaa]': {
|
||||
label: 'One of:',
|
||||
elements: [
|
||||
jasmine.objectContaining({ type: 'literal', textValue: 'a' })
|
||||
]
|
||||
},
|
||||
'[a-z]': {
|
||||
label: 'One of:',
|
||||
elements: [
|
||||
jasmine.objectContaining({ type: 'charset-range', textValue: 'a-z' })
|
||||
]
|
||||
},
|
||||
'[\\b]': {
|
||||
label: 'One of:',
|
||||
elements: [
|
||||
jasmine.objectContaining({ type: 'charset-escape', textValue: '\\b' })
|
||||
]
|
||||
}
|
||||
|
||||
}, (content, str) => {
|
||||
it(`parses "${str}" as a Charset`, function() {
|
||||
var parser = new javascript.Parser(str);
|
||||
expect(parser.__consume__charset()).toEqual(jasmine.objectContaining(content));
|
||||
});
|
||||
});
|
||||
|
||||
it('adds a warning for character sets the contain non-standard escapes', function() {
|
||||
var node;
|
||||
|
||||
Node.state = { warnings: [] };
|
||||
node = new javascript.Parser('[\\c]').__consume__charset();
|
||||
expect(node.state.warnings).toEqual(['The character set "[\\c]" contains the \\c escape followed by a character other than A-Z. This can lead to different behavior depending on browser. The representation here is the most common interpretation.']);
|
||||
});
|
||||
|
||||
describe('_anchor property', function() {
|
||||
|
||||
it('calculates the anchor based on the partContainer', function() {
|
||||
var node = new javascript.Parser('[a]').__consume__charset();
|
||||
|
||||
node.partContainer = jasmine.createSpyObj('partContainer', ['getBBox']);
|
||||
node.partContainer.getBBox.and.returnValue({
|
||||
cy: 20
|
||||
});
|
||||
|
||||
spyOn(node, 'transform').and.returnValue({
|
||||
localMatrix: Snap.matrix().translate(3, 8)
|
||||
});
|
||||
|
||||
expect(node._anchor).toEqual({
|
||||
ay: 28
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#_render', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
var counter = 0;
|
||||
|
||||
this.node = new javascript.Parser('[a]').__consume__charset();
|
||||
this.node.label = 'example label';
|
||||
this.node.elements = [
|
||||
jasmine.createSpyObj('item', ['render']),
|
||||
jasmine.createSpyObj('item', ['render']),
|
||||
jasmine.createSpyObj('item', ['render'])
|
||||
];
|
||||
this.elementDeferred = [
|
||||
this.testablePromise(),
|
||||
this.testablePromise(),
|
||||
this.testablePromise()
|
||||
];
|
||||
this.node.elements[0].render.and.returnValue(this.elementDeferred[0].promise);
|
||||
this.node.elements[1].render.and.returnValue(this.elementDeferred[1].promise);
|
||||
this.node.elements[2].render.and.returnValue(this.elementDeferred[2].promise);
|
||||
|
||||
this.node.container = Snap(document.createElement('svg')).group();
|
||||
this.partContainer = this.node.container.group();
|
||||
spyOn(this.node.container, 'group').and.returnValue(this.partContainer);
|
||||
spyOn(this.partContainer, 'group').and.callFake(function() {
|
||||
return `group ${counter++}`;
|
||||
});
|
||||
|
||||
spyOn(this.node, 'renderLabeledBox').and.returnValue('labeled box promise');
|
||||
spyOn(util, 'spaceVertically');
|
||||
});
|
||||
|
||||
it('creates a cotainer for the parts of the charset', function() {
|
||||
this.node._render();
|
||||
expect(this.node.partContainer).toEqual(this.partContainer);
|
||||
});
|
||||
|
||||
it('renders each item', function() {
|
||||
this.node._render();
|
||||
expect(this.node.elements[0].render).toHaveBeenCalledWith('group 0');
|
||||
expect(this.node.elements[1].render).toHaveBeenCalledWith('group 1');
|
||||
expect(this.node.elements[2].render).toHaveBeenCalledWith('group 2');
|
||||
});
|
||||
|
||||
describe('positioning of the items', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.elementDeferred[0].resolve();
|
||||
this.elementDeferred[1].resolve();
|
||||
this.elementDeferred[2].resolve();
|
||||
});
|
||||
|
||||
it('spaces the elements vertically', function(done) {
|
||||
this.node._render()
|
||||
.then(() => {
|
||||
expect(util.spaceVertically).toHaveBeenCalledWith(this.node.elements, { padding: 5 });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a labeled box', function(done) {
|
||||
this.node._render()
|
||||
.then(result => {
|
||||
expect(this.node.renderLabeledBox).toHaveBeenCalledWith('example label', this.partContainer, { padding: 5 });
|
||||
expect(result).toEqual('labeled box promise');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,72 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
import _ from 'lodash';
|
||||
import Snap from 'snapsvg';
|
||||
|
||||
describe('parser/javascript/escape.js', function() {
|
||||
|
||||
_.forIn({
|
||||
'\\b': { label: 'word boundary', ordinal: -1 },
|
||||
'\\B': { label: 'non-word boundary', ordinal: -1 },
|
||||
'\\d': { label: 'digit', ordinal: -1 },
|
||||
'\\D': { label: 'non-digit', ordinal: -1 },
|
||||
'\\f': { label: 'form feed (0x0C)', ordinal: 0x0c },
|
||||
'\\n': { label: 'line feed (0x0A)', ordinal: 0x0a },
|
||||
'\\r': { label: 'carriage return (0x0D)', ordinal: 0x0d },
|
||||
'\\s': { label: 'white space', ordinal: -1 },
|
||||
'\\S': { label: 'non-white space', ordinal: -1 },
|
||||
'\\t': { label: 'tab (0x09)', ordinal: 0x09 },
|
||||
'\\v': { label: 'vertical tab (0x0B)', ordinal: 0x0b },
|
||||
'\\w': { label: 'word', ordinal: -1 },
|
||||
'\\W': { label: 'non-word', ordinal: -1 },
|
||||
'\\0': { label: 'null (0x00)', ordinal: 0 },
|
||||
'\\1': { label: 'Back reference (group = 1)', ordinal: -1 },
|
||||
'\\2': { label: 'Back reference (group = 2)', ordinal: -1 },
|
||||
'\\3': { label: 'Back reference (group = 3)', ordinal: -1 },
|
||||
'\\4': { label: 'Back reference (group = 4)', ordinal: -1 },
|
||||
'\\5': { label: 'Back reference (group = 5)', ordinal: -1 },
|
||||
'\\6': { label: 'Back reference (group = 6)', ordinal: -1 },
|
||||
'\\7': { label: 'Back reference (group = 7)', ordinal: -1 },
|
||||
'\\8': { label: 'Back reference (group = 8)', ordinal: -1 },
|
||||
'\\9': { label: 'Back reference (group = 9)', ordinal: -1 },
|
||||
'\\012': { label: 'octal: 12 (0x0A)', ordinal: 10 },
|
||||
'\\cx': { label: 'ctrl-X (0x18)', ordinal: 24 },
|
||||
'\\xab': { label: '0xAB', ordinal: 0xab },
|
||||
'\\uabcd': { label: 'U+ABCD', ordinal: 0xabcd }
|
||||
}, (content, str) => {
|
||||
it(`parses "${str}" as an Escape`, function() {
|
||||
var parser = new javascript.Parser(str);
|
||||
expect(parser.__consume__terminal()).toEqual(jasmine.objectContaining(content));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_render', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
var parser = new javascript.Parser('\\b');
|
||||
this.node = parser.__consume__terminal();
|
||||
this.node.state = {};
|
||||
|
||||
this.svg = Snap(document.createElement('svg'));
|
||||
this.node.container = this.svg.group();
|
||||
spyOn(this.node, 'renderLabel').and.callThrough();
|
||||
});
|
||||
|
||||
it('renders a label', function() {
|
||||
this.node._render();
|
||||
expect(this.node.renderLabel).toHaveBeenCalledWith('word boundary');
|
||||
});
|
||||
|
||||
it('sets the edge radius of the rect', function(done) {
|
||||
this.node._render()
|
||||
.then(label => {
|
||||
expect(label.select('rect').attr()).toEqual(jasmine.objectContaining({
|
||||
rx: '3',
|
||||
ry: '3'
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,77 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
import Snap from 'snapsvg';
|
||||
|
||||
describe('parser/javascript/literal.js', function() {
|
||||
|
||||
it('parses "x" as a Literal', function() {
|
||||
var parser = new javascript.Parser('x');
|
||||
expect(parser.__consume__terminal()).toEqual(jasmine.objectContaining({
|
||||
type: 'literal',
|
||||
literal: 'x',
|
||||
ordinal: 120
|
||||
}));
|
||||
});
|
||||
|
||||
it('parses "\\x" as a Literal', function() {
|
||||
var parser = new javascript.Parser('\\x');
|
||||
expect(parser.__consume__terminal()).toEqual(jasmine.objectContaining({
|
||||
type: 'literal',
|
||||
literal: 'x',
|
||||
ordinal: 120
|
||||
}));
|
||||
});
|
||||
|
||||
describe('#_render', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
var parser = new javascript.Parser('a');
|
||||
this.node = parser.__consume__terminal();
|
||||
this.node.state = {};
|
||||
|
||||
this.svg = Snap(document.createElement('svg'));
|
||||
this.node.container = this.svg.group();
|
||||
spyOn(this.node, 'renderLabel').and.callThrough();
|
||||
});
|
||||
|
||||
it('renders a label', function() {
|
||||
this.node._render();
|
||||
expect(this.node.renderLabel).toHaveBeenCalledWith(['\u201c', 'a', '\u201d']);
|
||||
});
|
||||
|
||||
it('sets the class of the first and third tspan to "quote"', function(done) {
|
||||
this.node._render()
|
||||
.then(label => {
|
||||
expect(label.selectAll('tspan')[0].hasClass('quote')).toBeTruthy();
|
||||
expect(label.selectAll('tspan')[2].hasClass('quote')).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the edge radius of the rect', function(done) {
|
||||
this.node._render()
|
||||
.then(label => {
|
||||
expect(label.select('rect').attr()).toEqual(jasmine.objectContaining({
|
||||
rx: '3',
|
||||
ry: '3'
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#merge', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
var parser = new javascript.Parser('a');
|
||||
this.node = parser.__consume__terminal();
|
||||
});
|
||||
|
||||
it('appends to the literal value', function() {
|
||||
this.node.merge({ literal: 'b' });
|
||||
expect(this.node.literal).toEqual('ab');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,210 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
import _ from 'lodash';
|
||||
import Snap from 'snapsvg';
|
||||
|
||||
describe('parser/javascript/match_fragment.js', function() {
|
||||
|
||||
_.forIn({
|
||||
'a': {
|
||||
proxy: jasmine.objectContaining({ textValue: 'a' }),
|
||||
canMerge: true
|
||||
},
|
||||
'\\b': {
|
||||
proxy: jasmine.objectContaining({ textValue: '\\b' }),
|
||||
canMerge: false
|
||||
},
|
||||
'a*': {
|
||||
content: jasmine.objectContaining({ textValue: 'a' }),
|
||||
repeat: jasmine.objectContaining({ textValue: '*' }),
|
||||
canMerge: false
|
||||
}
|
||||
}, (content, str) => {
|
||||
it(`parses "${str}" as a MatchFragment`, function() {
|
||||
var parser = new javascript.Parser(str);
|
||||
expect(parser.__consume__match_fragment()).toEqual(jasmine.objectContaining(content));
|
||||
});
|
||||
});
|
||||
|
||||
describe('_anchor property', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node = new javascript.Parser('a').__consume__match_fragment();
|
||||
|
||||
this.node.content = {
|
||||
getBBox() {
|
||||
return {
|
||||
ax: 1,
|
||||
ax2: 2,
|
||||
ay: 3
|
||||
};
|
||||
}
|
||||
};
|
||||
spyOn(this.node, 'transform').and.returnValue({
|
||||
localMatrix: Snap.matrix().translate(10, 20)
|
||||
});
|
||||
});
|
||||
|
||||
it('applies the local transform to the content anchor', function() {
|
||||
expect(this.node._anchor).toEqual({
|
||||
ax: 11,
|
||||
ax2: 12,
|
||||
ay: 23
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#_render', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node = new javascript.Parser('a').__consume__match_fragment();
|
||||
|
||||
this.node.container = jasmine.createSpyObj('container', [
|
||||
'addClass',
|
||||
'group',
|
||||
'prepend',
|
||||
'path'
|
||||
]);
|
||||
this.node.container.group.and.returnValue('example group');
|
||||
|
||||
this.renderDeferred = this.testablePromise();
|
||||
this.node.content = jasmine.createSpyObj('content', [
|
||||
'render',
|
||||
'transform',
|
||||
'getBBox'
|
||||
]);
|
||||
this.node.content.getBBox.and.returnValue('content bbox');
|
||||
this.node.content.render.and.returnValue(this.renderDeferred.promise);
|
||||
|
||||
this.node.repeat = {
|
||||
contentPosition: 'example position',
|
||||
skipPath: jasmine.createSpy('skipPath').and.returnValue('skip path'),
|
||||
loopPath: jasmine.createSpy('loopPath').and.returnValue('loop path')
|
||||
};
|
||||
|
||||
spyOn(this.node, 'loopLabel');
|
||||
});
|
||||
|
||||
it('renders the content', function() {
|
||||
this.node._render();
|
||||
expect(this.node.content.render).toHaveBeenCalledWith('example group');
|
||||
});
|
||||
|
||||
describe('positioning of content', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.renderDeferred.resolve();
|
||||
});
|
||||
|
||||
it('moves the content to the correct position', function(done) {
|
||||
this.node._render()
|
||||
.then(() => {
|
||||
expect(this.node.content.transform).toHaveBeenCalledWith('example position');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a skip path and loop path', function(done) {
|
||||
this.node._render()
|
||||
.then(() => {
|
||||
expect(this.node.repeat.skipPath).toHaveBeenCalledWith('content bbox');
|
||||
expect(this.node.repeat.loopPath).toHaveBeenCalledWith('content bbox');
|
||||
expect(this.node.container.path).toHaveBeenCalledWith('skip pathloop path');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a loop label', function(done) {
|
||||
this.node._render()
|
||||
.then(() => {
|
||||
expect(this.node.loopLabel).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#loopLabel', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node = new javascript.Parser('a').__consume__match_fragment();
|
||||
|
||||
this.node.repeat = {};
|
||||
|
||||
this.node.container = jasmine.createSpyObj('container', [
|
||||
'addClass',
|
||||
'text'
|
||||
]);
|
||||
|
||||
this.text = jasmine.createSpyObj('text', [
|
||||
'addClass',
|
||||
'getBBox',
|
||||
'transform'
|
||||
]);
|
||||
this.node.container.text.and.returnValue(this.text);
|
||||
this.text.addClass.and.returnValue(this.text);
|
||||
this.text.getBBox.and.returnValue({
|
||||
width: 11,
|
||||
height: 22
|
||||
});
|
||||
spyOn(this.node, 'getBBox').and.returnValue({
|
||||
x2: 33,
|
||||
y2: 44
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a label is defined', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node.repeat.label = 'example label';
|
||||
});
|
||||
|
||||
it('renders a text element', function() {
|
||||
this.node.loopLabel();
|
||||
expect(this.node.container.text).toHaveBeenCalledWith(0, 0, ['example label']);
|
||||
});
|
||||
|
||||
describe('when there is a skip loop', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node.repeat.hasSkip = true;
|
||||
});
|
||||
|
||||
it('positions the text element', function() {
|
||||
this.node.loopLabel();
|
||||
expect(this.text.transform).toHaveBeenCalledWith(Snap.matrix()
|
||||
.translate(17, 66));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when there is no skip loop', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node.repeat.hasSkip = false;
|
||||
});
|
||||
|
||||
it('positions the text element', function() {
|
||||
this.node.loopLabel();
|
||||
expect(this.text.transform).toHaveBeenCalledWith(Snap.matrix()
|
||||
.translate(22, 66));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when a label is not defined', function() {
|
||||
|
||||
it('does not render a text element', function() {
|
||||
this.node.loopLabel();
|
||||
expect(this.node.container.text).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,196 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
import util from '../../../src/js/util.js';
|
||||
import _ from 'lodash';
|
||||
import Snap from 'snapsvg';
|
||||
|
||||
describe('parser/javascript/match.js', function() {
|
||||
|
||||
_.forIn({
|
||||
'example': {
|
||||
parts: [
|
||||
jasmine.objectContaining({
|
||||
content: jasmine.objectContaining({ literal: 'example' })
|
||||
})
|
||||
],
|
||||
proxy: jasmine.objectContaining({
|
||||
content: jasmine.objectContaining({ literal: 'example' })
|
||||
})
|
||||
},
|
||||
'example*': {
|
||||
parts: [
|
||||
jasmine.objectContaining({
|
||||
content: jasmine.objectContaining({ literal: 'exampl' })
|
||||
}),
|
||||
jasmine.objectContaining({
|
||||
content: jasmine.objectContaining({ literal: 'e' })
|
||||
})
|
||||
]
|
||||
},
|
||||
'': {
|
||||
parts: []
|
||||
}
|
||||
}, (content, str) => {
|
||||
it(`parses "${str}" as a Match`, function() {
|
||||
var parser = new javascript.Parser(str);
|
||||
expect(parser.__consume__match()).toEqual(jasmine.objectContaining(content));
|
||||
});
|
||||
});
|
||||
|
||||
describe('_anchor property', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node = new javascript.Parser('a').__consume__match();
|
||||
|
||||
this.node.start = jasmine.createSpyObj('start', ['getBBox']);
|
||||
this.node.start.getBBox.and.returnValue({
|
||||
x: 1,
|
||||
x2: 2,
|
||||
cy: 3
|
||||
});
|
||||
|
||||
this.node.end = jasmine.createSpyObj('start', ['getBBox']);
|
||||
this.node.end.getBBox.and.returnValue({
|
||||
x: 4,
|
||||
x2: 5,
|
||||
cy: 6
|
||||
});
|
||||
|
||||
spyOn(this.node, 'transform').and.returnValue({
|
||||
localMatrix: Snap.matrix().translate(10, 20)
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates the anchor from the start and end items', function() {
|
||||
expect(this.node._anchor).toEqual({
|
||||
ax: 11,
|
||||
ax2: 15,
|
||||
ay: 23
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#_render', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node = new javascript.Parser('a').__consume__match();
|
||||
|
||||
this.node.container = jasmine.createSpyObj('container', [
|
||||
'addClass',
|
||||
'group',
|
||||
'prepend',
|
||||
'path'
|
||||
]);
|
||||
this.node.container.group.and.returnValue('example group');
|
||||
|
||||
this.node.parts = [
|
||||
jasmine.createSpyObj('part 0', ['render']),
|
||||
jasmine.createSpyObj('part 1', ['render']),
|
||||
jasmine.createSpyObj('part 2', ['render'])
|
||||
];
|
||||
|
||||
this.partDeferreds = [
|
||||
this.testablePromise(),
|
||||
this.testablePromise(),
|
||||
this.testablePromise()
|
||||
];
|
||||
|
||||
this.node.parts[0].render.and.returnValue(this.partDeferreds[0].promise);
|
||||
this.node.parts[1].render.and.returnValue(this.partDeferreds[1].promise);
|
||||
this.node.parts[2].render.and.returnValue(this.partDeferreds[2].promise);
|
||||
});
|
||||
|
||||
it('renders each part', function() {
|
||||
this.node._render();
|
||||
expect(this.node.parts[0].render).toHaveBeenCalledWith('example group');
|
||||
expect(this.node.parts[1].render).toHaveBeenCalledWith('example group');
|
||||
expect(this.node.parts[2].render).toHaveBeenCalledWith('example group');
|
||||
});
|
||||
|
||||
describe('positioning of items', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.partDeferreds[0].resolve('part 0');
|
||||
this.partDeferreds[1].resolve('part 1');
|
||||
this.partDeferreds[2].resolve('part 2');
|
||||
|
||||
spyOn(util, 'spaceHorizontally');
|
||||
spyOn(this.node, 'connectorPaths').and.returnValue(['connector paths']);
|
||||
});
|
||||
|
||||
it('sets the start and end properties', function(done) {
|
||||
this.node._render()
|
||||
.then(() => {
|
||||
expect(this.node.start).toEqual('part 0');
|
||||
expect(this.node.end).toEqual('part 2');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('spaces the items horizontally', function(done) {
|
||||
this.node._render()
|
||||
.then(() => {
|
||||
expect(util.spaceHorizontally).toHaveBeenCalledWith([
|
||||
'part 0',
|
||||
'part 1',
|
||||
'part 2'
|
||||
], { padding: 10 });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the connector paths', function(done) {
|
||||
this.node._render()
|
||||
.then(() => {
|
||||
expect(this.node.connectorPaths).toHaveBeenCalledWith([
|
||||
'part 0',
|
||||
'part 1',
|
||||
'part 2'
|
||||
]);
|
||||
expect(this.node.container.path).toHaveBeenCalledWith('connector paths');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#connectorPaths', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node = new javascript.Parser('a').__consume__match();
|
||||
|
||||
this.items = [
|
||||
jasmine.createSpyObj('item 0', ['getBBox']),
|
||||
jasmine.createSpyObj('item 1', ['getBBox']),
|
||||
jasmine.createSpyObj('item 2', ['getBBox'])
|
||||
];
|
||||
|
||||
this.items[0].getBBox.and.returnValue({
|
||||
x: 10,
|
||||
x2: 20,
|
||||
cy: 5
|
||||
});
|
||||
this.items[1].getBBox.and.returnValue({
|
||||
x: 30,
|
||||
x2: 40,
|
||||
cy: 5
|
||||
});
|
||||
this.items[2].getBBox.and.returnValue({
|
||||
x: 50,
|
||||
x2: 60,
|
||||
cy: 5
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the connector paths between fragments', function() {
|
||||
expect(this.node.connectorPaths(this.items)).toEqual([
|
||||
'M20,5H30',
|
||||
'M40,5H50'
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,422 +0,0 @@
|
||||
import Node from '../../../src/js/parser/javascript/node.js';
|
||||
import Snap from 'snapsvg';
|
||||
|
||||
describe('parser/javascript/node.js', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
Node.state = {};
|
||||
this.node = new Node();
|
||||
});
|
||||
|
||||
it('references the state from Node.state', function() {
|
||||
Node.state.example = 'example state';
|
||||
expect(this.node.state.example).toEqual('example state');
|
||||
});
|
||||
|
||||
describe('module setter', function() {
|
||||
|
||||
it('extends the node with the module', function() {
|
||||
this.node.module = { example: 'value' };
|
||||
expect(this.node.example).toEqual('value');
|
||||
});
|
||||
|
||||
it('calls the module #setup method', function() {
|
||||
var setup = jasmine.createSpy('setup');
|
||||
this.node.module = { setup };
|
||||
expect(setup).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets up any defined properties and removes \'definedProperties\' field', function() {
|
||||
this.node.module = {
|
||||
definedProperties: {
|
||||
example: {
|
||||
get: function() {
|
||||
return 'value';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
expect(this.node.example).toEqual('value');
|
||||
expect(this.node.definedProperties).toBeUndefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('container setter', function() {
|
||||
|
||||
it('adds a class to the container element', function() {
|
||||
var container = jasmine.createSpyObj('container', ['addClass']);
|
||||
this.node.type = 'example type';
|
||||
this.node.container = container;
|
||||
expect(container.addClass).toHaveBeenCalledWith('example type');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('anchor getter', function() {
|
||||
|
||||
describe('when a proxy node is used', function() {
|
||||
|
||||
it('returns the anchor from the proxy', function() {
|
||||
this.node.proxy = { anchor: 'example anchor' };
|
||||
expect(this.node.anchor).toEqual('example anchor');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when a proxy node is not used', function() {
|
||||
|
||||
it('returns _anchor of the node', function() {
|
||||
this.node._anchor = { example: 'value' };
|
||||
expect(this.node.anchor).toEqual({
|
||||
example: 'value'
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#getBBox', function() {
|
||||
|
||||
it('returns the normalized bbox of the container merged with the anchor', function() {
|
||||
this.node.proxy = {
|
||||
anchor: {
|
||||
anchor: 'example anchor'
|
||||
}
|
||||
};
|
||||
this.node.container = jasmine.createSpyObj('container', ['addClass', 'getBBox']);
|
||||
this.node.container.getBBox.and.returnValue({
|
||||
bbox: 'example bbox',
|
||||
x: 'left',
|
||||
x2: 'right',
|
||||
cy: 'center'
|
||||
});
|
||||
expect(this.node.getBBox()).toEqual({
|
||||
bbox: 'example bbox',
|
||||
anchor: 'example anchor',
|
||||
x: 'left',
|
||||
x2: 'right',
|
||||
cy: 'center',
|
||||
ax: 'left',
|
||||
ax2: 'right',
|
||||
ay: 'center'
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#transform', function() {
|
||||
|
||||
it('returns the result of calling transform on the container', function() {
|
||||
this.node.container = jasmine.createSpyObj('container', ['addClass', 'transform']);
|
||||
this.node.container.transform.and.returnValue('transform result');
|
||||
expect(this.node.transform('matrix')).toEqual('transform result');
|
||||
expect(this.node.container.transform).toHaveBeenCalledWith('matrix');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#deferredStep', function() {
|
||||
|
||||
it('resolves the returned promise when the render is not canceled', function(done) {
|
||||
var resolve = jasmine.createSpy('resolve'),
|
||||
reject = jasmine.createSpy('reject');
|
||||
|
||||
this.node.deferredStep('result')
|
||||
.then(resolve, reject)
|
||||
.then(() => {
|
||||
expect(resolve).toHaveBeenCalledWith('result');
|
||||
expect(reject).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects the returned promise when the render is canceled', function(done) {
|
||||
var resolve = jasmine.createSpy('resolve'),
|
||||
reject = jasmine.createSpy('reject');
|
||||
|
||||
this.node.state.cancelRender = true;
|
||||
this.node.deferredStep('result', 'value')
|
||||
.then(resolve, reject)
|
||||
.then(() => {
|
||||
expect(resolve).not.toHaveBeenCalled();
|
||||
expect(reject).toHaveBeenCalledWith('Render cancelled');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#renderLabel', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.group = jasmine.createSpyObj('group', ['addClass', 'rect', 'text']);
|
||||
this.group.addClass.and.returnValue(this.group);
|
||||
|
||||
this.node.container = jasmine.createSpyObj('container', ['addClass', 'group']);
|
||||
this.node.container.group.and.returnValue(this.group);
|
||||
});
|
||||
|
||||
it('adds a "label" class to the group', function() {
|
||||
this.node.renderLabel('example label');
|
||||
expect(this.group.addClass).toHaveBeenCalledWith('label');
|
||||
});
|
||||
|
||||
it('creates a rect element', function() {
|
||||
this.node.renderLabel('example label');
|
||||
expect(this.group.rect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates a text element', function() {
|
||||
this.node.renderLabel('example label');
|
||||
expect(this.group.text).toHaveBeenCalledWith(0, 0, ['example label']);
|
||||
});
|
||||
|
||||
describe('positioning of label elements', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.text = jasmine.createSpyObj('text', ['getBBox', 'transform']);
|
||||
this.rect = jasmine.createSpyObj('rect', ['attr']);
|
||||
|
||||
this.text.getBBox.and.returnValue({
|
||||
width: 42,
|
||||
height: 24
|
||||
});
|
||||
|
||||
this.group.text.and.returnValue(this.text);
|
||||
this.group.rect.and.returnValue(this.rect);
|
||||
});
|
||||
|
||||
it('transforms the text element', function(done) {
|
||||
this.node.renderLabel('example label')
|
||||
.then(() => {
|
||||
expect(this.text.transform).toHaveBeenCalledWith(Snap.matrix()
|
||||
.translate(5, 22));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the dimensions of the rect element', function(done) {
|
||||
this.node.renderLabel('example label')
|
||||
.then(() => {
|
||||
expect(this.rect.attr).toHaveBeenCalledWith({
|
||||
width: 52,
|
||||
height: 34
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves with the group element', function(done) {
|
||||
this.node.renderLabel('example label')
|
||||
.then(group => {
|
||||
expect(group).toEqual(this.group);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#render', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.container = jasmine.createSpyObj('container', ['addClass']);
|
||||
});
|
||||
|
||||
describe('when a proxy node is used', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node.proxy = jasmine.createSpyObj('proxy', ['render']);
|
||||
this.node.proxy.render.and.returnValue('example proxy result');
|
||||
});
|
||||
|
||||
it('sets the container', function() {
|
||||
this.node.render(this.container);
|
||||
expect(this.node.container).toEqual(this.container);
|
||||
});
|
||||
|
||||
it('calls the proxy render method', function() {
|
||||
expect(this.node.render(this.container)).toEqual('example proxy result');
|
||||
expect(this.node.proxy.render).toHaveBeenCalledWith(this.container);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when a proxy node is not used', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.deferred = this.testablePromise();
|
||||
this.node._render = jasmine.createSpy('_render').and.returnValue(this.deferred.promise);
|
||||
});
|
||||
|
||||
it('sets the container', function() {
|
||||
this.node.render(this.container);
|
||||
expect(this.node.container).toEqual(this.container);
|
||||
});
|
||||
|
||||
it('increments the renderCounter', function() {
|
||||
this.node.state.renderCounter = 0;
|
||||
this.node.render(this.container);
|
||||
expect(this.node.state.renderCounter).toEqual(1);
|
||||
});
|
||||
|
||||
it('calls #_render', function() {
|
||||
this.node.render(this.container);
|
||||
expect(this.node._render).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when #_render is complete', function() {
|
||||
|
||||
it('decrements the renderCounter', function(done) {
|
||||
this.node.render(this.container)
|
||||
.then(() => {
|
||||
expect(this.node.state.renderCounter).toEqual(41);
|
||||
done();
|
||||
});
|
||||
this.node.state.renderCounter = 42;
|
||||
this.deferred.resolve();
|
||||
});
|
||||
|
||||
it('ultimately resolves with the node instance', function(done) {
|
||||
this.deferred.resolve();
|
||||
this.node.render(this.container)
|
||||
.then(result => {
|
||||
expect(result).toEqual(this.node);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#renderLabeledBox', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
var svg = Snap(document.createElement('svg'));
|
||||
|
||||
this.text = svg.text();
|
||||
this.rect = svg.rect();
|
||||
this.content = svg.rect();
|
||||
|
||||
this.node.container = jasmine.createSpyObj('container', ['addClass', 'text', 'rect', 'prepend']);
|
||||
this.node.container.text.and.returnValue(this.text);
|
||||
this.node.container.rect.and.returnValue(this.rect);
|
||||
|
||||
this.node.type = 'example-type';
|
||||
});
|
||||
|
||||
it('creates a text element', function() {
|
||||
this.node.renderLabeledBox('example label', this.content, { padding: 5 });
|
||||
expect(this.node.container.text).toHaveBeenCalledWith(0, 0, ['example label']);
|
||||
});
|
||||
|
||||
it('sets the class on the text element', function() {
|
||||
spyOn(this.text, 'addClass').and.callThrough();
|
||||
this.node.renderLabeledBox('example label', this.content, { padding: 5 });
|
||||
expect(this.text.addClass).toHaveBeenCalledWith('example-type-label');
|
||||
});
|
||||
|
||||
it('creates a rect element', function() {
|
||||
this.node.renderLabeledBox('example label', this.content, { padding: 5 });
|
||||
expect(this.node.container.rect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets the class on the rect element', function() {
|
||||
spyOn(this.rect, 'addClass').and.callThrough();
|
||||
this.node.renderLabeledBox('example label', this.content, { padding: 5 });
|
||||
expect(this.rect.addClass).toHaveBeenCalledWith('example-type-box');
|
||||
});
|
||||
|
||||
it('sets the corner radius on the rect element', function() {
|
||||
spyOn(this.rect, 'attr').and.callThrough();
|
||||
this.node.renderLabeledBox('example label', this.content, { padding: 5 });
|
||||
expect(this.rect.attr).toHaveBeenCalledWith({
|
||||
rx: 3,
|
||||
ry: 3
|
||||
});
|
||||
});
|
||||
|
||||
describe('positioning of elements', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
spyOn(this.text, 'getBBox').and.returnValue({
|
||||
width: 100,
|
||||
height: 20
|
||||
});
|
||||
spyOn(this.content, 'getBBox').and.returnValue({
|
||||
width: 200,
|
||||
height: 100,
|
||||
cx: 100
|
||||
});
|
||||
});
|
||||
|
||||
it('positions the text element', function(done) {
|
||||
spyOn(this.text, 'transform').and.callThrough();
|
||||
this.node.renderLabeledBox('example label', this.content, { padding: 5 })
|
||||
.then(() => {
|
||||
expect(this.text.transform).toHaveBeenCalledWith(Snap.matrix()
|
||||
.translate(0, 20));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('positions the rect element', function(done) {
|
||||
spyOn(this.rect, 'transform').and.callThrough();
|
||||
this.node.renderLabeledBox('example label', this.content, { padding: 5 })
|
||||
.then(() => {
|
||||
expect(this.rect.transform).toHaveBeenCalledWith(Snap.matrix()
|
||||
.translate(0, 20));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the dimensions of the rect element', function(done) {
|
||||
spyOn(this.rect, 'attr').and.callThrough();
|
||||
this.node.renderLabeledBox('example label', this.content, { padding: 5 })
|
||||
.then(() => {
|
||||
expect(this.rect.attr).toHaveBeenCalledWith({
|
||||
width: 210,
|
||||
height: 110
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the dimensions of the rect element (based on the text element)', function(done) {
|
||||
this.content.getBBox.and.returnValue({
|
||||
width: 50,
|
||||
height: 100,
|
||||
cx: 25
|
||||
});
|
||||
spyOn(this.rect, 'attr').and.callThrough();
|
||||
this.node.renderLabeledBox('example label', this.content, { padding: 5 })
|
||||
.then(() => {
|
||||
expect(this.rect.attr).toHaveBeenCalledWith({
|
||||
width: 100,
|
||||
height: 110
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('positions the content element', function(done) {
|
||||
spyOn(this.content, 'transform').and.callThrough();
|
||||
this.node.renderLabeledBox('example label', this.content, { padding: 5 })
|
||||
.then(() => {
|
||||
expect(this.content.transform).toHaveBeenCalledWith(Snap.matrix()
|
||||
.translate(5, 25));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,30 +0,0 @@
|
||||
import ParserState from '../../../src/js/parser/javascript/parser_state.js';
|
||||
|
||||
describe('parser/javascript/parser_state.js', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.progress = { style: {} };
|
||||
this.state = new ParserState(this.progress);
|
||||
});
|
||||
|
||||
describe('renderCounter property', function() {
|
||||
|
||||
it('sets the width of the progress element to the percent of completed steps', function() {
|
||||
this.state.renderCounter = 50;
|
||||
expect(this.progress.style.width).toEqual('0.00%');
|
||||
this.state.renderCounter = 10;
|
||||
expect(this.progress.style.width).toEqual('80.00%');
|
||||
});
|
||||
|
||||
it('does not change the width of the progress element when rendering has been cancelled', function() {
|
||||
this.state.renderCounter = 50;
|
||||
this.state.renderCounter = 40;
|
||||
expect(this.progress.style.width).toEqual('20.00%');
|
||||
this.state.cancelRender = true;
|
||||
this.state.renderCounter = 10;
|
||||
expect(this.progress.style.width).toEqual('20.00%');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,311 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
import util from '../../../src/js/util.js';
|
||||
import _ from 'lodash';
|
||||
import Snap from 'snapsvg';
|
||||
|
||||
describe('parser/javascript/regexp.js', function() {
|
||||
|
||||
_.forIn({
|
||||
'test': {
|
||||
proxy: jasmine.objectContaining({ textValue: 'test' })
|
||||
},
|
||||
'part 1|part 2': {
|
||||
matches: [
|
||||
jasmine.objectContaining({ textValue: 'part 1' }),
|
||||
jasmine.objectContaining({ textValue: 'part 2' })
|
||||
]
|
||||
}
|
||||
}, (content, str) => {
|
||||
it(`parses "${str}" as a Regexp`, function() {
|
||||
var parser = new javascript.Parser(str);
|
||||
expect(parser.__consume__regexp()).toEqual(jasmine.objectContaining(content));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_render', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
var counter = 0;
|
||||
|
||||
this.node = new javascript.Parser('a|b').__consume__regexp();
|
||||
|
||||
this.node.container = jasmine.createSpyObj('container', [
|
||||
'addClass',
|
||||
'group',
|
||||
'prepend',
|
||||
'path'
|
||||
]);
|
||||
|
||||
this.group = jasmine.createSpyObj('group', [
|
||||
'addClass',
|
||||
'transform',
|
||||
'group',
|
||||
'prepend',
|
||||
'path',
|
||||
'getBBox'
|
||||
]);
|
||||
this.node.container.group.and.returnValue(this.group);
|
||||
this.group.addClass.and.returnValue(this.group);
|
||||
this.group.transform.and.returnValue(this.group);
|
||||
this.group.getBBox.and.returnValue('group bbox');
|
||||
this.group.group.and.callFake(function() {
|
||||
return `group ${counter++}`;
|
||||
});
|
||||
|
||||
this.node.matches = [
|
||||
jasmine.createSpyObj('match', ['render']),
|
||||
jasmine.createSpyObj('match', ['render']),
|
||||
jasmine.createSpyObj('match', ['render'])
|
||||
];
|
||||
|
||||
this.matchDeferred = [
|
||||
this.testablePromise(),
|
||||
this.testablePromise(),
|
||||
this.testablePromise()
|
||||
];
|
||||
|
||||
this.node.matches[0].render.and.returnValue(this.matchDeferred[0].promise);
|
||||
this.node.matches[1].render.and.returnValue(this.matchDeferred[1].promise);
|
||||
this.node.matches[2].render.and.returnValue(this.matchDeferred[2].promise);
|
||||
|
||||
spyOn(this.node, 'getBBox').and.returnValue('container bbox');
|
||||
spyOn(this.node, 'makeCurve').and.returnValue('curve');
|
||||
spyOn(this.node, 'makeSide').and.returnValue('side');
|
||||
spyOn(this.node, 'makeConnector').and.returnValue('connector');
|
||||
|
||||
spyOn(util, 'spaceVertically');
|
||||
});
|
||||
|
||||
it('creates a container for the match nodes', function() {
|
||||
this.node._render();
|
||||
expect(this.node.container.group).toHaveBeenCalled();
|
||||
expect(this.group.addClass).toHaveBeenCalledWith('regexp-matches');
|
||||
expect(this.group.transform).toHaveBeenCalledWith(Snap.matrix()
|
||||
.translate(20, 0));
|
||||
});
|
||||
|
||||
it('renders each match node', function() {
|
||||
this.node._render();
|
||||
expect(this.node.matches[0].render).toHaveBeenCalledWith('group 0');
|
||||
expect(this.node.matches[1].render).toHaveBeenCalledWith('group 1');
|
||||
expect(this.node.matches[2].render).toHaveBeenCalledWith('group 2');
|
||||
});
|
||||
|
||||
describe('positioning of the match nodes', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.matchDeferred[0].resolve();
|
||||
this.matchDeferred[1].resolve();
|
||||
this.matchDeferred[2].resolve();
|
||||
});
|
||||
|
||||
it('spaces the nodes vertically', function(done) {
|
||||
this.node._render()
|
||||
.then(() => {
|
||||
expect(util.spaceVertically).toHaveBeenCalledWith(this.node.matches, { padding: 5 });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the sides and curves into the container', function(done) {
|
||||
this.node._render()
|
||||
.then(() => {
|
||||
expect(this.node.makeCurve).toHaveBeenCalledWith('container bbox', this.node.matches[0]);
|
||||
expect(this.node.makeCurve).toHaveBeenCalledWith('container bbox', this.node.matches[1]);
|
||||
expect(this.node.makeCurve).toHaveBeenCalledWith('container bbox', this.node.matches[2]);
|
||||
expect(this.node.makeSide).toHaveBeenCalledWith('container bbox', this.node.matches[0]);
|
||||
expect(this.node.makeSide).toHaveBeenCalledWith('container bbox', this.node.matches[2]);
|
||||
expect(this.node.container.path).toHaveBeenCalledWith('curvecurvecurvesideside');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the connectors into the match container', function(done) {
|
||||
this.node._render()
|
||||
.then(() => {
|
||||
expect(this.node.makeConnector).toHaveBeenCalledWith('group bbox', this.node.matches[0]);
|
||||
expect(this.node.makeConnector).toHaveBeenCalledWith('group bbox', this.node.matches[1]);
|
||||
expect(this.node.makeConnector).toHaveBeenCalledWith('group bbox', this.node.matches[2]);
|
||||
expect(this.group.path).toHaveBeenCalledWith('connectorconnectorconnector');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#madeSide', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node = new javascript.Parser('a|b').__consume__regexp();
|
||||
|
||||
this.containerBox = {
|
||||
cy: 50,
|
||||
width: 30
|
||||
};
|
||||
this.matchBox = {
|
||||
};
|
||||
|
||||
this.match = jasmine.createSpyObj('match', ['getBBox']);
|
||||
this.match.getBBox.and.returnValue(this.matchBox);
|
||||
});
|
||||
|
||||
describe('when the match node is 15px or more from the centerline', function() {
|
||||
|
||||
describe('when the match node is above the centerline', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.matchBox.ay = 22;
|
||||
});
|
||||
|
||||
it('returns the vertical sideline to the match node', function() {
|
||||
expect(this.node.makeSide(this.containerBox, this.match)).toEqual([
|
||||
'M0,50q10,0 10,-10V32',
|
||||
'M70,50q-10,0 -10,-10V32'
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when the match node is below the centerline', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.matchBox.ay = 88;
|
||||
});
|
||||
|
||||
it('returns the vertical sideline to the match node', function() {
|
||||
expect(this.node.makeSide(this.containerBox, this.match)).toEqual([
|
||||
'M0,50q10,0 10,10V78',
|
||||
'M70,50q-10,0 -10,10V78'
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when the match node is less than 15px from the centerline', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.matchBox.ay = 44;
|
||||
});
|
||||
|
||||
it('returns nothing', function() {
|
||||
expect(this.node.makeSide(this.containerBox, this.match)).toBeUndefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#makeCurve', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node = new javascript.Parser('a|b').__consume__regexp();
|
||||
|
||||
this.containerBox = {
|
||||
cy: 50,
|
||||
width: 30
|
||||
};
|
||||
this.matchBox = {};
|
||||
|
||||
this.match = jasmine.createSpyObj('match', ['getBBox']);
|
||||
this.match.getBBox.and.returnValue(this.matchBox);
|
||||
});
|
||||
|
||||
describe('when the match node is 15px or more from the centerline', function() {
|
||||
|
||||
describe('when the match node is above the centerline', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.matchBox.ay = 22;
|
||||
});
|
||||
|
||||
it('returns the curve to the match node', function() {
|
||||
expect(this.node.makeCurve(this.containerBox, this.match)).toEqual([
|
||||
'M10,32q0,-10 10,-10',
|
||||
'M60,32q0,-10 -10,-10'
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when the match node is below the centerline', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.matchBox.ay = 88;
|
||||
});
|
||||
|
||||
it('returns the curve to the match node', function() {
|
||||
expect(this.node.makeCurve(this.containerBox, this.match)).toEqual([
|
||||
'M10,78q0,10 10,10',
|
||||
'M60,78q0,10 -10,10'
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when the match node is less than 15px from the centerline', function() {
|
||||
|
||||
describe('when the match node is above the centerline', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.matchBox.ay = 44;
|
||||
});
|
||||
|
||||
it('returns the curve to the match node', function() {
|
||||
expect(this.node.makeCurve(this.containerBox, this.match)).toEqual([
|
||||
'M0,50c10,0 10,-6 20,-6',
|
||||
'M70,50c-10,0 -10,-6 -20,-6'
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when the match node is below the centerline', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.matchBox.ay = 55;
|
||||
});
|
||||
|
||||
it('returns the curve to the match node', function() {
|
||||
expect(this.node.makeCurve(this.containerBox, this.match)).toEqual([
|
||||
'M0,50c10,0 10,5 20,5',
|
||||
'M70,50c-10,0 -10,5 -20,5'
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#makeConnector', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node = new javascript.Parser('a|b').__consume__regexp();
|
||||
|
||||
this.containerBox = {
|
||||
width: 4
|
||||
};
|
||||
this.matchBox = {
|
||||
ay: 1,
|
||||
ax: 2,
|
||||
ax2: 3
|
||||
};
|
||||
|
||||
this.match = jasmine.createSpyObj('match', ['getBBox']);
|
||||
this.match.getBBox.and.returnValue(this.matchBox);
|
||||
});
|
||||
|
||||
it('returns a line from the curve to the match node', function() {
|
||||
expect(this.node.makeConnector(this.containerBox, this.match)).toEqual('M0,1h2M3,1H4');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,13 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
|
||||
describe('parser/javascript/repeat_any.js', function() {
|
||||
|
||||
it('parses "*" as a RepeatAny', function() {
|
||||
var parser = new javascript.Parser('*');
|
||||
expect(parser.__consume__repeat_any()).toEqual(jasmine.objectContaining({
|
||||
minimum: 0,
|
||||
maximum: -1
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
@ -1,13 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
|
||||
describe('parser/javascript/repeat_optional.js', function() {
|
||||
|
||||
it('parses "?" as a RepeatOptional', function() {
|
||||
var parser = new javascript.Parser('?');
|
||||
expect(parser.__consume__repeat_optional()).toEqual(jasmine.objectContaining({
|
||||
minimum: 0,
|
||||
maximum: 1
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
@ -1,13 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
|
||||
describe('parser/javascript/repeat_required.js', function() {
|
||||
|
||||
it('parses "+" as a RepeatRequired', function() {
|
||||
var parser = new javascript.Parser('+');
|
||||
expect(parser.__consume__repeat_required()).toEqual(jasmine.objectContaining({
|
||||
minimum: 1,
|
||||
maximum: -1
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
@ -1,410 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
import _ from 'lodash';
|
||||
import Snap from 'snapsvg';
|
||||
|
||||
describe('parser/javascript/repeat.js', function() {
|
||||
|
||||
_.forIn({
|
||||
'*': {
|
||||
minimum: 0,
|
||||
maximum: -1,
|
||||
greedy: true,
|
||||
hasSkip: true,
|
||||
hasLoop: true
|
||||
},
|
||||
'*?': {
|
||||
minimum: 0,
|
||||
maximum: -1,
|
||||
greedy: false,
|
||||
hasSkip: true,
|
||||
hasLoop: true
|
||||
},
|
||||
'+': {
|
||||
minimum: 1,
|
||||
maximum: -1,
|
||||
greedy: true,
|
||||
hasSkip: false,
|
||||
hasLoop: true
|
||||
},
|
||||
'+?': {
|
||||
minimum: 1,
|
||||
maximum: -1,
|
||||
greedy: false,
|
||||
hasSkip: false,
|
||||
hasLoop: true
|
||||
},
|
||||
'?': {
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
greedy: true,
|
||||
hasSkip: true,
|
||||
hasLoop: false
|
||||
},
|
||||
'??': {
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
greedy: false,
|
||||
hasSkip: true,
|
||||
hasLoop: false
|
||||
},
|
||||
'{1}': {
|
||||
minimum: 1,
|
||||
maximum: 1,
|
||||
greedy: true,
|
||||
hasSkip: false,
|
||||
hasLoop: false
|
||||
},
|
||||
'{0}': {
|
||||
minimum: 0,
|
||||
maximum: 0,
|
||||
greedy: true,
|
||||
hasSkip: true,
|
||||
hasLoop: false
|
||||
},
|
||||
'{1}?': {
|
||||
minimum: 1,
|
||||
maximum: 1,
|
||||
greedy: false,
|
||||
hasSkip: false,
|
||||
hasLoop: false
|
||||
},
|
||||
'{2}': {
|
||||
minimum: 2,
|
||||
maximum: 2,
|
||||
greedy: true,
|
||||
hasSkip: false,
|
||||
hasLoop: true
|
||||
},
|
||||
'{2}?': {
|
||||
minimum: 2,
|
||||
maximum: 2,
|
||||
greedy: false,
|
||||
hasSkip: false,
|
||||
hasLoop: true
|
||||
},
|
||||
'{0,}': {
|
||||
minimum: 0,
|
||||
maximum: -1,
|
||||
greedy: true,
|
||||
hasSkip: true,
|
||||
hasLoop: true
|
||||
},
|
||||
'{0,}?': {
|
||||
minimum: 0,
|
||||
maximum: -1,
|
||||
greedy: false,
|
||||
hasSkip: true,
|
||||
hasLoop: true
|
||||
},
|
||||
'{1,}': {
|
||||
minimum: 1,
|
||||
maximum: -1,
|
||||
greedy: true,
|
||||
hasSkip: false,
|
||||
hasLoop: true
|
||||
},
|
||||
'{1,}?': {
|
||||
minimum: 1,
|
||||
maximum: -1,
|
||||
greedy: false,
|
||||
hasSkip: false,
|
||||
hasLoop: true
|
||||
},
|
||||
'{0,1}': {
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
greedy: true,
|
||||
hasSkip: true,
|
||||
hasLoop: false
|
||||
},
|
||||
'{0,1}?': {
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
greedy: false,
|
||||
hasSkip: true,
|
||||
hasLoop: false
|
||||
},
|
||||
'{0,2}': {
|
||||
minimum: 0,
|
||||
maximum: 2,
|
||||
greedy: true,
|
||||
hasSkip: true,
|
||||
hasLoop: true
|
||||
},
|
||||
'{0,2}?': {
|
||||
minimum: 0,
|
||||
maximum: 2,
|
||||
greedy: false,
|
||||
hasSkip: true,
|
||||
hasLoop: true
|
||||
},
|
||||
'{1,2}': {
|
||||
minimum: 1,
|
||||
maximum: 2,
|
||||
greedy: true,
|
||||
hasSkip: false,
|
||||
hasLoop: true
|
||||
},
|
||||
'{1,2}?': {
|
||||
minimum: 1,
|
||||
maximum: 2,
|
||||
greedy: false,
|
||||
hasSkip: false,
|
||||
hasLoop: true
|
||||
}
|
||||
}, (content, str) => {
|
||||
it(`parses "${str}" as a Repeat`, function() {
|
||||
var parser = new javascript.Parser(str);
|
||||
expect(parser.__consume__repeat()).toEqual(jasmine.objectContaining(content));
|
||||
});
|
||||
});
|
||||
|
||||
describe('contentPosition property', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node = new javascript.Parser('*').__consume__repeat();
|
||||
});
|
||||
|
||||
_.each([
|
||||
{
|
||||
hasLoop: false,
|
||||
hasSkip: false,
|
||||
translate: { x: 0, y: 0 }
|
||||
},
|
||||
{
|
||||
hasLoop: true,
|
||||
hasSkip: false,
|
||||
translate: { x: 10, y: 0 }
|
||||
},
|
||||
{
|
||||
hasLoop: false,
|
||||
hasSkip: true,
|
||||
translate: { x: 15, y: 10 }
|
||||
},
|
||||
{
|
||||
hasLoop: true,
|
||||
hasSkip: true,
|
||||
translate: { x: 15, y: 10 }
|
||||
}
|
||||
], t => {
|
||||
it(`translates to [${t.translate.x}, ${t.translate.y}] when hasLoop is ${t.hasLoop} and hasSkip is ${t.hasSkip}`, function() {
|
||||
this.node.hasLoop = t.hasLoop;
|
||||
this.node.hasSkip = t.hasSkip;
|
||||
expect(this.node.contentPosition).toEqual(Snap.matrix()
|
||||
.translate(t.translate.x, t.translate.y));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('label property', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node = new javascript.Parser('*').__consume__repeat();
|
||||
});
|
||||
|
||||
_.each([
|
||||
{
|
||||
minimum: 1,
|
||||
maximum: -1,
|
||||
label: undefined
|
||||
},
|
||||
{
|
||||
minimum: 0,
|
||||
maximum: 0,
|
||||
label: undefined
|
||||
},
|
||||
{
|
||||
minimum: 2,
|
||||
maximum: -1,
|
||||
label: '1+ times'
|
||||
},
|
||||
{
|
||||
minimum: 3,
|
||||
maximum: -1,
|
||||
label: '2+ times'
|
||||
},
|
||||
{
|
||||
minimum: 0,
|
||||
maximum: 2,
|
||||
label: 'at most once'
|
||||
},
|
||||
{
|
||||
minimum: 0,
|
||||
maximum: 3,
|
||||
label: 'at most 2 times'
|
||||
},
|
||||
{
|
||||
minimum: 2,
|
||||
maximum: 2,
|
||||
label: 'once'
|
||||
},
|
||||
{
|
||||
minimum: 3,
|
||||
maximum: 3,
|
||||
label: '2 times'
|
||||
},
|
||||
{
|
||||
minimum: 2,
|
||||
maximum: 3,
|
||||
label: '1\u20262 times'
|
||||
},
|
||||
{
|
||||
minimum: 3,
|
||||
maximum: 4,
|
||||
label: '2\u20263 times'
|
||||
}
|
||||
|
||||
], t => {
|
||||
it(`is "${t.label}" when minimum=${t.minimum} and maximum=${t.maximum}`, function() {
|
||||
this.node.minimum = t.minimum;
|
||||
this.node.maximum = t.maximum;
|
||||
expect(this.node.label).toEqual(t.label);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('tooltip property', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node = new javascript.Parser('*').__consume__repeat();
|
||||
});
|
||||
|
||||
_.each([
|
||||
{
|
||||
minimum: 1,
|
||||
maximum: -1,
|
||||
tooltip: undefined
|
||||
},
|
||||
{
|
||||
minimum: 0,
|
||||
maximum: 0,
|
||||
tooltip: undefined
|
||||
},
|
||||
{
|
||||
minimum: 2,
|
||||
maximum: -1,
|
||||
tooltip: 'repeats 2+ times in total'
|
||||
},
|
||||
{
|
||||
minimum: 3,
|
||||
maximum: -1,
|
||||
tooltip: 'repeats 3+ times in total'
|
||||
},
|
||||
{
|
||||
minimum: 0,
|
||||
maximum: 2,
|
||||
tooltip: 'repeats at most 2 times in total'
|
||||
},
|
||||
{
|
||||
minimum: 0,
|
||||
maximum: 3,
|
||||
tooltip: 'repeats at most 3 times in total'
|
||||
},
|
||||
{
|
||||
minimum: 2,
|
||||
maximum: 2,
|
||||
tooltip: 'repeats 2 times in total'
|
||||
},
|
||||
{
|
||||
minimum: 3,
|
||||
maximum: 3,
|
||||
tooltip: 'repeats 3 times in total'
|
||||
},
|
||||
{
|
||||
minimum: 2,
|
||||
maximum: 3,
|
||||
tooltip: 'repeats 2\u20263 times in total'
|
||||
},
|
||||
{
|
||||
minimum: 3,
|
||||
maximum: 4,
|
||||
tooltip: 'repeats 3\u20264 times in total'
|
||||
}
|
||||
|
||||
], t => {
|
||||
it(`is "${t.tooltip}" when minimum=${t.minimum} and maximum=${t.maximum}`, function() {
|
||||
this.node.minimum = t.minimum;
|
||||
this.node.maximum = t.maximum;
|
||||
expect(this.node.tooltip).toEqual(t.tooltip);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#skipPath', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node = new javascript.Parser('*').__consume__repeat();
|
||||
|
||||
this.box = {
|
||||
y: 11,
|
||||
ay: 22,
|
||||
width: 33
|
||||
};
|
||||
});
|
||||
|
||||
it('returns nothing when there is no skip', function() {
|
||||
this.node.hasSkip = false;
|
||||
expect(this.node.skipPath(this.box)).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns a path when there is a skip', function() {
|
||||
this.node.hasSkip = true;
|
||||
this.node.greedy = true;
|
||||
expect(this.node.skipPath(this.box)).toEqual([
|
||||
'M0,22q10,0 10,-10v-1q0,-10 10,-10h23q10,0 10,10v1q0,10 10,10'
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns a path with arrow when there is a non-greedy skip', function() {
|
||||
this.node.hasSkip = true;
|
||||
this.node.greedy = false;
|
||||
expect(this.node.skipPath(this.box)).toEqual([
|
||||
'M0,22q10,0 10,-10v-1q0,-10 10,-10h23q10,0 10,10v1q0,10 10,10',
|
||||
'M10,7l5,5m-5,-5l-5,5'
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#loopPath', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node = new javascript.Parser('*').__consume__repeat();
|
||||
|
||||
this.box = {
|
||||
x: 11,
|
||||
x2: 22,
|
||||
ay: 33,
|
||||
y2: 44,
|
||||
width: 55
|
||||
};
|
||||
});
|
||||
|
||||
it('returns nothing when there is no loop', function() {
|
||||
this.node.hasLoop = false;
|
||||
expect(this.node.loopPath(this.box)).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns a path when there is a loop', function() {
|
||||
this.node.hasLoop = true;
|
||||
this.node.greedy = false;
|
||||
expect(this.node.loopPath(this.box)).toEqual([
|
||||
'M11,33q-10,0 -10,10v1q0,10 10,10h55q10,0 10,-10v-1q0,-10 -10,-10'
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns a path with arrow when there is a greedy loop', function() {
|
||||
this.node.hasLoop = true;
|
||||
this.node.greedy = true;
|
||||
expect(this.node.loopPath(this.box)).toEqual([
|
||||
'M11,33q-10,0 -10,10v1q0,10 10,10h55q10,0 10,-10v-1q0,-10 -10,-10',
|
||||
'M32,48l5,-5m-5,5l-5,-5'
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,41 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
|
||||
describe('parser/javascript/repeat_spec.js', function() {
|
||||
|
||||
it('parses "{n,m}" as a RepeatSpec (with minimum and maximum values)', function() {
|
||||
var parser = new javascript.Parser('{24,42}');
|
||||
expect(parser.__consume__repeat_spec()).toEqual(jasmine.objectContaining({
|
||||
minimum: 24,
|
||||
maximum: 42
|
||||
}));
|
||||
});
|
||||
|
||||
it('parses "{n,}" as a RepeatSpec (with only minimum value)', function() {
|
||||
var parser = new javascript.Parser('{24,}');
|
||||
expect(parser.__consume__repeat_spec()).toEqual(jasmine.objectContaining({
|
||||
minimum: 24,
|
||||
maximum: -1
|
||||
}));
|
||||
});
|
||||
|
||||
it('parses "{n}" as a RepeatSpec (with an exact count)', function() {
|
||||
var parser = new javascript.Parser('{24}');
|
||||
expect(parser.__consume__repeat_spec()).toEqual(jasmine.objectContaining({
|
||||
minimum: 24,
|
||||
maximum: 24
|
||||
}));
|
||||
});
|
||||
|
||||
it('does not parse "{,m}" as a RepeatSpec', function() {
|
||||
var parser = new javascript.Parser('{,42}');
|
||||
expect(parser.__consume__repeat_spec()).toEqual(null);
|
||||
});
|
||||
|
||||
it('throws an exception when the numbers are out of order', function() {
|
||||
var parser = new javascript.Parser('{42,24}');
|
||||
expect(() => {
|
||||
parser.__consume__repeat_spec();
|
||||
}).toThrow('Numbers out of order: {42,24}');
|
||||
});
|
||||
|
||||
});
|
@ -1,175 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
import Snap from 'snapsvg';
|
||||
import _ from 'lodash';
|
||||
|
||||
describe('parser/javascript/root.js', function() {
|
||||
|
||||
_.forIn({
|
||||
'test': {
|
||||
flags: [],
|
||||
regexp: jasmine.objectContaining({ textValue: 'test' })
|
||||
},
|
||||
'/test/': {
|
||||
flags: [],
|
||||
regexp: jasmine.objectContaining({ textValue: 'test' })
|
||||
},
|
||||
'/test/i': {
|
||||
flags: ['Ignore Case'],
|
||||
regexp: jasmine.objectContaining({ textValue: 'test' })
|
||||
},
|
||||
'/test/g': {
|
||||
flags: ['Global'],
|
||||
regexp: jasmine.objectContaining({ textValue: 'test' })
|
||||
},
|
||||
'/test/m': {
|
||||
flags: ['Multiline'],
|
||||
regexp: jasmine.objectContaining({ textValue: 'test' })
|
||||
},
|
||||
'/test/y': {
|
||||
flags: ['Sticky'],
|
||||
regexp: jasmine.objectContaining({ textValue: 'test' })
|
||||
},
|
||||
'/test/u': {
|
||||
flags: ['Unicode'],
|
||||
regexp: jasmine.objectContaining({ textValue: 'test' })
|
||||
},
|
||||
'/test/mgi': {
|
||||
flags: ['Global', 'Ignore Case', 'Multiline'],
|
||||
regexp: jasmine.objectContaining({ textValue: 'test' })
|
||||
}
|
||||
}, (content, str) => {
|
||||
it(`parses "${str}" as a Root`, function() {
|
||||
var parser = new javascript.Parser(str);
|
||||
expect(parser.__consume__root()).toEqual(jasmine.objectContaining(content));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_render', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.textElement = jasmine.createSpyObj('text', ['getBBox']);
|
||||
this.textElement.getBBox.and.returnValue({
|
||||
height: 20
|
||||
});
|
||||
|
||||
this.node = new javascript.Parser('test').__consume__root();
|
||||
this.node.container = jasmine.createSpyObj('container', [
|
||||
'addClass',
|
||||
'text',
|
||||
'group',
|
||||
'path',
|
||||
'circle'
|
||||
]);
|
||||
this.node.container.text.and.returnValue(this.textElement);
|
||||
this.node.container.group.and.returnValue('group element');
|
||||
|
||||
this.node.regexp = jasmine.createSpyObj('regexp', [
|
||||
'render',
|
||||
'transform',
|
||||
'getBBox'
|
||||
]);
|
||||
|
||||
this.renderDeferred = this.testablePromise();
|
||||
this.node.regexp.render.and.returnValue(this.renderDeferred.promise);
|
||||
});
|
||||
|
||||
it('renders the regexp', function() {
|
||||
this.node._render();
|
||||
expect(this.node.regexp.render).toHaveBeenCalledWith('group element');
|
||||
});
|
||||
|
||||
describe('when there are flags', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node.flags = ['example', 'flags'];
|
||||
});
|
||||
|
||||
it('renders a text element', function() {
|
||||
this.node._render();
|
||||
expect(this.node.container.text).toHaveBeenCalledWith(0, 0, 'Flags: example, flags');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when there are no flags', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node.flags = [];
|
||||
});
|
||||
|
||||
it('does not render a text element', function() {
|
||||
this.node._render();
|
||||
expect(this.node.container.text).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('positioning of elements', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.renderDeferred.resolve();
|
||||
|
||||
this.node.regexp.getBBox.and.returnValue({
|
||||
ax: 1,
|
||||
ay: 2,
|
||||
ax2: 3,
|
||||
x2: 4
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a path element to lead in and out of the regexp', function(done) {
|
||||
this.node._render()
|
||||
.then(() => {
|
||||
expect(this.node.container.path).toHaveBeenCalledWith('M1,2H0M3,2H14');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders circle elements before and after the regexp', function(done) {
|
||||
this.node._render()
|
||||
.then(() => {
|
||||
expect(this.node.container.circle).toHaveBeenCalledWith(0, 2, 5);
|
||||
expect(this.node.container.circle).toHaveBeenCalledWith(14, 2, 5);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are flags', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node.flags = ['example'];
|
||||
});
|
||||
|
||||
it('moves the regexp below the flag text', function(done) {
|
||||
this.node._render()
|
||||
.then(() => {
|
||||
expect(this.node.regexp.transform).toHaveBeenCalledWith(Snap.matrix()
|
||||
.translate(10, 20));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when there are no flags', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.node.flags = [];
|
||||
});
|
||||
|
||||
it('positions the regexp', function(done) {
|
||||
this.node._render()
|
||||
.then(() => {
|
||||
expect(this.node.regexp.transform).toHaveBeenCalledWith(Snap.matrix()
|
||||
.translate(10, 0));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,120 +0,0 @@
|
||||
import javascript from '../../../src/js/parser/javascript/parser.js';
|
||||
import Node from '../../../src/js/parser/javascript/node.js';
|
||||
import _ from 'lodash';
|
||||
import Snap from 'snapsvg';
|
||||
|
||||
describe('parser/javascript/subexp.js', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
Node.state = { groupCounter: 1 };
|
||||
});
|
||||
|
||||
_.forIn({
|
||||
'(test)': {
|
||||
regexp: jasmine.objectContaining({ textValue: 'test' })
|
||||
},
|
||||
'(?=test)': {
|
||||
regexp: jasmine.objectContaining({ textValue: 'test' })
|
||||
},
|
||||
'(?!test)': {
|
||||
regexp: jasmine.objectContaining({ textValue: 'test' })
|
||||
},
|
||||
'(?:test)': {
|
||||
regexp: jasmine.objectContaining({ textValue: 'test' }),
|
||||
proxy: jasmine.objectContaining({ textValue: 'test' })
|
||||
}
|
||||
}, (content, str) => {
|
||||
it(`parses "${str}" as a Subexp`, function() {
|
||||
var parser = new javascript.Parser(str);
|
||||
expect(parser.__consume__subexp()).toEqual(jasmine.objectContaining(content));
|
||||
});
|
||||
});
|
||||
|
||||
describe('_anchor property', function() {
|
||||
|
||||
it('applies the local transform matrix to the anchor from the regexp', function() {
|
||||
var node = new javascript.Parser('(test)').__consume__subexp();
|
||||
|
||||
node.regexp = {
|
||||
getBBox() {
|
||||
return {
|
||||
ax: 10,
|
||||
ax2: 15,
|
||||
ay: 20
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
spyOn(node, 'transform').and.returnValue({
|
||||
localMatrix: Snap.matrix().translate(3, 8)
|
||||
});
|
||||
|
||||
expect(node._anchor).toEqual({
|
||||
ax: 13,
|
||||
ax2: 18,
|
||||
ay: 28
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#_render', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.renderDeferred = this.testablePromise();
|
||||
|
||||
this.node = new javascript.Parser('(test)').__consume__subexp();
|
||||
this.node.regexp = jasmine.createSpyObj('regexp', ['render']);
|
||||
this.node.container = jasmine.createSpyObj('container', ['addClass', 'group']);
|
||||
spyOn(this.node, 'label').and.returnValue('example label')
|
||||
|
||||
this.node.regexp.render.and.returnValue(this.renderDeferred.promise);
|
||||
});
|
||||
|
||||
it('renders the regexp', function() {
|
||||
this.node._render();
|
||||
expect(this.node.regexp.render).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders a labeled box', function(done) {
|
||||
spyOn(this.node, 'renderLabeledBox');
|
||||
this.renderDeferred.resolve();
|
||||
this.node._render()
|
||||
.then(() => {
|
||||
expect(this.node.renderLabeledBox).toHaveBeenCalledWith('example label', this.node.regexp, { padding: 10 });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#label', function() {
|
||||
|
||||
_.forIn({
|
||||
'(test)': {
|
||||
label: 'group #1',
|
||||
groupCounter: 2
|
||||
},
|
||||
'(?=test)': {
|
||||
label: 'positive lookahead',
|
||||
groupCounter: 1
|
||||
},
|
||||
'(?!test)': {
|
||||
label: 'negative lookahead',
|
||||
groupCounter: 1
|
||||
},
|
||||
'(?:test)': {
|
||||
label: '',
|
||||
groupCounter: 1
|
||||
}
|
||||
}, (data, str) => {
|
||||
it(`generates the correct label for "${str}"`, function() {
|
||||
var node = new javascript.Parser(str).__consume__subexp();
|
||||
expect(node.label()).toEqual(data.label);
|
||||
expect(node.state.groupCounter).toEqual(data.groupCounter);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,173 +0,0 @@
|
||||
import Parser from '../../src/js/parser/javascript.js';
|
||||
import regexpParser from '../../src/js/parser/javascript/grammar.peg';
|
||||
import Snap from 'snapsvg';
|
||||
|
||||
describe('parser/javascript.js', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.container = document.createElement('div');
|
||||
this.parser = new Parser(this.container);
|
||||
});
|
||||
|
||||
describe('container property', function() {
|
||||
|
||||
it('sets the content of the element', function() {
|
||||
var element = document.createElement('div');
|
||||
this.parser.container = element;
|
||||
|
||||
expect(element.innerHTML).not.toEqual('');
|
||||
});
|
||||
|
||||
it('keeps the original content if the keepContent option is set', function() {
|
||||
var element = document.createElement('div');
|
||||
element.innerHTML = 'example content';
|
||||
|
||||
this.parser.options.keepContent = true;
|
||||
this.parser.container = element;
|
||||
|
||||
expect(element.innerHTML).toContain('example content');
|
||||
expect(element.innerHTML).not.toEqual('example content');
|
||||
});
|
||||
|
||||
it('adds the "svg-container" class', function() {
|
||||
spyOn(this.parser, '_addClass');
|
||||
this.parser.container = document.createElement('div');
|
||||
expect(this.parser._addClass).toHaveBeenCalledWith('svg-container');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#parse', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
spyOn(regexpParser, 'parse');
|
||||
});
|
||||
|
||||
it('adds the "loading" class', function() {
|
||||
spyOn(this.parser, '_addClass');
|
||||
this.parser.parse('example expression');
|
||||
expect(this.parser._addClass).toHaveBeenCalledWith('loading');
|
||||
});
|
||||
|
||||
it('parses the expression', function(done) {
|
||||
this.parser.parse('example expression')
|
||||
.then(() => {
|
||||
expect(regexpParser.parse).toHaveBeenCalledWith('example expression');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('replaces newlines with "\\n"', function(done) {
|
||||
this.parser.parse('multiline\nexpression')
|
||||
.then(() => {
|
||||
expect(regexpParser.parse).toHaveBeenCalledWith('multiline\\nexpression');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves the returned promise with the parser instance', function(done) {
|
||||
this.parser.parse('example expression')
|
||||
.then(result => {
|
||||
expect(result).toEqual(this.parser);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects the returned promise with the exception thrown', function(done) {
|
||||
regexpParser.parse.and.throwError('fail');
|
||||
this.parser.parse('(example')
|
||||
.then(null, result => {
|
||||
expect(result).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#render', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.renderPromise = this.testablePromise();
|
||||
this.parser.parsed = jasmine.createSpyObj('parsed', ['render']);
|
||||
this.parser.parsed.render.and.returnValue(this.renderPromise.promise);
|
||||
});
|
||||
|
||||
it('render the parsed expression', function() {
|
||||
this.parser.render();
|
||||
expect(this.parser.parsed.render).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when rendering is complete', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.result = jasmine.createSpyObj('result', ['getBBox', 'transform']);
|
||||
this.result.getBBox.and.returnValue({
|
||||
x: 4,
|
||||
y: 2,
|
||||
width: 42,
|
||||
height: 24
|
||||
});
|
||||
|
||||
this.renderPromise.resolve(this.result);
|
||||
});
|
||||
|
||||
it('positions the renderd expression', function(done) {
|
||||
this.parser.render()
|
||||
.then(() => {
|
||||
expect(this.result.transform).toHaveBeenCalledWith(Snap.matrix()
|
||||
.translate(6, 8));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the dimensions of the image', function(done) {
|
||||
this.parser.render()
|
||||
.then(() => {
|
||||
let svg = this.container.querySelector('svg');
|
||||
|
||||
expect(svg.getAttribute('width')).toEqual('62');
|
||||
expect(svg.getAttribute('height')).toEqual('44');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('removes the "loading" class', function(done) {
|
||||
spyOn(this.parser, '_removeClass');
|
||||
this.parser.render()
|
||||
.then(() => {
|
||||
expect(this.parser._removeClass).toHaveBeenCalledWith('loading');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('removes the progress element', function(done) {
|
||||
this.parser.render()
|
||||
.then(() => {
|
||||
expect(this.container.querySelector('.loading')).toBeNull();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#cancel', function() {
|
||||
|
||||
it('sets the cancelRender state to true', function() {
|
||||
this.parser.cancel();
|
||||
expect(this.parser.state.cancelRender).toEqual(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('warnings property', function() {
|
||||
|
||||
it('returns the content of the warnings state variable', function() {
|
||||
this.parser.state.warnings.push('example');
|
||||
expect(this.parser.warnings).toEqual(['example']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,569 +0,0 @@
|
||||
import util from '../src/js/util.js';
|
||||
import Regexper from '../src/js/regexper.js';
|
||||
import Parser from '../src/js/parser/javascript.js';
|
||||
import Snap from 'snapsvg';
|
||||
|
||||
describe('regexper.js', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.root = document.createElement('div');
|
||||
this.root.innerHTML = [
|
||||
'<form id="regexp-form" action="/">',
|
||||
'<input type="text" id="regexp-input">',
|
||||
'<ul class="example">',
|
||||
'<ul><a href="#" data-action="permalink"></a></ul>',
|
||||
'<ul><a href="#" data-action="download-svg"></a></ul>',
|
||||
'<ul><a href="#" data-action="download-png"></a></ul>',
|
||||
'</ul>',
|
||||
'</form>',
|
||||
'<div id="error"></div>',
|
||||
'<ul id="warnings"></ul>',
|
||||
'<div id="regexp-render"></div>'
|
||||
].join('');
|
||||
|
||||
this.regexper = new Regexper(this.root);
|
||||
spyOn(this.regexper, '_setHash');
|
||||
spyOn(this.regexper, '_getHash');
|
||||
});
|
||||
|
||||
describe('#keypressListener', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.event = util.customEvent('keypress');
|
||||
spyOn(this.event, 'preventDefault');
|
||||
spyOn(this.regexper.form, 'dispatchEvent');
|
||||
});
|
||||
|
||||
describe('when the shift key is not depressed', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.event.shiftKey = false;
|
||||
this.event.keyCode = 13;
|
||||
});
|
||||
|
||||
it('does not prevent the default action', function() {
|
||||
this.regexper.keypressListener(this.event);
|
||||
expect(this.event.returnValue).not.toEqual(false);
|
||||
expect(this.event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not trigger a submit event', function() {
|
||||
this.regexper.keypressListener(this.event);
|
||||
expect(this.regexper.form.dispatchEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when the keyCode is not 13 (Enter)', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.event.shiftKey = true;
|
||||
this.event.keyCode = 42;
|
||||
});
|
||||
|
||||
it('does not prevent the default action', function() {
|
||||
this.regexper.keypressListener(this.event);
|
||||
expect(this.event.returnValue).not.toEqual(false);
|
||||
expect(this.event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not trigger a submit event', function() {
|
||||
this.regexper.keypressListener(this.event);
|
||||
expect(this.regexper.form.dispatchEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when the shift key is depressed and the keyCode is 13 (Enter)', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.event.shiftKey = true;
|
||||
this.event.keyCode = 13;
|
||||
});
|
||||
|
||||
it('prevents the default action', function() {
|
||||
this.regexper.keypressListener(this.event);
|
||||
expect(this.event.returnValue).not.toEqual(true);
|
||||
expect(this.event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('triggers a submit event', function() {
|
||||
var event;
|
||||
|
||||
this.regexper.keypressListener(this.event);
|
||||
expect(this.regexper.form.dispatchEvent).toHaveBeenCalled();
|
||||
|
||||
event = this.regexper.form.dispatchEvent.calls.mostRecent().args[0];
|
||||
expect(event.type).toEqual('submit');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#documentKeypressListener', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.event = util.customEvent('keyup');
|
||||
this.regexper.running = jasmine.createSpyObj('parser', ['cancel']);
|
||||
});
|
||||
|
||||
describe('when the keyCode is not 27 (Escape)', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.event.keyCode = 42;
|
||||
});
|
||||
|
||||
it('does not cancel the parser', function() {
|
||||
this.regexper.documentKeypressListener(this.event);
|
||||
expect(this.regexper.running.cancel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when the keyCode is 27 (Escape)', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.event.keyCode = 27;
|
||||
});
|
||||
|
||||
it('cancels the parser', function() {
|
||||
this.regexper.documentKeypressListener(this.event);
|
||||
expect(this.regexper.running.cancel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#submitListener', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.event = util.customEvent('submit');
|
||||
spyOn(this.event, 'preventDefault');
|
||||
|
||||
this.regexper.field.value = 'example value';
|
||||
});
|
||||
|
||||
it('prevents the default action', function() {
|
||||
this.regexper.submitListener(this.event);
|
||||
expect(this.event.returnValue).not.toEqual(true);
|
||||
expect(this.event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets the location.hash', function() {
|
||||
this.regexper.submitListener(this.event);
|
||||
expect(this.regexper._setHash).toHaveBeenCalledWith('example value');
|
||||
});
|
||||
|
||||
describe('when setting location.hash fails', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.regexper._setHash.and.throwError('hash failure');
|
||||
});
|
||||
|
||||
it('disables the permalink', function() {
|
||||
this.regexper.submitListener(this.event);
|
||||
expect(this.regexper.permalinkEnabled).toEqual(false);
|
||||
});
|
||||
|
||||
it('shows the expression directly', function() {
|
||||
spyOn(this.regexper, 'showExpression');
|
||||
this.regexper.submitListener(this.event);
|
||||
expect(this.regexper.showExpression).toHaveBeenCalledWith('example value');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#hashchangeListener', function() {
|
||||
|
||||
describe('when the URL is invalid', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.regexper._getHash.and.returnValue(new Error('example error'));
|
||||
});
|
||||
|
||||
it('displays an error message', function() {
|
||||
this.regexper.hashchangeListener();
|
||||
expect(this.regexper.state).toEqual('has-error');
|
||||
expect(this.regexper.error.innerHTML).toEqual('Malformed expression in URL');
|
||||
});
|
||||
|
||||
it('tracks the event', function() {
|
||||
this.regexper.hashchangeListener();
|
||||
expect(util.track).toHaveBeenCalledWith('send', 'event', 'visualization', 'malformed URL');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when the URL is valid', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.regexper._getHash.and.returnValue('example hash value');
|
||||
});
|
||||
|
||||
it('enables the permalink', function() {
|
||||
this.regexper.hashchangeListener();
|
||||
expect(this.regexper.permalinkEnabled).toEqual(true);
|
||||
});
|
||||
|
||||
it('shows the expression from the hash', function() {
|
||||
spyOn(this.regexper, 'showExpression');
|
||||
this.regexper.hashchangeListener();
|
||||
expect(this.regexper.showExpression).toHaveBeenCalledWith('example hash value');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#bindListeners', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
spyOn(this.regexper, 'keypressListener');
|
||||
spyOn(this.regexper, 'submitListener');
|
||||
spyOn(this.regexper, 'documentKeypressListener');
|
||||
spyOn(this.regexper, 'hashchangeListener');
|
||||
});
|
||||
|
||||
it('binds #keypressListener to keypress on the text field', function() {
|
||||
spyOn(this.regexper.field, 'addEventListener');
|
||||
this.regexper.bindListeners();
|
||||
expect(this.regexper.field.addEventListener).toHaveBeenCalledWith('keypress', jasmine.any(Function));
|
||||
|
||||
this.regexper.field.addEventListener.calls.mostRecent().args[1]();
|
||||
expect(this.regexper.keypressListener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('binds #submitListener to submit on the form', function() {
|
||||
spyOn(this.regexper.form, 'addEventListener');
|
||||
this.regexper.bindListeners();
|
||||
expect(this.regexper.form.addEventListener).toHaveBeenCalledWith('submit', jasmine.any(Function));
|
||||
|
||||
this.regexper.form.addEventListener.calls.mostRecent().args[1]();
|
||||
expect(this.regexper.submitListener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('binds #documentKeypressListener to keyup on the root', function() {
|
||||
spyOn(this.regexper.root, 'addEventListener');
|
||||
this.regexper.bindListeners();
|
||||
expect(this.regexper.root.addEventListener).toHaveBeenCalledWith('keyup', jasmine.any(Function));
|
||||
|
||||
this.regexper.root.addEventListener.calls.mostRecent().args[1]();
|
||||
expect(this.regexper.documentKeypressListener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('binds #hashchangeListener to hashchange on the window', function() {
|
||||
spyOn(window, 'addEventListener');
|
||||
this.regexper.bindListeners();
|
||||
expect(window.addEventListener).toHaveBeenCalledWith('hashchange', jasmine.any(Function));
|
||||
|
||||
window.addEventListener.calls.mostRecent().args[1]();
|
||||
expect(this.regexper.hashchangeListener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#showExpression', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
spyOn(this.regexper, 'renderRegexp').and.returnValue(jasmine.createSpyObj('renderRegexp', ['catch']));
|
||||
});
|
||||
|
||||
it('sets the text field value', function() {
|
||||
this.regexper.showExpression('example expression');
|
||||
expect(this.regexper.field.value).toEqual('example expression');
|
||||
});
|
||||
|
||||
it('clears the state', function() {
|
||||
this.regexper.showExpression('');
|
||||
expect(this.regexper.state).toEqual('');
|
||||
});
|
||||
|
||||
describe('when the expression is not blank', function() {
|
||||
|
||||
it('renders the expression', function() {
|
||||
this.regexper.showExpression('example expression');
|
||||
expect(this.regexper.renderRegexp).toHaveBeenCalledWith('example expression');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#updateLinks', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
spyOn(this.regexper, 'buildBlobURL');
|
||||
this.regexper.svgContainer.innerHTML = '<div class="svg">example image</div>';
|
||||
});
|
||||
|
||||
it('builds the blob URL from the SVG image', function() {
|
||||
this.regexper.updateLinks();
|
||||
expect(this.regexper.buildBlobURL).toHaveBeenCalledWith('example image');
|
||||
});
|
||||
|
||||
describe('when blob URLs are supported', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.regexper.buildBlobURL.and.returnValue('http://example.com/blob');
|
||||
});
|
||||
|
||||
it('sets the download link href', function() {
|
||||
this.regexper.updateLinks();
|
||||
expect(this.regexper.downloadSvg.href).toEqual('http://example.com/blob');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when blob URLs are not supported', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.regexper.buildBlobURL.and.throwError('blob failure');
|
||||
});
|
||||
|
||||
it('hides the download link', function() {
|
||||
this.regexper.updateLinks();
|
||||
expect(this.regexper.links.className).toMatch(/\bexample\b/);
|
||||
expect(this.regexper.links.className).toMatch(/\bhide-download\b/);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when the permalink is enabled', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.regexper.permalinkEnabled = true;
|
||||
});
|
||||
|
||||
it('sets the permalink href', function() {
|
||||
this.regexper.updateLinks();
|
||||
expect(this.regexper.permalink.href).toEqual(location.toString());
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when the permalink is disabled', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.regexper.permalinkEnabled = false;
|
||||
});
|
||||
|
||||
it('hides the permalink', function() {
|
||||
this.regexper.updateLinks();
|
||||
expect(this.regexper.links.className).toMatch(/\bexample\b/);
|
||||
expect(this.regexper.links.className).toMatch(/\bhide-permalink\b/);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#displayWarnings', function() {
|
||||
|
||||
it('adds a list item for each warning', function() {
|
||||
spyOn(util, 'icon').and.returnValue('(icon-markup)');
|
||||
this.regexper.displayWarnings(['warning 1', 'warning 2']);
|
||||
expect(this.regexper.warnings.innerHTML).toEqual('<li class="inline-icon">(icon-markup)warning 1</li><li class="inline-icon">(icon-markup)warning 2</li>');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#renderRegexp', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.parsePromise = this.testablePromise();
|
||||
this.renderPromise = this.testablePromise();
|
||||
spyOn(Parser.prototype, 'parse').and.returnValue(this.parsePromise.promise);
|
||||
spyOn(Parser.prototype, 'render').and.returnValue(this.renderPromise.promise);
|
||||
spyOn(Parser.prototype, 'cancel');
|
||||
|
||||
spyOn(this.regexper, 'updateLinks');
|
||||
spyOn(this.regexper, 'displayWarnings');
|
||||
});
|
||||
|
||||
it('sets the state to "is-loading"', function() {
|
||||
this.regexper.renderRegexp('example expression');
|
||||
expect(this.regexper.state).toEqual('is-loading');
|
||||
});
|
||||
|
||||
it('tracks the beginning of the render', function() {
|
||||
this.regexper.renderRegexp('example expression');
|
||||
expect(util.track).toHaveBeenCalledWith('send', 'event', 'visualization', 'start');
|
||||
});
|
||||
|
||||
it('keeps a copy of the running property parser', function() {
|
||||
this.regexper.renderRegexp('example expression');
|
||||
expect(this.regexper.running).toBeTruthy();
|
||||
});
|
||||
|
||||
it('parses the expression', function() {
|
||||
this.regexper.renderRegexp('example expression');
|
||||
expect(this.regexper.running.parse).toHaveBeenCalledWith('example expression');
|
||||
});
|
||||
|
||||
describe('when parsing fails', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.parsePromise.reject(new Error('example parse error'));
|
||||
});
|
||||
|
||||
it('sets the state to be "has-error"', function(done) {
|
||||
this.regexper.renderRegexp('example expression')
|
||||
.then(() => {
|
||||
expect(this.regexper.state).toEqual('has-error');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays the error message', function(done) {
|
||||
this.regexper.renderRegexp('example expression')
|
||||
.then(() => {
|
||||
expect(this.regexper.error.innerHTML).toEqual('Error: example parse error');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('tracks the parse error', function(done) {
|
||||
this.regexper.renderRegexp('example expression')
|
||||
.then(() => {
|
||||
expect(util.track).toHaveBeenCalledWith('send', 'event', 'visualization', 'parse error');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when parsing succeeds', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.parser = new Parser(this.regexper.svgContainer);
|
||||
this.parsePromise.resolve(this.parser);
|
||||
this.renderPromise.resolve();
|
||||
});
|
||||
|
||||
it('renders the expression', function(done) {
|
||||
this.regexper.renderRegexp('example expression')
|
||||
.then(() => {
|
||||
expect(this.parser.render).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when rendering is complete', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.parser = new Parser(this.regexper.svgContainer);
|
||||
this.parsePromise.resolve(this.parser);
|
||||
this.renderPromise.resolve();
|
||||
});
|
||||
|
||||
it('sets the state to "has-results"', function(done) {
|
||||
this.regexper.renderRegexp('example expression')
|
||||
.then(() => {
|
||||
expect(this.regexper.state).toEqual('has-results');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('updates the links', function(done) {
|
||||
this.regexper.renderRegexp('example expression')
|
||||
.then(() => {
|
||||
expect(this.regexper.updateLinks).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays the warnings', function(done) {
|
||||
this.regexper.renderRegexp('example expression')
|
||||
.then(() => {
|
||||
expect(this.regexper.displayWarnings).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('tracks the complete render', function(done) {
|
||||
this.regexper.renderRegexp('example expression')
|
||||
.then(() => {
|
||||
expect(util.track).toHaveBeenCalledWith('send', 'event', 'visualization', 'complete');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the running property to false', function(done) {
|
||||
this.regexper.renderRegexp('example expression')
|
||||
.then(() => {
|
||||
expect(this.regexper.running).toBeFalsy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('tracks the total rendering time', function(done) {
|
||||
this.regexper.renderRegexp('example expression')
|
||||
.then(() => {
|
||||
expect(util.track).toHaveBeenCalledWith('send', 'timing', 'visualization', 'total time', jasmine.any(Number));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when the rendering is cancelled', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.parser = new Parser(this.regexper.svgContainer);
|
||||
this.parsePromise.resolve(this.parser);
|
||||
this.renderPromise.reject('Render cancelled');
|
||||
});
|
||||
|
||||
it('clears the state', function(done) {
|
||||
this.regexper.renderRegexp('example expression')
|
||||
.then(() => {
|
||||
expect(this.regexper.state).toEqual('');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('tracks the cancelled render', function(done) {
|
||||
this.regexper.renderRegexp('example expression')
|
||||
.then(() => {
|
||||
expect(util.track).toHaveBeenCalledWith('send', 'event', 'visualization', 'cancelled');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the running property to false', function(done) {
|
||||
this.regexper.renderRegexp('example expression')
|
||||
.then(() => {
|
||||
expect(this.regexper.running).toBeFalsy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when the rendering fails', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.parser = new Parser(this.regexper.svgContainer);
|
||||
this.parsePromise.resolve(this.parser);
|
||||
this.renderPromise.reject('example render failure');
|
||||
});
|
||||
|
||||
it('sets the running property to false', function(done) {
|
||||
this.regexper.renderRegexp('example expression')
|
||||
.then(fail, () => {
|
||||
expect(this.regexper.running).toBeFalsy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,33 +0,0 @@
|
||||
import util from '../src/js/util.js';
|
||||
|
||||
// Setup (and teardown) SVG container template
|
||||
beforeEach(function() {
|
||||
var template = document.createElement('script');
|
||||
template.setAttribute('type', 'text/html');
|
||||
template.setAttribute('id', 'svg-container-base');
|
||||
template.innerHTML = [
|
||||
'<div class="svg"><svg></svg></div>',
|
||||
'<div class="progress"><div></div></div>'
|
||||
].join('');
|
||||
document.body.appendChild(template);
|
||||
|
||||
this.testablePromise = function() {
|
||||
var result = {};
|
||||
|
||||
result.promise = new Promise((resolve, reject) => {
|
||||
result.resolve = resolve;
|
||||
result.reject = reject;
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
document.body.removeChild(document.body.querySelector('#svg-container-base'));
|
||||
});
|
||||
|
||||
// Spy on util.track to prevent unnecessary logging
|
||||
beforeEach(function() {
|
||||
spyOn(util, 'track');
|
||||
});
|
@ -1,2 +0,0 @@
|
||||
var testsContext = require.context(".", true, /_spec$/);
|
||||
testsContext.keys().forEach(testsContext);
|
@ -1,97 +0,0 @@
|
||||
import util from '../src/js/util.js';
|
||||
|
||||
describe('util.js', function() {
|
||||
|
||||
describe('customEvent', function() {
|
||||
|
||||
it('sets the event type', function() {
|
||||
var event = util.customEvent('example');
|
||||
expect(event.type).toEqual('example');
|
||||
});
|
||||
|
||||
it('sets the event detail', function() {
|
||||
var event = util.customEvent('example', 'detail');
|
||||
expect(event.detail).toEqual('detail');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('normalizeBBox', function() {
|
||||
|
||||
it('defaults the anchor keys to values from the bbox', function() {
|
||||
expect(util.normalizeBBox({
|
||||
x: 'bbox x',
|
||||
x2: 'bbox x2',
|
||||
cy: 'bbox cy',
|
||||
ay: 'bbox ay'
|
||||
})).toEqual({
|
||||
x: 'bbox x',
|
||||
x2: 'bbox x2',
|
||||
cy: 'bbox cy',
|
||||
ax: 'bbox x',
|
||||
ax2: 'bbox x2',
|
||||
ay: 'bbox ay'
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('spaceHorizontally', function() {
|
||||
|
||||
it('positions each item', function() {
|
||||
var svg = Snap(document.createElement('svg')),
|
||||
items = [
|
||||
svg.group(),
|
||||
svg.group(),
|
||||
svg.group()
|
||||
];
|
||||
|
||||
spyOn(items[0], 'getBBox').and.returnValue({ ay: 5, width: 10 });
|
||||
spyOn(items[1], 'getBBox').and.returnValue({ ay: 15, width: 30 });
|
||||
spyOn(items[2], 'getBBox').and.returnValue({ ay: 10, width: 20 });
|
||||
spyOn(items[0], 'transform').and.callThrough();
|
||||
spyOn(items[1], 'transform').and.callThrough();
|
||||
spyOn(items[2], 'transform').and.callThrough();
|
||||
|
||||
util.spaceHorizontally(items, { padding: 5 });
|
||||
|
||||
expect(items[0].transform).toHaveBeenCalledWith(Snap.matrix()
|
||||
.translate(0, 10));
|
||||
expect(items[1].transform).toHaveBeenCalledWith(Snap.matrix()
|
||||
.translate(15, 0));
|
||||
expect(items[2].transform).toHaveBeenCalledWith(Snap.matrix()
|
||||
.translate(50, 5));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('spaceVertically', function() {
|
||||
|
||||
it('positions each item', function() {
|
||||
var svg = Snap(document.createElement('svg')),
|
||||
items = [
|
||||
svg.group(),
|
||||
svg.group(),
|
||||
svg.group()
|
||||
];
|
||||
|
||||
spyOn(items[0], 'getBBox').and.returnValue({ cx: 5, height: 10 });
|
||||
spyOn(items[1], 'getBBox').and.returnValue({ cx: 15, height: 30 });
|
||||
spyOn(items[2], 'getBBox').and.returnValue({ cx: 10, height: 20 });
|
||||
spyOn(items[0], 'transform').and.callThrough();
|
||||
spyOn(items[1], 'transform').and.callThrough();
|
||||
spyOn(items[2], 'transform').and.callThrough();
|
||||
|
||||
util.spaceVertically(items, { padding: 5 });
|
||||
|
||||
expect(items[0].transform).toHaveBeenCalledWith(Snap.matrix()
|
||||
.translate(10, 0));
|
||||
expect(items[1].transform).toHaveBeenCalledWith(Snap.matrix()
|
||||
.translate(0, 15));
|
||||
expect(items[2].transform).toHaveBeenCalledWith(Snap.matrix()
|
||||
.translate(5, 50));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
17
src/404.hbs
17
src/404.hbs
@ -1,17 +0,0 @@
|
||||
---
|
||||
title: Page Not Found
|
||||
---
|
||||
{{#extend "layout"}}
|
||||
{{#content "body"}}
|
||||
<div class="error copy">
|
||||
<h1>404: Not Found</h1>
|
||||
|
||||
<blockquote>
|
||||
Some people, when confronted with a problem, think<br/>
|
||||
“I know, I'll use regular expressions.” Now they have two problems.
|
||||
</blockquote>
|
||||
|
||||
<p>Apparently, you have three problems…because the page you requested cannot be found.</p>
|
||||
</div>
|
||||
{{/content}}
|
||||
{{/extend}}
|
19
src/__mocks__/component-mock.js
Normal file
19
src/__mocks__/component-mock.js
Normal file
@ -0,0 +1,19 @@
|
||||
const React = require('react');
|
||||
|
||||
const buildMock = component => {
|
||||
const componentName = component.displayName || component.name || 'Component';
|
||||
const Mock = ({ children, ...props }) => (
|
||||
<span
|
||||
data-component={ componentName }
|
||||
data-props={ JSON.stringify(props, null, ' ') }>{ children }</span>
|
||||
);
|
||||
Mock.propTypes = component.propTypes;
|
||||
return Mock;
|
||||
};
|
||||
|
||||
module.exports = path => {
|
||||
const actual = jest.requireActual(path);
|
||||
return buildMock(actual.default || actual);
|
||||
};
|
||||
|
||||
module.exports.buildMock = buildMock;
|
14
src/__mocks__/gatsby.js
Normal file
14
src/__mocks__/gatsby.js
Normal file
@ -0,0 +1,14 @@
|
||||
const React = require('react');
|
||||
const gatsby = jest.requireActual('gatsby');
|
||||
|
||||
module.exports = {
|
||||
...gatsby,
|
||||
graphql: jest.fn().mockImplementation(([query]) => query),
|
||||
Link: jest.fn().mockImplementation(({ to, ...rest }) =>
|
||||
React.createElement('a', {
|
||||
...rest,
|
||||
href: to
|
||||
})
|
||||
),
|
||||
StaticQuery: jest.fn()
|
||||
};
|
15
src/__mocks__/i18n.js
Normal file
15
src/__mocks__/i18n.js
Normal file
@ -0,0 +1,15 @@
|
||||
const i18n = jest.requireActual('i18n');
|
||||
|
||||
// Load empty resource bundle to reduce logging output
|
||||
i18n.default.addResourceBundle('dev', 'translation', {});
|
||||
i18n.default.addResourceBundle('en', 'translation', {});
|
||||
i18n.default.addResourceBundle('other', 'translation', {});
|
||||
|
||||
module.exports = {
|
||||
...i18n,
|
||||
locales: [
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'other', name: 'Other' }
|
||||
],
|
||||
mockT: str => `TRANSLATE(${ str })`
|
||||
};
|
8
src/__mocks__/react-i18next.js
vendored
Normal file
8
src/__mocks__/react-i18next.js
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
const reactI18next = jest.requireActual('react-i18next');
|
||||
const i18n = require('i18n');
|
||||
|
||||
module.exports = {
|
||||
...reactI18next,
|
||||
Trans: require('__mocks__/component-mock').buildMock(reactI18next.Trans),
|
||||
useTranslation: () => ({ i18n, t: i18n.mockT })
|
||||
};
|
87
src/__snapshots__/layout.test.js.snap
Normal file
87
src/__snapshots__/layout.test.js.snap
Normal file
@ -0,0 +1,87 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`layout running against a node with a basic bounding box 1`] = `
|
||||
Object {
|
||||
"box": Object {
|
||||
"axisX1": 0,
|
||||
"axisX2": 20,
|
||||
"axisY": 5,
|
||||
"height": 10,
|
||||
"width": 20,
|
||||
},
|
||||
"props": Object {},
|
||||
"type": "Example",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`layout running against a node with a complete bounding box 1`] = `
|
||||
Object {
|
||||
"box": Object {
|
||||
"axisX1": 5,
|
||||
"axisX2": 15,
|
||||
"axisY": 2,
|
||||
"height": 10,
|
||||
"width": 20,
|
||||
},
|
||||
"props": Object {},
|
||||
"type": "Example",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`layout running against a node with props 1`] = `
|
||||
Object {
|
||||
"box": Object {
|
||||
"axisX1": 0,
|
||||
"axisX2": 0,
|
||||
"axisY": 0,
|
||||
"height": 0,
|
||||
"width": 0,
|
||||
},
|
||||
"props": Object {
|
||||
"property": "example",
|
||||
},
|
||||
"type": "Example",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`layout running against a simple node 1`] = `
|
||||
Object {
|
||||
"box": Object {
|
||||
"axisX1": 0,
|
||||
"axisX2": 0,
|
||||
"axisY": 0,
|
||||
"height": 0,
|
||||
"width": 0,
|
||||
},
|
||||
"props": Object {},
|
||||
"type": "Example",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`layout running layout on children 1`] = `
|
||||
Object {
|
||||
"box": Object {
|
||||
"axisX1": 0,
|
||||
"axisX2": 0,
|
||||
"axisY": 0,
|
||||
"height": 0,
|
||||
"width": 0,
|
||||
},
|
||||
"children": Array [
|
||||
Object {
|
||||
"box": Object {
|
||||
"axisX1": 0,
|
||||
"axisX2": 20,
|
||||
"axisY": 5,
|
||||
"height": 10,
|
||||
"width": 20,
|
||||
},
|
||||
"props": Object {},
|
||||
"type": "Other",
|
||||
},
|
||||
"string example",
|
||||
],
|
||||
"props": Object {},
|
||||
"type": "Example",
|
||||
}
|
||||
`;
|
@ -1,17 +0,0 @@
|
||||
---
|
||||
title: Changelog
|
||||
---
|
||||
{{#extend "layout"}}
|
||||
{{#content "body"}}
|
||||
<div class="copy changelog">
|
||||
<dl>
|
||||
{{#each changelog}}
|
||||
<dt>{{label}}</dt>
|
||||
{{#each changes}}
|
||||
<dd>{{{this}}}</dd>
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
</dl>
|
||||
</div>
|
||||
{{/content}}
|
||||
{{/extend}}
|
255
src/components/App/__snapshots__/test.js.snap
Normal file
255
src/components/App/__snapshots__/test.js.snap
Normal file
@ -0,0 +1,255 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App removing rendered expression 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Form"
|
||||
data-props="{
|
||||
\\"syntax\\": \\"js\\",
|
||||
\\"expr\\": \\"test expression\\",
|
||||
\\"syntaxList\\": [
|
||||
{
|
||||
\\"id\\": \\"testJS\\",
|
||||
\\"label\\": \\"Testing JS\\"
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"other\\",
|
||||
\\"label\\": \\"Other\\"
|
||||
}
|
||||
]
|
||||
}"
|
||||
>
|
||||
<span
|
||||
data-component="FormActions"
|
||||
data-props="{}"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
data-component="Render"
|
||||
data-props="{
|
||||
\\"data\\": \\"LAYOUT(PARSED(test expression))\\"
|
||||
}"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`App removing rendered expression 2`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Form"
|
||||
data-props="{
|
||||
\\"syntax\\": \\"js\\",
|
||||
\\"expr\\": \\"\\",
|
||||
\\"syntaxList\\": [
|
||||
{
|
||||
\\"id\\": \\"testJS\\",
|
||||
\\"label\\": \\"Testing JS\\"
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"other\\",
|
||||
\\"label\\": \\"Other\\"
|
||||
}
|
||||
]
|
||||
}"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`App rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Form"
|
||||
data-props="{
|
||||
\\"syntax\\": \\"js\\",
|
||||
\\"expr\\": \\"\\",
|
||||
\\"syntaxList\\": [
|
||||
{
|
||||
\\"id\\": \\"testJS\\",
|
||||
\\"label\\": \\"Testing JS\\"
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"other\\",
|
||||
\\"label\\": \\"Other\\"
|
||||
}
|
||||
]
|
||||
}"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`App rendering an expression 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Form"
|
||||
data-props="{
|
||||
\\"syntax\\": \\"js\\",
|
||||
\\"expr\\": \\"\\",
|
||||
\\"syntaxList\\": [
|
||||
{
|
||||
\\"id\\": \\"testJS\\",
|
||||
\\"label\\": \\"Testing JS\\"
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"other\\",
|
||||
\\"label\\": \\"Other\\"
|
||||
}
|
||||
]
|
||||
}"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`App rendering an expression 2`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Form"
|
||||
data-props="{
|
||||
\\"syntax\\": \\"js\\",
|
||||
\\"expr\\": \\"test expression\\",
|
||||
\\"syntaxList\\": [
|
||||
{
|
||||
\\"id\\": \\"testJS\\",
|
||||
\\"label\\": \\"Testing JS\\"
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"other\\",
|
||||
\\"label\\": \\"Other\\"
|
||||
}
|
||||
]
|
||||
}"
|
||||
/>
|
||||
<span
|
||||
data-component="Loader"
|
||||
data-props="{}"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`App rendering an expression 3`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Form"
|
||||
data-props="{
|
||||
\\"syntax\\": \\"js\\",
|
||||
\\"expr\\": \\"test expression\\",
|
||||
\\"syntaxList\\": [
|
||||
{
|
||||
\\"id\\": \\"testJS\\",
|
||||
\\"label\\": \\"Testing JS\\"
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"other\\",
|
||||
\\"label\\": \\"Other\\"
|
||||
}
|
||||
]
|
||||
}"
|
||||
>
|
||||
<span
|
||||
data-component="FormActions"
|
||||
data-props="{}"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
data-component="Render"
|
||||
data-props="{
|
||||
\\"data\\": \\"LAYOUT(PARSED(test expression))\\"
|
||||
}"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`App rendering with an invalid syntax 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Form"
|
||||
data-props="{
|
||||
\\"syntax\\": \\"invalid\\",
|
||||
\\"expr\\": \\"\\",
|
||||
\\"syntaxList\\": [
|
||||
{
|
||||
\\"id\\": \\"testJS\\",
|
||||
\\"label\\": \\"Testing JS\\"
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"other\\",
|
||||
\\"label\\": \\"Other\\"
|
||||
}
|
||||
]
|
||||
}"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`App rendering with an invalid syntax 2`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Form"
|
||||
data-props="{
|
||||
\\"syntax\\": \\"invalid\\",
|
||||
\\"expr\\": \\"test expression\\",
|
||||
\\"syntaxList\\": [
|
||||
{
|
||||
\\"id\\": \\"testJS\\",
|
||||
\\"label\\": \\"Testing JS\\"
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"other\\",
|
||||
\\"label\\": \\"Other\\"
|
||||
}
|
||||
]
|
||||
}"
|
||||
/>
|
||||
<span
|
||||
data-component="Loader"
|
||||
data-props="{}"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`App rendering with an invalid syntax 3`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Form"
|
||||
data-props="{
|
||||
\\"syntax\\": \\"invalid\\",
|
||||
\\"expr\\": \\"test expression\\",
|
||||
\\"syntaxList\\": [
|
||||
{
|
||||
\\"id\\": \\"testJS\\",
|
||||
\\"label\\": \\"Testing JS\\"
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"other\\",
|
||||
\\"label\\": \\"Other\\"
|
||||
}
|
||||
]
|
||||
}"
|
||||
/>
|
||||
<span
|
||||
data-component="Message"
|
||||
data-props="{
|
||||
\\"type\\": \\"error\\",
|
||||
\\"heading\\": \\"TRANSLATE(Render Failure)\\"
|
||||
}"
|
||||
>
|
||||
<p>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
An error occurred while rendering the regular expression.
|
||||
</span>
|
||||
</p>
|
||||
<a
|
||||
href="#retry"
|
||||
>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Retry
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
165
src/components/App/index.js
Normal file
165
src/components/App/index.js
Normal file
@ -0,0 +1,165 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation, Trans } from 'react-i18next';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import URLSearchParams from '@ungap/url-search-params';
|
||||
|
||||
import Form from 'components/Form';
|
||||
import FormActions from 'components/FormActions';
|
||||
import Loader from 'components/Loader';
|
||||
import Message from 'components/Message';
|
||||
|
||||
class App extends React.PureComponent {
|
||||
static propTypes = {
|
||||
syntax: PropTypes.string.isRequired,
|
||||
expr: PropTypes.string.isRequired,
|
||||
permalinkUrl: PropTypes.string,
|
||||
syntaxList: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string
|
||||
})),
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
state = {
|
||||
loading: false,
|
||||
loadingError: null,
|
||||
render: {}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.expr) {
|
||||
this.handleRender();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { syntax, expr } = this.props;
|
||||
|
||||
if (syntax !== prevProps.syntax || expr !== prevProps.expr) {
|
||||
this.handleRender();
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit = ({ syntax, expr }) => {
|
||||
if (expr) {
|
||||
document.location.hash = new URLSearchParams({
|
||||
syntax,
|
||||
expr
|
||||
}).toString();
|
||||
}
|
||||
}
|
||||
|
||||
handleRender = async () => {
|
||||
const { syntax, expr } = this.props;
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
loadingError: null,
|
||||
render: {}
|
||||
});
|
||||
|
||||
if (!expr) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
|
||||
try {
|
||||
const syntaxModule = await import(
|
||||
/* webpackChunkName: "render-[index]" */
|
||||
`syntax/${ syntax }`
|
||||
);
|
||||
|
||||
const exprData = syntaxModule.layout(syntaxModule.parse(expr));
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
render: {
|
||||
syntax,
|
||||
exprData,
|
||||
Component: syntaxModule.Render
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
Sentry.withScope(scope => {
|
||||
scope.setExtra('syntax', syntax);
|
||||
Sentry.captureException(e);
|
||||
});
|
||||
this.setState({
|
||||
loading: false,
|
||||
loadingError: e
|
||||
});
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
handleRetry = event => {
|
||||
event.preventDefault();
|
||||
this.handleRender();
|
||||
}
|
||||
|
||||
handleSvg = imageDetails => this.setState({ imageDetails });
|
||||
|
||||
render() {
|
||||
const {
|
||||
syntax,
|
||||
expr,
|
||||
permalinkUrl,
|
||||
syntaxList,
|
||||
t
|
||||
} = this.props;
|
||||
const {
|
||||
loading,
|
||||
loadingError,
|
||||
imageDetails,
|
||||
render: {
|
||||
syntax: renderSyntax,
|
||||
exprData,
|
||||
Component
|
||||
}
|
||||
} = this.state;
|
||||
|
||||
|
||||
const formProps = {
|
||||
onSubmit: this.handleSubmit,
|
||||
syntax,
|
||||
expr,
|
||||
syntaxList
|
||||
};
|
||||
const actionProps = {
|
||||
imageDetails,
|
||||
permalinkUrl
|
||||
};
|
||||
const renderProps = {
|
||||
onRender: this.handleSvg,
|
||||
data: exprData
|
||||
};
|
||||
|
||||
const doRender = renderSyntax === syntax;
|
||||
|
||||
return <>
|
||||
<Form { ...formProps }>
|
||||
{ doRender && <FormActions { ...actionProps } /> }
|
||||
</Form>
|
||||
|
||||
{ loading && <Loader /> }
|
||||
|
||||
{ loadingError && <Message type="error" heading={ t('Render Failure') }>
|
||||
<p><Trans>
|
||||
An error occurred while rendering the regular expression.
|
||||
</Trans></p>
|
||||
<a href="#retry" onClick={ this.handleRetry }><Trans>Retry</Trans></a>
|
||||
</Message> }
|
||||
|
||||
{ doRender && <Component { ...renderProps } /> }
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
export { App };
|
||||
export default withTranslation()(App);
|
90
src/components/App/test.js
Normal file
90
src/components/App/test.js
Normal file
@ -0,0 +1,90 @@
|
||||
jest.mock('components/Form', () =>
|
||||
require('__mocks__/component-mock')('components/Form'));
|
||||
jest.mock('components/FormActions', () =>
|
||||
require('__mocks__/component-mock')('components/FormActions'));
|
||||
jest.mock('components/Loader', () =>
|
||||
require('__mocks__/component-mock')('components/Loader'));
|
||||
jest.mock('components/Message', () =>
|
||||
require('__mocks__/component-mock')('components/Message'));
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'react-testing-library';
|
||||
|
||||
import { mockT } from 'i18n';
|
||||
import { App } from 'components/App';
|
||||
|
||||
jest.mock('syntax/js', () => ({
|
||||
parse: expr => `PARSED(${ expr })`,
|
||||
layout: parsed => `LAYOUT(${ parsed })`,
|
||||
Render: require('__mocks__/component-mock').buildMock(function Render() {})
|
||||
}));
|
||||
|
||||
const syntaxList = [
|
||||
{ id: 'testJS', label: 'Testing JS' },
|
||||
{ id: 'other', label: 'Other' }
|
||||
];
|
||||
const commonProps = { syntaxList, t: mockT };
|
||||
|
||||
describe('App', () => {
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<App expr="" syntax="js" { ...commonProps } />
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('rendering an expression', async () => {
|
||||
const { asFragment, rerender } = render(
|
||||
<App expr="" syntax="js" { ...commonProps } />
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
rerender(
|
||||
<App expr="test expression" syntax="js" { ...commonProps } />
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
// Give a beat for module to load
|
||||
await new Promise(resolve => setTimeout(resolve));
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('rendering with an invalid syntax', async () => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const { asFragment, rerender } = render(
|
||||
<App expr="" syntax="invalid" { ...commonProps } />
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
rerender(
|
||||
<App expr="test expression" syntax="invalid" { ...commonProps } />
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
// Give a beat for module to load
|
||||
await new Promise(resolve => setTimeout(resolve));
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('removing rendered expression', async () => {
|
||||
const { asFragment, rerender } = render(
|
||||
<App expr="test expression" syntax="js" { ...commonProps } />
|
||||
);
|
||||
|
||||
// Give a beat for module to load
|
||||
await new Promise(resolve => setTimeout(resolve));
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
rerender(
|
||||
<App expr="" syntax="js" { ...commonProps } />
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
50
src/components/Footer/__snapshots__/test.js.snap
Normal file
50
src/components/Footer/__snapshots__/test.js.snap
Normal file
@ -0,0 +1,50 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Footer rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<footer
|
||||
class="footer"
|
||||
>
|
||||
<ul
|
||||
class="list"
|
||||
>
|
||||
<li>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Created by
|
||||
<a
|
||||
href="mailto:jeff.avallone@gmail.com"
|
||||
>
|
||||
Jeff Avallone
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Generated images licensed:
|
||||
<a
|
||||
href="http://creativecommons.org/licenses/by/3.0/"
|
||||
rel="license external noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
alt="TRANSLATE(Creative Commons CC-BY-3.0 License)"
|
||||
src="cc-by.svg"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
class="buildId"
|
||||
>
|
||||
abc-123
|
||||
</div>
|
||||
</footer>
|
||||
</DocumentFragment>
|
||||
`;
|
121
src/components/Footer/cc-by.svg
Normal file
121
src/components/Footer/cc-by.svg
Normal file
@ -0,0 +1,121 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://web.resource.org/cc/"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="80"
|
||||
height="15"
|
||||
id="svg2279"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="0.45+devel"
|
||||
version="1.0"
|
||||
sodipodi:docname="by.svg"
|
||||
inkscape:output_extension="org.inkscape.output.svg.inkscape">
|
||||
<defs
|
||||
id="defs2281">
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3442">
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.92243534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect3444"
|
||||
width="20.614058"
|
||||
height="12.483703"
|
||||
x="171.99832"
|
||||
y="239.1203" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#999999"
|
||||
borderopacity="1"
|
||||
gridtolerance="10000"
|
||||
guidetolerance="10"
|
||||
objecttolerance="10"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="10.5125"
|
||||
inkscape:cx="40"
|
||||
inkscape:cy="7.5"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
width="80px"
|
||||
height="15px"
|
||||
showborder="true"
|
||||
inkscape:showpageshadow="false"
|
||||
inkscape:window-width="935"
|
||||
inkscape:window-height="624"
|
||||
inkscape:window-x="50"
|
||||
inkscape:window-y="160" />
|
||||
<metadata
|
||||
id="metadata2284">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="BY"
|
||||
transform="matrix(0.9875019,0,0,0.9333518,-323.90064,-271.87688)">
|
||||
<g
|
||||
transform="translate(158,54)"
|
||||
id="g3693">
|
||||
<rect
|
||||
y="237.86218"
|
||||
x="170.5"
|
||||
height="15"
|
||||
width="80"
|
||||
id="rect3695"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.04161763;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.92243534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect3697"
|
||||
width="77"
|
||||
height="12"
|
||||
x="172"
|
||||
y="239.36218" />
|
||||
<path
|
||||
sodipodi:nodetypes="cccscc"
|
||||
id="path3699"
|
||||
d="M 171.99996,239.37505 L 171.99996,251.37505 L 192.33474,251.37505 C 193.64339,249.62474 194.52652,247.59057 194.52652,245.37505 C 194.52652,243.17431 193.65859,241.1179 192.36599,239.37505 L 171.99996,239.37505 z"
|
||||
style="fill:#abb1aa;fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.46913578" />
|
||||
<g
|
||||
clip-path="url(#clipPath3442)"
|
||||
transform="matrix(0.9612533,0,0,0.9612533,6.8341566,9.5069994)"
|
||||
id="g3701">
|
||||
<path
|
||||
style="opacity:1;fill:#ffffff"
|
||||
d="M 190.06417,245.36206 C 190.06667,249.25405 186.91326,252.41072 183.02153,252.41323 C 179.12979,252.41572 175.97262,249.26256 175.97036,245.3706 C 175.97036,245.36783 175.97036,245.36507 175.97036,245.36206 C 175.9681,241.47007 179.12126,238.3134 183.013,238.31113 C 186.90524,238.30864 190.06191,241.46181 190.06417,245.3538 C 190.06417,245.35628 190.06417,245.35929 190.06417,245.36206 z"
|
||||
rx="22.939548"
|
||||
type="arc"
|
||||
cy="264.3577"
|
||||
ry="22.939548"
|
||||
cx="296.35416"
|
||||
id="path3703" />
|
||||
<path
|
||||
style="opacity:1"
|
||||
id="path3705"
|
||||
d="M 188.74576,239.62226 C 190.30843,241.18492 191.08988,243.09869 191.08988,245.36206 C 191.08988,247.62592 190.32197,249.51913 188.78615,251.04165 C 187.15627,252.64521 185.22995,253.44672 183.00722,253.44672 C 180.81132,253.44672 178.91837,252.65172 177.32887,251.06174 C 175.73912,249.47198 174.94436,247.57226 174.94436,245.36206 C 174.94436,243.15235 175.73912,241.23908 177.32887,239.62226 C 178.87799,238.0591 180.77094,237.27764 183.00722,237.27764 C 185.2706,237.27764 187.18312,238.05909 188.74576,239.62226 z M 178.38093,240.67355 C 177.05978,242.008 176.39945,243.57116 176.39945,245.36429 C 176.39945,247.15694 177.05326,248.70682 178.36062,250.01393 C 179.66822,251.32153 181.22487,251.97509 183.03105,251.97509 C 184.83724,251.97509 186.40716,251.31502 187.74161,249.99412 C 189.0086,248.76725 189.64234,247.22467 189.64234,245.36429 C 189.64234,243.51799 188.99831,241.95084 187.71101,240.66354 C 186.42396,239.37649 184.86406,238.7327 183.03105,238.7327 C 181.19804,238.73271 179.64767,239.37975 178.38093,240.67355 z M 181.85761,244.57559 C 181.65573,244.13545 181.35354,243.91525 180.95051,243.91525 C 180.23802,243.91525 179.8819,244.39501 179.8819,245.35404 C 179.8819,246.31328 180.23802,246.79255 180.95051,246.79255 C 181.421,246.79255 181.75705,246.55908 181.95869,246.09111 L 182.94629,246.61701 C 182.47555,247.45339 181.76934,247.87168 180.82763,247.87168 C 180.10136,247.87168 179.51953,247.64899 179.08265,247.20409 C 178.64502,246.7587 178.42684,246.14477 178.42684,245.36206 C 178.42684,244.59313 178.65204,243.98271 179.10271,243.53056 C 179.55338,243.07838 180.11463,242.8524 180.7875,242.8524 C 181.78288,242.8524 182.49561,243.24465 182.92647,244.02835 L 181.85761,244.57559 z M 186.50398,244.57559 C 186.30184,244.13545 186.00567,243.91525 185.61517,243.91525 C 184.88839,243.91525 184.52474,244.39501 184.52474,245.35404 C 184.52474,246.31328 184.88839,246.79255 185.61517,246.79255 C 186.08642,246.79255 186.41644,246.55908 186.6048,246.09111 L 187.61447,246.61701 C 187.14448,247.45339 186.43926,247.87168 185.49931,247.87168 C 184.77403,247.87168 184.19346,247.64899 183.75683,247.20409 C 183.32096,246.7587 183.10254,246.14477 183.10254,245.36206 C 183.10254,244.59313 183.32422,243.98271 183.76737,243.53056 C 184.21026,243.07838 184.77404,242.8524 185.4592,242.8524 C 186.45282,242.8524 187.16455,243.24465 187.5939,244.02835 L 186.50398,244.57559 z" />
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
id="text3707"
|
||||
d="M 357.4197,298.68502 C 357.66518,298.68503 357.85131,298.63145 357.9781,298.52427 C 358.10488,298.41711 358.16827,298.25904 358.16828,298.05007 C 358.16827,297.84377 358.10488,297.68704 357.9781,297.57987 C 357.85131,297.47003 357.66518,297.41511 357.4197,297.4151 L 356.55784,297.4151 L 356.55784,298.68502 L 357.4197,298.68502 M 357.4723,301.30928 C 357.78522,301.30928 358.0199,301.24363 358.17637,301.11235 C 358.33552,300.98108 358.4151,300.78282 358.4151,300.51758 C 358.4151,300.2577 358.33686,300.06346 358.18041,299.93486 C 358.02396,299.80358 357.78792,299.73795 357.4723,299.73794 L 356.55784,299.73794 L 356.55784,301.30928 L 357.4723,301.30928 M 358.92089,299.15121 C 359.25538,299.24766 359.51434,299.42582 359.69779,299.6857 C 359.88121,299.94558 359.97293,300.26439 359.97294,300.64216 C 359.97293,301.22086 359.776,301.6522 359.38217,301.9362 C 358.98833,302.22019 358.38947,302.36218 357.5856,302.36218 L 355.00001,302.36218 L 355.00001,296.36218 L 357.33878,296.36218 C 358.17771,296.36219 358.78466,296.48811 359.15962,296.73995 C 359.53727,296.9918 359.7261,297.39501 359.7261,297.94959 C 359.7261,298.24163 359.65732,298.49079 359.51975,298.69708 C 359.38217,298.9007 359.18255,299.05208 358.92089,299.15121 M 359.83746,296.36218 L 361.54096,296.36218 L 362.91671,298.50016 L 364.29245,296.36218 L 366,296.36218 L 363.69764,299.83439 L 363.69764,302.36218 L 362.13982,302.36218 L 362.13982,299.83439 L 359.83746,296.36218"
|
||||
style="font-size:8.25858784px;font-style:normal;font-weight:bold;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:'Bitstream Vera Sans'" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.1 KiB |
38
src/components/Footer/index.js
Normal file
38
src/components/Footer/index.js
Normal file
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import ccLogo from './cc-by.svg';
|
||||
|
||||
import style from './style.module.css';
|
||||
|
||||
export const Footer = ({ buildId }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <footer className={ style.footer }>
|
||||
<ul className={ style.list }>
|
||||
<li>
|
||||
<Trans>Created by <a
|
||||
href="mailto:jeff.avallone@gmail.com">Jeff Avallone</a></Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>Generated images licensed: <a
|
||||
href="http://creativecommons.org/licenses/by/3.0/"
|
||||
rel="license external noopener noreferrer"
|
||||
target="_blank">
|
||||
<img src={ ccLogo }
|
||||
alt={ t('Creative Commons CC-BY-3.0 License') } />
|
||||
</a></Trans>
|
||||
</li>
|
||||
</ul>
|
||||
<div className={ style.buildId }>
|
||||
{ buildId }
|
||||
</div>
|
||||
</footer>;
|
||||
};
|
||||
|
||||
Footer.propTypes = {
|
||||
buildId: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default Footer;
|
26
src/components/Footer/style.module.css
Normal file
26
src/components/Footer/style.module.css
Normal file
@ -0,0 +1,26 @@
|
||||
@import url('../../globals.module.css');
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin: var(--spacing-margin) 0;
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& img {
|
||||
vertical-align: text-top;
|
||||
width: 80px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
composes: inline-list with-separator-left;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.buildId {
|
||||
color: color(var(--color-brown) blend(var(--color-tan) 25%));
|
||||
}
|
13
src/components/Footer/test.js
Normal file
13
src/components/Footer/test.js
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-testing-library';
|
||||
|
||||
import Footer from 'components/Footer';
|
||||
|
||||
describe('Footer', () => {
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<Footer buildId="abc-123" />
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
54
src/components/Form/__snapshots__/test.js.snap
Normal file
54
src/components/Form/__snapshots__/test.js.snap
Normal file
@ -0,0 +1,54 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Form rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="form"
|
||||
data-requires-js="true"
|
||||
>
|
||||
<form
|
||||
data-testid="form"
|
||||
>
|
||||
<textarea
|
||||
data-testid="expr-input"
|
||||
name="expr"
|
||||
placeholder="TRANSLATE(Enter regular expression to display)"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Display
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
data-testid="syntax-select"
|
||||
name="syntax"
|
||||
>
|
||||
<option
|
||||
value="testJS"
|
||||
>
|
||||
TRANSLATE(Testing JS)
|
||||
</option>
|
||||
<option
|
||||
value="other"
|
||||
>
|
||||
TRANSLATE(Other)
|
||||
</option>
|
||||
</select>
|
||||
<span
|
||||
data-component="ChevronsDown"
|
||||
data-props="{}"
|
||||
/>
|
||||
</div>
|
||||
Actions
|
||||
</form>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
73
src/components/Form/index.js
Normal file
73
src/components/Form/index.js
Normal file
@ -0,0 +1,73 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import ExpandIcon from 'react-feather/dist/icons/chevrons-down';
|
||||
|
||||
import style from './style.module.css';
|
||||
|
||||
const Form = ({ syntaxList, children, onSubmit, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
const [ expr, exprUpdate ] = useState(props.expr);
|
||||
const [ syntax, syntaxUpdate ] = useState(props.syntax);
|
||||
|
||||
const handleExprChange = useCallback(event => {
|
||||
exprUpdate(event.target.value);
|
||||
}, [exprUpdate]);
|
||||
const handleSyntaxChange = useCallback(event => {
|
||||
syntaxUpdate(event.target.value);
|
||||
}, [syntaxUpdate]);
|
||||
const handleSubmit = useCallback(event => {
|
||||
event.preventDefault();
|
||||
|
||||
onSubmit({ expr, syntax });
|
||||
}, [expr, syntax, onSubmit]);
|
||||
const handleKeyPress = useCallback(event => {
|
||||
if (event.charCode === 13 && event.shiftKey) {
|
||||
handleSubmit(event);
|
||||
}
|
||||
}, [handleSubmit]);
|
||||
|
||||
return <div className={ style.form } data-requires-js>
|
||||
<form data-testid="form" onSubmit={ handleSubmit }>
|
||||
<textarea
|
||||
data-testid="expr-input"
|
||||
name="expr"
|
||||
value={ expr }
|
||||
onKeyPress={ handleKeyPress }
|
||||
onChange={ handleExprChange }
|
||||
autoFocus
|
||||
placeholder={ t('Enter regular expression to display') }></textarea>
|
||||
<button type="submit"><Trans>Display</Trans></button>
|
||||
<div className={ style.select }>
|
||||
<select
|
||||
data-testid="syntax-select"
|
||||
name="syntax"
|
||||
value={ syntax }
|
||||
onChange={ handleSyntaxChange } >
|
||||
{ syntaxList.map(({ id, label }) => (
|
||||
<option value={ id } key={ id }>{ t(label) }</option>
|
||||
)) }
|
||||
</select>
|
||||
<ExpandIcon />
|
||||
</div>
|
||||
{ children }
|
||||
</form>
|
||||
</div>;
|
||||
};
|
||||
|
||||
Form.propTypes = {
|
||||
expr: PropTypes.string,
|
||||
syntax: PropTypes.string,
|
||||
syntaxList: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string
|
||||
})),
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node
|
||||
])
|
||||
};
|
||||
|
||||
export default Form;
|
49
src/components/Form/style.module.css
Normal file
49
src/components/Form/style.module.css
Normal file
@ -0,0 +1,49 @@
|
||||
@import url('../../globals.module.css');
|
||||
|
||||
:root {
|
||||
--control-gradient: var(--color-green) var(--gradient-green);
|
||||
--select-height: 2.8rem;
|
||||
--select-width: 12rem;
|
||||
--entry-line-height: 1.5em;
|
||||
}
|
||||
|
||||
.form {
|
||||
margin: var(--spacing-margin) 0;
|
||||
overflow: hidden; /* Keep floated content in the box */
|
||||
|
||||
& textarea {
|
||||
display: block;
|
||||
font-size: inherit;
|
||||
line-height: var(--entry-line-height);
|
||||
border: 0 none;
|
||||
outline: none;
|
||||
background: var(--color-tan);
|
||||
padding: 0 1rem;
|
||||
margin-bottom: var(--spacing-margin);
|
||||
width: 100% !important; /* "!important" to prevent user changing width */
|
||||
height: calc(3 * var(--entry-line-height));
|
||||
box-sizing: border-box;
|
||||
font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
}
|
||||
|
||||
& textarea::placeholder {
|
||||
color: var(--color-brown);
|
||||
}
|
||||
|
||||
& button {
|
||||
font-size: inherit;
|
||||
font-weight: bold;
|
||||
line-height: 2.8rem;
|
||||
width: 10rem;
|
||||
border: 0 none;
|
||||
background: var(--control-gradient);
|
||||
color: var(--color-black);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.select {
|
||||
composes: fancy-select;
|
||||
}
|
80
src/components/Form/test.js
Normal file
80
src/components/Form/test.js
Normal file
@ -0,0 +1,80 @@
|
||||
jest.mock('react-feather/dist/icons/chevrons-down', () =>
|
||||
require('__mocks__/component-mock')(
|
||||
'react-feather/dist/icons/chevrons-down'));
|
||||
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from 'react-testing-library';
|
||||
|
||||
import Form from 'components/Form';
|
||||
|
||||
const syntaxList = [
|
||||
{ id: 'testJS', label: 'Testing JS' },
|
||||
{ id: 'other', label: 'Other' }
|
||||
];
|
||||
const commonProps = { syntaxList };
|
||||
|
||||
describe('Form', () => {
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<Form onSubmit={ jest.fn() } { ...commonProps }>
|
||||
Actions
|
||||
</Form>
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('submitting expression', () => {
|
||||
test('submitting form', () => {
|
||||
const onSubmit = jest.fn();
|
||||
const { getByTestId } = render(
|
||||
<Form onSubmit={ onSubmit } { ...commonProps } />
|
||||
);
|
||||
|
||||
fireEvent.change(getByTestId('expr-input'), {
|
||||
target: { value: 'Test expression' }
|
||||
});
|
||||
fireEvent.change(getByTestId('syntax-select'), {
|
||||
target: { value: 'other' }
|
||||
});
|
||||
|
||||
const event = new Event('submit');
|
||||
jest.spyOn(event, 'preventDefault');
|
||||
|
||||
fireEvent(getByTestId('form'), event);
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
expr: 'Test expression',
|
||||
syntax: 'other'
|
||||
});
|
||||
});
|
||||
|
||||
test('submitting form with Shift+Enter', () => {
|
||||
const onSubmit = jest.fn();
|
||||
const { getByTestId } = render(
|
||||
<Form onSubmit={ onSubmit } { ...commonProps } />
|
||||
);
|
||||
|
||||
fireEvent.keyPress(getByTestId('expr-input'), {
|
||||
charCode: 13,
|
||||
shiftKey: true
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('not submitting with just Enter', () => {
|
||||
const onSubmit = jest.fn();
|
||||
const { getByTestId } = render(
|
||||
<Form onSubmit={ onSubmit } { ...commonProps } />
|
||||
);
|
||||
|
||||
fireEvent.keyPress(getByTestId('expr-input'), {
|
||||
charCode: 13,
|
||||
shiftKey: false
|
||||
});
|
||||
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
34
src/components/FormActions/__snapshots__/test.js.snap
Normal file
34
src/components/FormActions/__snapshots__/test.js.snap
Normal file
@ -0,0 +1,34 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FormActions rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<ul
|
||||
class="actions"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`FormActions rendering with a permalink 1`] = `
|
||||
<DocumentFragment>
|
||||
<ul
|
||||
class="actions"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="http://example.com"
|
||||
>
|
||||
<span
|
||||
data-component="Link"
|
||||
data-props="{}"
|
||||
/>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Permalink
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</DocumentFragment>
|
||||
`;
|
60
src/components/FormActions/index.js
Normal file
60
src/components/FormActions/index.js
Normal file
@ -0,0 +1,60 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import DownloadIcon from 'react-feather/dist/icons/download';
|
||||
import LinkIcon from 'react-feather/dist/icons/link';
|
||||
|
||||
import style from './style.module.css';
|
||||
|
||||
import { createPngLink, createSvgLink } from './links';
|
||||
|
||||
const downloadLink = (link, t) => {
|
||||
const { url, filename, type, label } = link;
|
||||
return <li>
|
||||
<a href={ url } download={ filename } type={ type }>
|
||||
<DownloadIcon />{ t(label) }
|
||||
</a>
|
||||
</li>;
|
||||
};
|
||||
|
||||
const FormActions = ({
|
||||
permalinkUrl,
|
||||
imageDetails
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [svgLink, setSvgLink] = useState(null);
|
||||
const [pngLink, setPngLink] = useState(null);
|
||||
|
||||
const generateDownloadLinks = useCallback(async () => {
|
||||
const { svg, width, height } = imageDetails;
|
||||
|
||||
setSvgLink(await createSvgLink({ svg }));
|
||||
setPngLink(await createPngLink({ svg, width, height }));
|
||||
}, [setSvgLink, setPngLink, imageDetails]);
|
||||
|
||||
useEffect(() => {
|
||||
if (imageDetails && imageDetails.svg) {
|
||||
generateDownloadLinks();
|
||||
}
|
||||
}, [imageDetails]);
|
||||
|
||||
return <ul className={ style.actions }>
|
||||
{ pngLink && downloadLink(pngLink, t) }
|
||||
{ svgLink && downloadLink(svgLink, t) }
|
||||
{ permalinkUrl && <li>
|
||||
<a href={ permalinkUrl }><LinkIcon /><Trans>Permalink</Trans></a>
|
||||
</li> }
|
||||
</ul>;
|
||||
};
|
||||
|
||||
FormActions.propTypes = {
|
||||
permalinkUrl: PropTypes.string,
|
||||
imageDetails: PropTypes.shape({
|
||||
svg: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number
|
||||
})
|
||||
};
|
||||
|
||||
export default FormActions;
|
49
src/components/FormActions/links.js
Normal file
49
src/components/FormActions/links.js
Normal file
@ -0,0 +1,49 @@
|
||||
const createSvgLink = async ({ svg }) => {
|
||||
try {
|
||||
const type = 'image/svg+xml';
|
||||
const blob = new Blob([svg], { type });
|
||||
|
||||
return {
|
||||
url: URL.createObjectURL(blob),
|
||||
label: 'Download SVG',
|
||||
filename: 'image.svg',
|
||||
type
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e); // eslint-disable-line no-console
|
||||
}
|
||||
};
|
||||
|
||||
const createPngLink = async ({ svg, width, height }) => {
|
||||
try {
|
||||
const type = 'image/png';
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
const loader = new Image();
|
||||
|
||||
loader.width = canvas.width = width * 2;
|
||||
loader.height = canvas.height = height * 2;
|
||||
|
||||
await new Promise(resolve => {
|
||||
loader.onload = resolve;
|
||||
loader.src = 'data:image/svg+xml,' + encodeURIComponent(svg);
|
||||
});
|
||||
|
||||
context.drawImage(loader, 0, 0, loader.width, loader.height);
|
||||
|
||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, type));
|
||||
|
||||
return {
|
||||
url: URL.createObjectURL(blob),
|
||||
label: 'Download PNG',
|
||||
filename: 'image.png',
|
||||
type
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e); // eslint-disable-line no-console
|
||||
}
|
||||
};
|
||||
|
||||
export { createSvgLink, createPngLink };
|
19
src/components/FormActions/style.module.css
Normal file
19
src/components/FormActions/style.module.css
Normal file
@ -0,0 +1,19 @@
|
||||
@import url('../../globals.module.css');
|
||||
|
||||
.actions {
|
||||
composes: inline-list with-separator-left;
|
||||
float: right;
|
||||
|
||||
@media (max-width: 700px) {
|
||||
& li {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
& svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-right: 0.5rem;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
}
|
42
src/components/FormActions/test.js
Normal file
42
src/components/FormActions/test.js
Normal file
@ -0,0 +1,42 @@
|
||||
jest.mock('./links');
|
||||
jest.mock('react-feather/dist/icons/download', () =>
|
||||
require('__mocks__/component-mock')(
|
||||
'react-feather/dist/icons/download'));
|
||||
jest.mock('react-feather/dist/icons/link', () =>
|
||||
require('__mocks__/component-mock')(
|
||||
'react-feather/dist/icons/link'));
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'react-testing-library';
|
||||
|
||||
import FormActions from 'components/FormActions';
|
||||
import { createPngLink, createSvgLink } from './links';
|
||||
|
||||
createPngLink.mockResolvedValue({
|
||||
url: 'http://example.com/image.png',
|
||||
filename: 'image.png',
|
||||
type: 'image/png',
|
||||
label: 'Example PNG Link'
|
||||
});
|
||||
createSvgLink.mockResolvedValue({
|
||||
url: 'http://example.com/image.svg',
|
||||
filename: 'image.svg',
|
||||
type: 'image/svg+xml',
|
||||
label: 'Example SVG Link'
|
||||
});
|
||||
|
||||
describe('FormActions', () => {
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<FormActions/>
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('rendering with a permalink', () => {
|
||||
const { asFragment } = render(
|
||||
<FormActions permalinkUrl="http://example.com" />
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
534
src/components/Header/__snapshots__/test.js.snap
Normal file
534
src/components/Header/__snapshots__/test.js.snap
Normal file
@ -0,0 +1,534 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Header opening the Privacy Policy modal 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Modal"
|
||||
data-props="{
|
||||
\\"isOpen\\": true
|
||||
}"
|
||||
>
|
||||
<span
|
||||
data-component="PrivacyPolicy"
|
||||
data-props="{}"
|
||||
/>
|
||||
</span>
|
||||
<header
|
||||
class="header"
|
||||
>
|
||||
<h1>
|
||||
<a
|
||||
href="/"
|
||||
>
|
||||
Regexper
|
||||
</a>
|
||||
</h1>
|
||||
<ul
|
||||
class="list"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://gitlab.com/javallone/regexper-static"
|
||||
rel="external noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
data-component="Gitlab"
|
||||
data-props="{}"
|
||||
/>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Source on GitLab
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
data-testid="privacy-link"
|
||||
href="/privacy"
|
||||
>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Privacy Policy
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
data-component="InstallPrompt"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
data-requires-js="true"
|
||||
>
|
||||
<span
|
||||
data-component="LocaleSwitcher"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`Header opening the Privacy Policy modal while holding alt key 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Modal"
|
||||
data-props="{
|
||||
\\"isOpen\\": false
|
||||
}"
|
||||
>
|
||||
<span
|
||||
data-component="PrivacyPolicy"
|
||||
data-props="{}"
|
||||
/>
|
||||
</span>
|
||||
<header
|
||||
class="header"
|
||||
>
|
||||
<h1>
|
||||
<a
|
||||
href="/"
|
||||
>
|
||||
Regexper
|
||||
</a>
|
||||
</h1>
|
||||
<ul
|
||||
class="list"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://gitlab.com/javallone/regexper-static"
|
||||
rel="external noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
data-component="Gitlab"
|
||||
data-props="{}"
|
||||
/>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Source on GitLab
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
data-testid="privacy-link"
|
||||
href="/privacy"
|
||||
>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Privacy Policy
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
data-component="InstallPrompt"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
data-requires-js="true"
|
||||
>
|
||||
<span
|
||||
data-component="LocaleSwitcher"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`Header opening the Privacy Policy modal while holding ctrl key 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Modal"
|
||||
data-props="{
|
||||
\\"isOpen\\": false
|
||||
}"
|
||||
>
|
||||
<span
|
||||
data-component="PrivacyPolicy"
|
||||
data-props="{}"
|
||||
/>
|
||||
</span>
|
||||
<header
|
||||
class="header"
|
||||
>
|
||||
<h1>
|
||||
<a
|
||||
href="/"
|
||||
>
|
||||
Regexper
|
||||
</a>
|
||||
</h1>
|
||||
<ul
|
||||
class="list"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://gitlab.com/javallone/regexper-static"
|
||||
rel="external noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
data-component="Gitlab"
|
||||
data-props="{}"
|
||||
/>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Source on GitLab
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
data-testid="privacy-link"
|
||||
href="/privacy"
|
||||
>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Privacy Policy
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
data-component="InstallPrompt"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
data-requires-js="true"
|
||||
>
|
||||
<span
|
||||
data-component="LocaleSwitcher"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`Header opening the Privacy Policy modal while holding meta key 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Modal"
|
||||
data-props="{
|
||||
\\"isOpen\\": false
|
||||
}"
|
||||
>
|
||||
<span
|
||||
data-component="PrivacyPolicy"
|
||||
data-props="{}"
|
||||
/>
|
||||
</span>
|
||||
<header
|
||||
class="header"
|
||||
>
|
||||
<h1>
|
||||
<a
|
||||
href="/"
|
||||
>
|
||||
Regexper
|
||||
</a>
|
||||
</h1>
|
||||
<ul
|
||||
class="list"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://gitlab.com/javallone/regexper-static"
|
||||
rel="external noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
data-component="Gitlab"
|
||||
data-props="{}"
|
||||
/>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Source on GitLab
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
data-testid="privacy-link"
|
||||
href="/privacy"
|
||||
>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Privacy Policy
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
data-component="InstallPrompt"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
data-requires-js="true"
|
||||
>
|
||||
<span
|
||||
data-component="LocaleSwitcher"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`Header opening the Privacy Policy modal while holding shift key 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Modal"
|
||||
data-props="{
|
||||
\\"isOpen\\": false
|
||||
}"
|
||||
>
|
||||
<span
|
||||
data-component="PrivacyPolicy"
|
||||
data-props="{}"
|
||||
/>
|
||||
</span>
|
||||
<header
|
||||
class="header"
|
||||
>
|
||||
<h1>
|
||||
<a
|
||||
href="/"
|
||||
>
|
||||
Regexper
|
||||
</a>
|
||||
</h1>
|
||||
<ul
|
||||
class="list"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://gitlab.com/javallone/regexper-static"
|
||||
rel="external noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
data-component="Gitlab"
|
||||
data-props="{}"
|
||||
/>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Source on GitLab
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
data-testid="privacy-link"
|
||||
href="/privacy"
|
||||
>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Privacy Policy
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
data-component="InstallPrompt"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
data-requires-js="true"
|
||||
>
|
||||
<span
|
||||
data-component="LocaleSwitcher"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`Header rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Modal"
|
||||
data-props="{
|
||||
\\"isOpen\\": false
|
||||
}"
|
||||
>
|
||||
<span
|
||||
data-component="PrivacyPolicy"
|
||||
data-props="{}"
|
||||
/>
|
||||
</span>
|
||||
<header
|
||||
class="header"
|
||||
data-banner="testing"
|
||||
>
|
||||
<h1>
|
||||
<a
|
||||
href="/"
|
||||
>
|
||||
Regexper
|
||||
</a>
|
||||
</h1>
|
||||
<ul
|
||||
class="list"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://gitlab.com/javallone/regexper-static"
|
||||
rel="external noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
data-component="Gitlab"
|
||||
data-props="{}"
|
||||
/>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Source on GitLab
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
data-testid="privacy-link"
|
||||
href="/privacy"
|
||||
>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Privacy Policy
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
data-component="InstallPrompt"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
data-requires-js="true"
|
||||
>
|
||||
<span
|
||||
data-component="LocaleSwitcher"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`Header rendering with no banner 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Modal"
|
||||
data-props="{
|
||||
\\"isOpen\\": false
|
||||
}"
|
||||
>
|
||||
<span
|
||||
data-component="PrivacyPolicy"
|
||||
data-props="{}"
|
||||
/>
|
||||
</span>
|
||||
<header
|
||||
class="header"
|
||||
>
|
||||
<h1>
|
||||
<a
|
||||
href="/"
|
||||
>
|
||||
Regexper
|
||||
</a>
|
||||
</h1>
|
||||
<ul
|
||||
class="list"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://gitlab.com/javallone/regexper-static"
|
||||
rel="external noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
data-component="Gitlab"
|
||||
data-props="{}"
|
||||
/>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Source on GitLab
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
data-testid="privacy-link"
|
||||
href="/privacy"
|
||||
>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Privacy Policy
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
data-component="InstallPrompt"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
data-requires-js="true"
|
||||
>
|
||||
<span
|
||||
data-component="LocaleSwitcher"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
</DocumentFragment>
|
||||
`;
|
77
src/components/Header/index.js
Normal file
77
src/components/Header/index.js
Normal file
@ -0,0 +1,77 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-modal';
|
||||
import { Link } from 'gatsby';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import GitlabIcon from 'react-feather/dist/icons/gitlab';
|
||||
|
||||
import LocaleSwitcher from 'components/LocaleSwitcher';
|
||||
import InstallPrompt from 'components/InstallPrompt';
|
||||
import PrivacyPolicy from 'components/PrivacyPolicy';
|
||||
|
||||
import style from './style.module.css';
|
||||
|
||||
const Header = ({ banner }) => {
|
||||
const [ showModal, updateShowModal] = useState(false);
|
||||
const handleClose = useCallback(() => {
|
||||
updateShowModal(false);
|
||||
}, [updateShowModal]);
|
||||
const handleOpen = useCallback(event => {
|
||||
if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
updateShowModal(true);
|
||||
}, [updateShowModal]);
|
||||
|
||||
return <>
|
||||
<Modal
|
||||
isOpen={ showModal }
|
||||
onRequestClose={ handleClose }>
|
||||
<PrivacyPolicy onClose={ handleClose } />
|
||||
</Modal>
|
||||
<header
|
||||
className={ style.header }
|
||||
data-banner={ banner || null }>
|
||||
<h1>
|
||||
<Link to="/">Regexper</Link>
|
||||
</h1>
|
||||
|
||||
<ul className={ style.list }>
|
||||
<li>
|
||||
<a href="https://gitlab.com/javallone/regexper-static"
|
||||
rel="external noopener noreferrer"
|
||||
target="_blank">
|
||||
<GitlabIcon />
|
||||
<Trans>Source on GitLab</Trans>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/privacy"
|
||||
data-testid="privacy-link"
|
||||
onClick={ handleOpen }
|
||||
>
|
||||
<Trans>Privacy Policy</Trans>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<InstallPrompt />
|
||||
</li>
|
||||
<li data-requires-js>
|
||||
<LocaleSwitcher />
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
</>;
|
||||
};
|
||||
|
||||
Header.propTypes = {
|
||||
banner: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.string
|
||||
]).isRequired
|
||||
};
|
||||
|
||||
export default Header;
|
68
src/components/Header/style.module.css
Normal file
68
src/components/Header/style.module.css
Normal file
@ -0,0 +1,68 @@
|
||||
@import url('../../globals.module.css');
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--color-green) var(--gradient-green);
|
||||
box-shadow: 0 0 1rem color(var(--color-black) alpha(0.7));
|
||||
padding: 0 var(--content-margin);
|
||||
margin: 0 calc(-1 * var(--content-margin)) var(--spacing-margin) calc(-1 * var(--content-margin));
|
||||
position: relative;
|
||||
color: var(--color-black);
|
||||
min-width: 320px;
|
||||
|
||||
&:after {
|
||||
content: attr(data-banner);
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: var(--header-height);
|
||||
line-height: var(--header-height);
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
font-size: calc(var(--header-height) * 0.7);
|
||||
font-weight: bold;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
& h1 {
|
||||
flex-grow: 1;
|
||||
font-family: 'Bangers', 'cursive';
|
||||
font-size: 4rem;
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
line-height: var(--header-height);
|
||||
text-shadow: 0 0 5px var(--color-green);
|
||||
}
|
||||
|
||||
& a {
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
composes: inline-list with-separator-right;
|
||||
text-align: right;
|
||||
margin: 1rem 0;
|
||||
|
||||
& li {
|
||||
line-height: 2.4rem;
|
||||
|
||||
& a:hover,
|
||||
& a:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
& a svg {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-right: 0.5rem;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
}
|
||||
}
|
62
src/components/Header/test.js
Normal file
62
src/components/Header/test.js
Normal file
@ -0,0 +1,62 @@
|
||||
jest.mock('react-modal', () =>
|
||||
require('__mocks__/component-mock')('react-modal'));
|
||||
jest.mock('react-feather/dist/icons/gitlab', () =>
|
||||
require('__mocks__/component-mock')('react-feather/dist/icons/gitlab'));
|
||||
jest.mock('components/LocaleSwitcher', () =>
|
||||
require('__mocks__/component-mock')('components/LocaleSwitcher'));
|
||||
jest.mock('components/InstallPrompt', () =>
|
||||
require('__mocks__/component-mock')('components/InstallPrompt'));
|
||||
jest.mock('components/PrivacyPolicy', () =>
|
||||
require('__mocks__/component-mock')('components/PrivacyPolicy'));
|
||||
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from 'react-testing-library';
|
||||
|
||||
import Header from 'components/Header';
|
||||
|
||||
describe('Header', () => {
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<Header banner="testing" />
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('rendering with no banner', () => {
|
||||
const { asFragment } = render(
|
||||
<Header banner={ false } />
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('opening the Privacy Policy modal', () => {
|
||||
const { asFragment, getByTestId } = render(
|
||||
<Header banner={ false } />
|
||||
);
|
||||
const event = new MouseEvent('click', { bubbles: true });
|
||||
jest.spyOn(event, 'preventDefault');
|
||||
|
||||
fireEvent(getByTestId('privacy-link'), event);
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
['shift', 'ctrl', 'alt', 'meta'].forEach(key => {
|
||||
test(`opening the Privacy Policy modal while holding ${ key } key`, () => {
|
||||
const { asFragment, getByTestId } = render(
|
||||
<Header banner={ false } />
|
||||
);
|
||||
const event = new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
[key + 'Key']: true
|
||||
});
|
||||
jest.spyOn(event, 'preventDefault');
|
||||
|
||||
fireEvent(getByTestId('privacy-link'), event);
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
21
src/components/InstallPrompt/__snapshots__/test.js.snap
Normal file
21
src/components/InstallPrompt/__snapshots__/test.js.snap
Normal file
@ -0,0 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`InstallPrompt rendering 1`] = `<DocumentFragment />`;
|
||||
|
||||
exports[`InstallPrompt rendering after an install prompt has been requested 1`] = `<DocumentFragment />`;
|
||||
|
||||
exports[`InstallPrompt rendering after an install prompt has been requested 2`] = `
|
||||
<DocumentFragment>
|
||||
<a
|
||||
data-testid="install"
|
||||
href="#install"
|
||||
>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Add to Home Screen
|
||||
</span>
|
||||
</a>
|
||||
</DocumentFragment>
|
||||
`;
|
41
src/components/InstallPrompt/index.js
Normal file
41
src/components/InstallPrompt/index.js
Normal file
@ -0,0 +1,41 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
const InstallPrompt = () => {
|
||||
const [ installPrompt, updateInstallPrompt ] = useState(null);
|
||||
|
||||
const handleInstall = useCallback(async event => {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
installPrompt.prompt();
|
||||
await installPrompt.userChoice;
|
||||
}
|
||||
catch {
|
||||
// User cancelled install
|
||||
}
|
||||
|
||||
updateInstallPrompt(null);
|
||||
}, [installPrompt, updateInstallPrompt]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('beforeinstallprompt', updateInstallPrompt);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeinstallprompt', updateInstallPrompt);
|
||||
};
|
||||
});
|
||||
|
||||
if (!installPrompt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <a href="#install"
|
||||
data-testid="install"
|
||||
onClick={ handleInstall }
|
||||
>
|
||||
<Trans>Add to Home Screen</Trans>
|
||||
</a>;
|
||||
};
|
||||
|
||||
export default InstallPrompt;
|
42
src/components/InstallPrompt/test.js
Normal file
42
src/components/InstallPrompt/test.js
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from 'react-testing-library';
|
||||
|
||||
import InstallPrompt from 'components/InstallPrompt';
|
||||
|
||||
describe('InstallPrompt', () => {
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<InstallPrompt />
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('rendering after an install prompt has been requested', () => {
|
||||
const { asFragment } = render(
|
||||
<InstallPrompt />
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
const event = new Event('beforeinstallprompt', {
|
||||
prompt: jest.fn()
|
||||
});
|
||||
fireEvent(window, event);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('removing event listener', () => {
|
||||
jest.spyOn(window, 'addEventListener');
|
||||
jest.spyOn(window, 'removeEventListener');
|
||||
|
||||
const { unmount } = render(
|
||||
<InstallPrompt />
|
||||
);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(window.removeEventListener).toHaveBeenCalledWith(
|
||||
'beforeinstallprompt',
|
||||
expect.any(Function));
|
||||
});
|
||||
});
|
30
src/components/Layout/__snapshots__/test.js.snap
Normal file
30
src/components/Layout/__snapshots__/test.js.snap
Normal file
@ -0,0 +1,30 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Layout rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="SentryBoundary"
|
||||
data-props="{}"
|
||||
>
|
||||
<noscript />
|
||||
<span
|
||||
data-component="Header"
|
||||
data-props="{
|
||||
\\"banner\\": \\"Test Banner\\"
|
||||
}"
|
||||
/>
|
||||
<span
|
||||
data-component="SentryBoundary"
|
||||
data-props="{}"
|
||||
>
|
||||
Example content
|
||||
</span>
|
||||
<span
|
||||
data-component="Footer"
|
||||
data-props="{
|
||||
\\"buildId\\": \\"test-buildid\\"
|
||||
}"
|
||||
/>
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
54
src/components/Layout/index.js
Normal file
54
src/components/Layout/index.js
Normal file
@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { graphql, StaticQuery } from 'gatsby';
|
||||
|
||||
import SentryBoundary from 'components/SentryBoundary';
|
||||
import Header from 'components/Header';
|
||||
import Footer from 'components/Footer';
|
||||
|
||||
const query = graphql`
|
||||
query LayoutQuery {
|
||||
site {
|
||||
siteMetadata {
|
||||
banner
|
||||
buildId
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const noscriptStyle = `
|
||||
[data-requires-js] {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Layout = ({ banner, buildId, children }) => <SentryBoundary>
|
||||
<noscript>
|
||||
<style type="text/css">{ noscriptStyle }</style>
|
||||
</noscript>
|
||||
<Header banner={ banner } />
|
||||
<SentryBoundary>
|
||||
{ children }
|
||||
</SentryBoundary>
|
||||
<Footer buildId={ buildId } />
|
||||
</SentryBoundary>;
|
||||
|
||||
Layout.propTypes = {
|
||||
banner: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.string
|
||||
]).isRequired,
|
||||
buildId: PropTypes.string.isRequired,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node
|
||||
]).isRequired
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
export default props => (
|
||||
<StaticQuery query={ query } render={ ({ site: { siteMetadata } }) => (
|
||||
<Layout { ...props } { ...siteMetadata } />
|
||||
) } />
|
||||
);
|
22
src/components/Layout/test.js
Normal file
22
src/components/Layout/test.js
Normal file
@ -0,0 +1,22 @@
|
||||
jest.mock('components/SentryBoundary', () =>
|
||||
require('__mocks__/component-mock')('components/SentryBoundary'));
|
||||
jest.mock('components/Header', () =>
|
||||
require('__mocks__/component-mock')('components/Header'));
|
||||
jest.mock('components/Footer', () =>
|
||||
require('__mocks__/component-mock')('components/Footer'));
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'react-testing-library';
|
||||
|
||||
import { Layout } from 'components/Layout';
|
||||
|
||||
describe('Layout', () => {
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<Layout banner="Test Banner" buildId="test-buildid">
|
||||
Example content
|
||||
</Layout>
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
75
src/components/Loader/__snapshots__/test.js.snap
Normal file
75
src/components/Loader/__snapshots__/test.js.snap
Normal file
@ -0,0 +1,75 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Loader rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="loader"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="2"
|
||||
y2="6"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="18"
|
||||
y2="22"
|
||||
/>
|
||||
<line
|
||||
x1="4.93"
|
||||
x2="7.76"
|
||||
y1="4.93"
|
||||
y2="7.76"
|
||||
/>
|
||||
<line
|
||||
x1="16.24"
|
||||
x2="19.07"
|
||||
y1="16.24"
|
||||
y2="19.07"
|
||||
/>
|
||||
<line
|
||||
x1="2"
|
||||
x2="6"
|
||||
y1="12"
|
||||
y2="12"
|
||||
/>
|
||||
<line
|
||||
x1="18"
|
||||
x2="22"
|
||||
y1="12"
|
||||
y2="12"
|
||||
/>
|
||||
<line
|
||||
x1="4.93"
|
||||
x2="7.76"
|
||||
y1="19.07"
|
||||
y2="16.24"
|
||||
/>
|
||||
<line
|
||||
x1="16.24"
|
||||
x2="19.07"
|
||||
y1="7.76"
|
||||
y2="4.93"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="message"
|
||||
>
|
||||
TRANSLATE(Loading...)
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
17
src/components/Loader/index.js
Normal file
17
src/components/Loader/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import LoaderIcon from 'react-feather/dist/icons/loader';
|
||||
|
||||
import style from './style.module.css';
|
||||
|
||||
const Loader = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <div className={ style.loader }>
|
||||
<LoaderIcon />
|
||||
<div className={ style.message }>{ t('Loading...') }</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default Loader;
|
43
src/components/Loader/style.module.css
Normal file
43
src/components/Loader/style.module.css
Normal file
@ -0,0 +1,43 @@
|
||||
@import url('../../globals.module.css');
|
||||
|
||||
.loader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: var(--spacing-margin) 0;
|
||||
padding: 2rem;
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
|
||||
& .message {
|
||||
font-weight: bold;
|
||||
font-size: 2.5rem;
|
||||
margin-top: 2rem;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& svg {
|
||||
display: block;
|
||||
transform: scaleZ(1); /* Move to separate render layer in Chrome */
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
stroke: var(--color-black);
|
||||
animation: loader-spin 1s steps(8) infinite;
|
||||
|
||||
& line:nth-of-type(1) { stroke: color(var(--color-black) alpha(0.75)); }
|
||||
& line:nth-of-type(3) { stroke: color(var(--color-black) alpha(0.50)); }
|
||||
& line:nth-of-type(5) { stroke: color(var(--color-black) alpha(0.25)); }
|
||||
& line:nth-of-type(7) { stroke: color(var(--color-black) alpha(0)); }
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loader-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
15
src/components/Loader/test.js
Normal file
15
src/components/Loader/test.js
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-testing-library';
|
||||
|
||||
import Loader from 'components/Loader';
|
||||
|
||||
describe('Loader', () => {
|
||||
test('rendering', () => {
|
||||
// Using full rendering here since styles for this depend on the structure
|
||||
// of the SVG.
|
||||
const { asFragment } = render(
|
||||
<Loader/>
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
36
src/components/LocaleSwitcher/__snapshots__/test.js.snap
Normal file
36
src/components/LocaleSwitcher/__snapshots__/test.js.snap
Normal file
@ -0,0 +1,36 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LocaleSwitcher rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<label>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Language
|
||||
</span>
|
||||
<div
|
||||
class="switcher"
|
||||
>
|
||||
<select
|
||||
data-testid="language-select"
|
||||
>
|
||||
<option
|
||||
value="en"
|
||||
>
|
||||
English
|
||||
</option>
|
||||
<option
|
||||
value="other"
|
||||
>
|
||||
Other
|
||||
</option>
|
||||
</select>
|
||||
<span
|
||||
data-component="ChevronsDown"
|
||||
data-props="{}"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</DocumentFragment>
|
||||
`;
|
47
src/components/LocaleSwitcher/index.js
Normal file
47
src/components/LocaleSwitcher/index.js
Normal file
@ -0,0 +1,47 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import ExpandIcon from 'react-feather/dist/icons/chevrons-down';
|
||||
|
||||
import i18n, { locales } from 'i18n';
|
||||
|
||||
import localeToAvailable from './locale-to-available';
|
||||
import style from './style.module.css';
|
||||
|
||||
const LocaleSwitcher = () => {
|
||||
const [ current, updateCurrent ] = useState(localeToAvailable(
|
||||
i18n.language || '',
|
||||
locales.map(l => l.code),
|
||||
'en'));
|
||||
|
||||
useEffect(() => {
|
||||
i18n.on('languageChanged', updateCurrent);
|
||||
|
||||
return () => {
|
||||
i18n.off('languageChanged', updateCurrent);
|
||||
};
|
||||
});
|
||||
|
||||
const handleSelectChange = useCallback(({ target }) => {
|
||||
i18n.changeLanguage(target.value);
|
||||
});
|
||||
|
||||
return <label>
|
||||
<Trans>Language</Trans>
|
||||
<div className={ style.switcher }>
|
||||
<select data-testid="language-select"
|
||||
value={ current }
|
||||
onChange={ handleSelectChange }
|
||||
>
|
||||
{ locales.map(locale => (
|
||||
<option value={ locale.code } key={ locale.code }>
|
||||
{ locale.name }
|
||||
</option>
|
||||
)) }
|
||||
</select>
|
||||
<ExpandIcon />
|
||||
</div>
|
||||
</label>;
|
||||
};
|
||||
|
||||
export default LocaleSwitcher;
|
15
src/components/LocaleSwitcher/locale-to-available.js
Normal file
15
src/components/LocaleSwitcher/locale-to-available.js
Normal file
@ -0,0 +1,15 @@
|
||||
const localeToAvailable = (locale, available, defaultLocale) => {
|
||||
if (available.includes(locale)) {
|
||||
return locale;
|
||||
}
|
||||
|
||||
const parts = locale.split('-');
|
||||
|
||||
if (parts.length > 0 && available.includes(parts[0])) {
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
return defaultLocale;
|
||||
};
|
||||
|
||||
export default localeToAvailable;
|
18
src/components/LocaleSwitcher/locale-to-available.test.js
Normal file
18
src/components/LocaleSwitcher/locale-to-available.test.js
Normal file
@ -0,0 +1,18 @@
|
||||
import localeToAvailable from './locale-to-available';
|
||||
|
||||
describe('localeToAvailable', () => {
|
||||
test('when requested language and region are available', () => {
|
||||
expect(localeToAvailable('en-US', ['en', 'en-US', 'other'], 'other'))
|
||||
.toEqual('en-US');
|
||||
});
|
||||
|
||||
test('when only requested language is available', () => {
|
||||
expect(localeToAvailable('en-US', ['en', 'en-GB', 'other'], 'other'))
|
||||
.toEqual('en');
|
||||
});
|
||||
|
||||
test('when language is unavailable', () => {
|
||||
expect(localeToAvailable('en-US', ['tlh', 'other'], 'other'))
|
||||
.toEqual('other');
|
||||
});
|
||||
});
|
11
src/components/LocaleSwitcher/style.module.css
Normal file
11
src/components/LocaleSwitcher/style.module.css
Normal file
@ -0,0 +1,11 @@
|
||||
@import url('../../globals.module.css');
|
||||
|
||||
:root {
|
||||
--control-gradient: var(--color-tan) var(--gradient-tan);
|
||||
--select-height: 2.4rem;
|
||||
--select-width: 10rem;
|
||||
}
|
||||
|
||||
.switcher {
|
||||
composes: fancy-select;
|
||||
}
|
63
src/components/LocaleSwitcher/test.js
Normal file
63
src/components/LocaleSwitcher/test.js
Normal file
@ -0,0 +1,63 @@
|
||||
jest.mock('react-feather/dist/icons/chevrons-down', () =>
|
||||
require('__mocks__/component-mock')(
|
||||
'react-feather/dist/icons/chevrons-down'));
|
||||
|
||||
import React from 'react';
|
||||
import { render, fireEvent, act } from 'react-testing-library';
|
||||
|
||||
import i18n from 'i18n';
|
||||
import LocaleSwitcher from 'components/LocaleSwitcher';
|
||||
|
||||
// Ensure initial locale is always "en" during tests
|
||||
jest.mock('./locale-to-available', () => jest.fn(() => 'en'));
|
||||
|
||||
describe('LocaleSwitcher', () => {
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<LocaleSwitcher />
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('changing language', () => {
|
||||
jest.spyOn(i18n, 'changeLanguage');
|
||||
|
||||
const { getByTestId } = render(
|
||||
<LocaleSwitcher />
|
||||
);
|
||||
const event = new Event('change', { bubbles: true });
|
||||
const select = getByTestId('language-select');
|
||||
select.value = 'other';
|
||||
|
||||
fireEvent(select, event);
|
||||
|
||||
expect(i18n.changeLanguage).toHaveBeenCalledWith('other');
|
||||
});
|
||||
|
||||
test('interface update from language change', () => {
|
||||
const { getByTestId } = render(
|
||||
<LocaleSwitcher />
|
||||
);
|
||||
|
||||
expect(getByTestId('language-select').value).toEqual('en');
|
||||
|
||||
act(() => {
|
||||
i18n.emit('languageChanged', 'other');
|
||||
});
|
||||
|
||||
expect(getByTestId('language-select').value).toEqual('other');
|
||||
});
|
||||
|
||||
test('disconnecting event handler on unmount', () => {
|
||||
const { unmount } = render(
|
||||
<LocaleSwitcher />
|
||||
);
|
||||
|
||||
jest.spyOn(i18n, 'off');
|
||||
|
||||
unmount();
|
||||
expect(i18n.off).toHaveBeenCalledWith(
|
||||
'languageChanged',
|
||||
expect.any(Function));
|
||||
});
|
||||
});
|
105
src/components/Message/__snapshots__/test.js.snap
Normal file
105
src/components/Message/__snapshots__/test.js.snap
Normal file
@ -0,0 +1,105 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Message rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="message"
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
>
|
||||
<h2>
|
||||
Testing
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Message content
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`Message rendering with a close button 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="message"
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
>
|
||||
<h2>
|
||||
Testing
|
||||
</h2>
|
||||
<button>
|
||||
<span
|
||||
data-component="XSquare"
|
||||
data-props="{}"
|
||||
/>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Message content
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`Message rendering with icon 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="message"
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
>
|
||||
Sample icon SVG
|
||||
<h2>
|
||||
Testing
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Message content
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`Message rendering with type 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="message error"
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
>
|
||||
<span
|
||||
data-component="AlertOctagon"
|
||||
data-props="{}"
|
||||
/>
|
||||
<h2>
|
||||
Testing
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Message content
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
63
src/components/Message/index.js
Normal file
63
src/components/Message/index.js
Normal file
@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import style from './style.module.css';
|
||||
|
||||
import InfoIcon from 'react-feather/dist/icons/info';
|
||||
import ErrorIcon from 'react-feather/dist/icons/alert-octagon';
|
||||
import WarningIcon from 'react-feather/dist/icons/alert-triangle';
|
||||
import CloseIcon from 'react-feather/dist/icons/x-square';
|
||||
|
||||
const iconTypes = {
|
||||
info: InfoIcon,
|
||||
error: ErrorIcon,
|
||||
warning: WarningIcon
|
||||
};
|
||||
|
||||
const renderIcon = (type, icon) => {
|
||||
const Icon = icon || iconTypes[type];
|
||||
|
||||
if (!Icon) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <Icon />;
|
||||
};
|
||||
|
||||
const Message = ({ type, icon, heading, onClose, children }) => (
|
||||
<div className={ [
|
||||
style.message,
|
||||
type && style[type]
|
||||
].filter(Boolean).join(' ') }>
|
||||
<div className={ style.header }>
|
||||
{ renderIcon(type, icon) }
|
||||
<h2>{ heading }</h2>
|
||||
{ onClose && <button onClick={ onClose }>
|
||||
<CloseIcon /> Close
|
||||
</button> }
|
||||
</div>
|
||||
<div className={ style.content }>
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Message.propTypes = {
|
||||
type: PropTypes.oneOf([
|
||||
'info',
|
||||
'error',
|
||||
'warning'
|
||||
]),
|
||||
icon: PropTypes.oneOfType([
|
||||
PropTypes.element,
|
||||
PropTypes.func
|
||||
]),
|
||||
heading: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node
|
||||
]).isRequired
|
||||
};
|
||||
|
||||
export default Message;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user