mirror of
https://github.com/aelve/guide.git
synced 2024-11-25 07:52:52 +03:00
parent
d334d7e355
commit
0336942072
88
.travis.yml
88
.travis.yml
@ -11,8 +11,7 @@ services:
|
||||
- postgresql
|
||||
|
||||
before_install:
|
||||
- # start your web application and listen on `localhost`
|
||||
- google-chrome-stable --headless --disable-gpu --remote-debugging-port=5000 http://localhost &
|
||||
- nvm install --lts
|
||||
|
||||
cache:
|
||||
directories:
|
||||
@ -23,22 +22,6 @@ cache:
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: "Build the frontend and upload a Docker image"
|
||||
script:
|
||||
# Build
|
||||
- cd front
|
||||
- npm install
|
||||
- npm run build
|
||||
- cd -
|
||||
# Upload the Docker image
|
||||
- |
|
||||
if [ "$TRAVIS_EVENT_TYPE" = "push" ]; then
|
||||
export BRANCH="$(echo "$TRAVIS_BRANCH" | tr '/' '-')"
|
||||
make front/travis-docker "tag=$BRANCH--front" || travis_terminate 1
|
||||
docker login quay.io -u "$DOCKER_USER" -p "$DOCKER_PASS"
|
||||
docker push "quay.io/aelve/guide:$BRANCH--front"
|
||||
fi
|
||||
|
||||
- stage: "Build the backend and upload a Docker image"
|
||||
before_script:
|
||||
- sudo apt-get install -y libgmp-dev
|
||||
@ -97,10 +80,68 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
before_cache:
|
||||
- rm -rf $HOME/.stack/programs # GHC is faster to install than to cache
|
||||
- rm -rf $HOME/.stack/programs # GHC is faster to install than to cache
|
||||
|
||||
- stage: "Build the frontend and upload a Docker image, test frontend"
|
||||
- name: "Build the frontend and upload a Docker image"
|
||||
script:
|
||||
# Build
|
||||
- cd front
|
||||
- npm install
|
||||
- npm run build
|
||||
- cd -
|
||||
# Upload the Docker image
|
||||
- |
|
||||
if [ "$TRAVIS_EVENT_TYPE" = "push" ]; then
|
||||
export BRANCH="$(echo "$TRAVIS_BRANCH" | tr '/' '-')"
|
||||
make front/travis-docker "tag=$BRANCH--front"
|
||||
docker login quay.io -u "$DOCKER_USER" -p "$DOCKER_PASS"
|
||||
docker push "quay.io/aelve/guide:$BRANCH--front"
|
||||
fi
|
||||
|
||||
- name: "Test frontend prod desktop"
|
||||
script:
|
||||
- node -v
|
||||
- . ./scripts/ci-functions.sh
|
||||
- cd front
|
||||
- npm install
|
||||
- npm run build
|
||||
- export BRANCH="$(echo "$TRAVIS_BRANCH" | tr '/' '-')"
|
||||
- run_back $BRANCH
|
||||
- API_URL=http://localhost:4400/ npm run test:prod
|
||||
|
||||
- name: "Test frontend dev desktop"
|
||||
script:
|
||||
- . ./scripts/ci-functions.sh
|
||||
- cd front
|
||||
- npm install
|
||||
- export BRANCH="$(echo "$TRAVIS_BRANCH" | tr '/' '-')"
|
||||
- run_back $BRANCH
|
||||
- API_URL=http://localhost:4400/ npm run test:dev
|
||||
|
||||
- name: "Test frontend prod mobile"
|
||||
script:
|
||||
- . ./scripts/ci-functions.sh
|
||||
- cd front
|
||||
- npm install
|
||||
- npm run build
|
||||
- export BRANCH="$(echo "$TRAVIS_BRANCH" | tr '/' '-')"
|
||||
- run_back $BRANCH
|
||||
- API_URL=http://localhost:4400/ npm run test:prod:mobile
|
||||
|
||||
- name: "Test frontend dev mobile"
|
||||
script:
|
||||
- . ./scripts/ci-functions.sh
|
||||
- cd front
|
||||
- npm install
|
||||
- export BRANCH="$(echo "$TRAVIS_BRANCH" | tr '/' '-')"
|
||||
- run_back $BRANCH
|
||||
- API_URL=http://localhost:4400/ npm run test:dev:mobile
|
||||
|
||||
|
||||
- stage: "Test the backend"
|
||||
before_script:
|
||||
- google-chrome-stable --headless --dis able-gpu --remote-debugging-port=5000 http://localhost &
|
||||
- sudo apt-get install -y libgmp-dev fluxbox
|
||||
- curl -sSL https://get.haskellstack.org/ | sh
|
||||
# travis_retry works around https://github.com/commercialhaskell/stack/issues/4888
|
||||
@ -123,14 +164,7 @@ jobs:
|
||||
- make back/test-db
|
||||
- make back/load-into-postgres
|
||||
before_cache:
|
||||
- rm -rf $HOME/.stack/programs # GHC is faster to install than to cache
|
||||
|
||||
# - stage: "Test the frontend"
|
||||
# - sleep 10
|
||||
# - fluxbox >/dev/null 2>&1 &
|
||||
# script:
|
||||
# # Run testcafe e2e testing
|
||||
# - npm run test
|
||||
- rm -rf $HOME/.stack/programs # GHC is faster to install than to cache
|
||||
|
||||
notifications:
|
||||
slack:
|
||||
|
@ -2,6 +2,7 @@ import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { createBundleRenderer } from 'vue-server-renderer'
|
||||
// TODO move 'src' prefix to config
|
||||
// TODO do not copy setupDevServer, webpack configs to builded front
|
||||
// TODO find a way to get rid of ts ignore and still build without errors
|
||||
// @ts-ignore
|
||||
import bundle from '../src/vue-ssr-server-bundle.json'
|
||||
|
24
front/client/components/AddCategoryBtn.vue
Normal file
24
front/client/components/AddCategoryBtn.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<v-btn
|
||||
text
|
||||
class="ma-0 mt-1 px-2"
|
||||
color="grey darken-2"
|
||||
aria-label="Add new category"
|
||||
data-testid="CreateCategoryBtn"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<v-icon size="14" class="mr-1" left v-text="'$vuetify.icons.plus'"></v-icon>
|
||||
Add new category
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import Component from 'vue-class-component'
|
||||
|
||||
@Component
|
||||
export default class AddCategoryBtn extends Vue { }
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -30,11 +30,13 @@
|
||||
ref="categoryNameInput"
|
||||
class="mb-3"
|
||||
label="Category name"
|
||||
data-testid="add-category-dialog_category-name-input"
|
||||
data-testid="AddCategoryDialog-NameInput"
|
||||
:rules="categoryValidationRules"
|
||||
v-model="categoryName"
|
||||
/>
|
||||
<v-text-field
|
||||
data-testid="AddCategoryDialog-GroupInput"
|
||||
:rules="groupValidationRules"
|
||||
v-model="groupNameInternal"
|
||||
label="Group"
|
||||
/>
|
||||
@ -54,7 +56,7 @@
|
||||
aria-label="Submit"
|
||||
color="info"
|
||||
class="add-category-submit-btn"
|
||||
data-testid="add-category-dialog_submit-btn"
|
||||
data-testid="AddCategoryDialog-SubmitBtn"
|
||||
:disabled="!isValid"
|
||||
@click.native="submit"
|
||||
>
|
||||
@ -71,25 +73,26 @@
|
||||
<ConfirmDialog
|
||||
eager
|
||||
ref="duplicateConfirm"
|
||||
data-testid="duplicate-category-dialog"
|
||||
:confirmBtnProps="{
|
||||
'data-testid': 'duplicate-category-dialog_confirm-btn'
|
||||
'data-testid': 'DuplicateCategoryDialog-ConfirmBtn'
|
||||
}"
|
||||
max-width="500px"
|
||||
:value="isDuplicateConfirmShow"
|
||||
>
|
||||
This group already has categories with the same name:
|
||||
<ul class="duplicate-categories-list">
|
||||
<li
|
||||
v-for="category in sameNameCategories"
|
||||
:key="category.id"
|
||||
>
|
||||
<router-link
|
||||
:to="`/haskell/${getCategoryUrl(category)}`"
|
||||
target="_blank"
|
||||
>{{ category.title }}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<div data-testid="DuplicateCategoryDialog">
|
||||
This group already has categories with the same name:
|
||||
<ul class="duplicate-categories-list">
|
||||
<li
|
||||
v-for="category in sameNameCategories"
|
||||
:key="category.id"
|
||||
>
|
||||
<router-link
|
||||
:to="`/haskell/${getCategoryUrl(category)}`"
|
||||
target="_blank"
|
||||
>{{ category.title }}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
|
||||
</v-dialog>
|
||||
@ -118,36 +121,44 @@ export default class AddCategoryDialog extends Vue {
|
||||
categoryValidationRules: Array<(x: string) => boolean | string> = [
|
||||
(x: string) => !!x || `Category name can't be empty`
|
||||
]
|
||||
groupValidationRules: Array<(x: string) => boolean | string> = [
|
||||
(x: string) => !!x || `Group can't be empty`
|
||||
]
|
||||
isValid: boolean = false
|
||||
isDuplicateConfirmShow: boolean = false
|
||||
sameNameCategories = []
|
||||
|
||||
get categories () {
|
||||
return this.$store.state.category.categoryList
|
||||
}
|
||||
|
||||
get sameNameCategories () {
|
||||
if (!this.categoryName || !this.groupName) {
|
||||
return []
|
||||
}
|
||||
return this.categories
|
||||
.filter(x => x.group === this.groupName && x.title === this.categoryName)
|
||||
}
|
||||
|
||||
@Watch('value')
|
||||
onOpen (newVal: boolean) {
|
||||
this.categoryName = ''
|
||||
this.groupNameInternal = this.groupName
|
||||
// wait for vue to render form
|
||||
this.$nextTick(() => {
|
||||
this.$refs.form.resetValidation()
|
||||
this.categoryName = ''
|
||||
this.groupNameInternal = this.groupName
|
||||
})
|
||||
}
|
||||
|
||||
close () {
|
||||
this.$emit('input', false)
|
||||
}
|
||||
|
||||
calcSameNameCategories () {
|
||||
if (!this.categoryName || !this.groupNameInternal) {
|
||||
return []
|
||||
}
|
||||
return this.categories
|
||||
.filter(x => x.group === this.groupNameInternal && x.title === this.categoryName)
|
||||
}
|
||||
|
||||
async submit () {
|
||||
if (!this.$refs.form.validate()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.sameNameCategories = this.calcSameNameCategories()
|
||||
if (this.sameNameCategories.length) {
|
||||
const isDuplicationConfirmed = await this.confirmDuplicate()
|
||||
if (!isDuplicationConfirmed) {
|
||||
|
@ -28,17 +28,20 @@
|
||||
autofocus
|
||||
class="mb-2"
|
||||
label="Item name"
|
||||
data-testid="AddItemDialog-NameInput"
|
||||
:rules="nameValidationRules"
|
||||
v-model="name"
|
||||
/>
|
||||
<v-text-field
|
||||
class="mb-2"
|
||||
label="Name on Hackage (optional)"
|
||||
data-testid="AddItemDialog-HackageInput"
|
||||
v-model="hackage"
|
||||
/>
|
||||
<v-text-field
|
||||
class="mb-2"
|
||||
label="Link to the official site, if exists"
|
||||
data-testid="AddItemDialog-LinkInput"
|
||||
v-model="link"
|
||||
/>
|
||||
|
||||
@ -57,6 +60,7 @@
|
||||
<v-btn
|
||||
color="info"
|
||||
:disabled="!isValid"
|
||||
data-testid="AddItemDialog-SubmitBtn"
|
||||
aria-label="Create"
|
||||
@click.native="submit"
|
||||
>
|
||||
|
@ -2,18 +2,32 @@
|
||||
<div class="categories">
|
||||
<div class="categories-flex-container">
|
||||
|
||||
<AddCategoryBtn
|
||||
class="mt-4"
|
||||
v-if="!categories || !categories.length"
|
||||
@click="openAddCategoryDialog()"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="categories-column"
|
||||
v-for="(groupCategories, groupName, index) in groups"
|
||||
:key="index"
|
||||
>
|
||||
<div class="category-group">
|
||||
<h2 class="mt-0 mb-2 group-title">
|
||||
<div
|
||||
class="category-group"
|
||||
data-testid="Categories-CategoryGroup"
|
||||
>
|
||||
<h2
|
||||
class="mt-0 mb-2 group-title"
|
||||
data-testid="Categories-CategoryGroup-Title"
|
||||
>
|
||||
{{ groupName }}
|
||||
</h2>
|
||||
|
||||
<!-- TODO remove duplication of category title links (move to component or refactor v-for) -->
|
||||
<router-link
|
||||
class="category-title ml-2"
|
||||
data-testid="Categories-CategoryTitle"
|
||||
v-for="category in groupCategories[CategoryStatus.finished]"
|
||||
:key="category.id"
|
||||
:to="`/haskell/${getCategoryUrl(category)}`"
|
||||
@ -27,6 +41,7 @@
|
||||
</h3>
|
||||
<router-link
|
||||
class="category-title ml-3"
|
||||
data-testid="Categories-CategoryTitle"
|
||||
v-for="category in groupCategories[CategoryStatus.inProgress]"
|
||||
:key="category.id"
|
||||
:to="`/haskell/${getCategoryUrl(category)}`"
|
||||
@ -42,6 +57,7 @@
|
||||
|
||||
<router-link
|
||||
class="category-title ml-3"
|
||||
data-testid="Categories-CategoryTitle"
|
||||
v-for="category in groupCategories[CategoryStatus.toBeWritten]"
|
||||
:key="category.id"
|
||||
:to="`/haskell/${getCategoryUrl(category)}`"
|
||||
@ -50,16 +66,7 @@
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<v-btn
|
||||
text
|
||||
class="ma-0 mt-1 px-2"
|
||||
color="grey darken-2"
|
||||
aria-label="Add new category"
|
||||
@click="openAddCategoryDialog(groupName)"
|
||||
>
|
||||
<v-icon size="14" class="mr-1" left v-text="'$vuetify.icons.plus'"></v-icon>
|
||||
Add new category
|
||||
</v-btn>
|
||||
<AddCategoryBtn @click="openAddCategoryDialog(groupName)"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -76,6 +83,7 @@
|
||||
import Vue from 'vue'
|
||||
import Component from 'vue-class-component'
|
||||
import AddCategoryDialog from 'client/components/AddCategoryDialog.vue'
|
||||
import AddCategoryBtn from 'client/components/AddCategoryBtn.vue'
|
||||
import _groupBy from 'lodash/groupBy'
|
||||
import _sortBy from 'lodash/sortBy'
|
||||
import _fromPairs from 'lodash/fromPairs'
|
||||
@ -84,7 +92,8 @@ import { ICategoryInfo, CategoryStatus } from 'client/service/Category'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
AddCategoryDialog
|
||||
AddCategoryDialog,
|
||||
AddCategoryBtn
|
||||
}
|
||||
})
|
||||
export default class Categories extends Vue {
|
||||
|
@ -6,12 +6,14 @@
|
||||
v-else
|
||||
v-html="categoryDescription"
|
||||
class="category-description__content"
|
||||
data-testid="CategoryDescription-Content"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<markdown-editor
|
||||
v-else
|
||||
toolbar
|
||||
data-testid="CategoryDescription-Editor"
|
||||
:value="categoryDescriptionRaw"
|
||||
@cancel="toggleEditDescription"
|
||||
@save="updateDescription({ original: categoryDescriptionRaw, modified: $event})"
|
||||
@ -19,6 +21,7 @@
|
||||
|
||||
<v-btn
|
||||
text
|
||||
data-testid="Category-EditDescriptionBtn"
|
||||
v-if="!isEditDescription"
|
||||
color="grey darken-2"
|
||||
class="mt-3"
|
||||
|
@ -10,34 +10,47 @@
|
||||
>
|
||||
<v-icon class="rss-link-icon">$vuetify.icons.rss</v-icon>
|
||||
</a>
|
||||
<h1 class="category-name-title" :title="categoryTitle">{{categoryTitle}}</h1>
|
||||
<h1
|
||||
class="category-name-title"
|
||||
data-testid="CategoryHeader-Title"
|
||||
:title="categoryTitle"
|
||||
>{{categoryTitle}}</h1>
|
||||
</div>
|
||||
|
||||
<div class="category-header__second-row">
|
||||
<div class="category-group-title-wrap">
|
||||
in <span :title="categoryGroup" class="category-group-title"> {{ categoryGroup }} </span>
|
||||
in
|
||||
<span
|
||||
class="category-group-title"
|
||||
data-testid="CategoryHeader-Group"
|
||||
:title="categoryGroup"
|
||||
>{{ categoryGroup }}</span>
|
||||
</div>
|
||||
|
||||
<div class="category-actions">
|
||||
<CategoryHeaderBtn
|
||||
class="mr-1"
|
||||
text="New item"
|
||||
icon="plus"
|
||||
class="mr-1"
|
||||
data-testid="CategoryHeader-NewItemBtn"
|
||||
@click="openAddItemDialog"
|
||||
/>
|
||||
<CategoryHeaderBtn
|
||||
text="Category settings"
|
||||
icon="cog "
|
||||
class="mr-1"
|
||||
text="Category settings"
|
||||
icon="cog"
|
||||
data-testid="CategoryHeader-CategorySettingsBtn"
|
||||
@click="openCategorySettingsEditDialog"
|
||||
/>
|
||||
<CategoryHeaderBtn
|
||||
text="Delete category"
|
||||
icon="trash-alt"
|
||||
data-testid="CategoryHeader-CategoryDeleteBtn"
|
||||
@click="deleteCategory"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- TODO create responsive toolbar component to get rid of duplication of buttons -->
|
||||
<v-menu bottom left offset-y>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
@ -45,6 +58,7 @@
|
||||
icon
|
||||
aria-label="Actions"
|
||||
v-tooltip="'Actions'"
|
||||
data-testid="CategoryHeader-MobileMenuBtn"
|
||||
class="category-actions-menu-btn"
|
||||
v-on="on"
|
||||
>
|
||||
@ -61,6 +75,7 @@
|
||||
block
|
||||
text="New item"
|
||||
icon="plus"
|
||||
data-testid="CategoryHeader-NewItemBtn"
|
||||
@click="openAddItemDialog"
|
||||
/>
|
||||
</v-list-item>
|
||||
@ -69,6 +84,7 @@
|
||||
block
|
||||
text="Category settings"
|
||||
icon="cog"
|
||||
data-testid="CategoryHeader-CategorySettingsBtn"
|
||||
@click="openCategorySettingsEditDialog"
|
||||
/>
|
||||
</v-list-item>
|
||||
@ -77,6 +93,7 @@
|
||||
block
|
||||
text="Delete category"
|
||||
icon="trash-alt"
|
||||
data-testid="CategoryHeader-CategoryDeleteBtn"
|
||||
@click="deleteCategory"
|
||||
/>
|
||||
</v-list-item>
|
||||
@ -129,7 +146,8 @@ export default class CategoryHeader extends Vue {
|
||||
text: 'delete this category',
|
||||
confirmBtnText: 'Delete',
|
||||
confirmBtnProps: {
|
||||
color: 'error'
|
||||
'color': 'error',
|
||||
'data-testid': 'DeleteCategoryDialog-ConfirmBtn'
|
||||
}
|
||||
})
|
||||
async deleteCategory () {
|
||||
|
@ -18,17 +18,20 @@
|
||||
<category-item-section
|
||||
title="Summary"
|
||||
class="mb-3"
|
||||
data-testid="CategoryItem-SummarySection"
|
||||
:editText="summary.text"
|
||||
@save="updateSummary({original: summary.text, modified: $event})"
|
||||
>
|
||||
<div
|
||||
class="mb-2"
|
||||
data-testid="CategoryItem-SummarySection-Content"
|
||||
v-html="summary.html"
|
||||
/>
|
||||
</category-item-section>
|
||||
|
||||
<div
|
||||
v-if="isSectionEnabled('ItemProsConsSection')"
|
||||
data-testid="CategoryItem-TraitsSection"
|
||||
class="category-item-traits mb-3"
|
||||
>
|
||||
<category-item-traits
|
||||
@ -49,17 +52,22 @@
|
||||
v-if="isSectionEnabled('ItemEcosystemSection')"
|
||||
title="Ecosystem"
|
||||
class="mb-3"
|
||||
data-testid="CategoryItem-EcosystemSection"
|
||||
:editText="ecosystem.text"
|
||||
@save="updateEcosystem({original: ecosystem.text, modified: $event})"
|
||||
@toggleEdit="toggleItemEcosystemEditState"
|
||||
>
|
||||
<div v-html="ecosystem.html" />
|
||||
<div
|
||||
v-html="ecosystem.html"
|
||||
data-testid="CategoryItem-EcosystemSection-Content"
|
||||
/>
|
||||
</category-item-section>
|
||||
|
||||
<category-item-section
|
||||
v-if="isSectionEnabled('ItemNotesSection')"
|
||||
title="Notes"
|
||||
class="mb-3"
|
||||
data-testid="CategoryItem-NotesSection"
|
||||
:editText="notes.text"
|
||||
@save="updateNotes({original: notes.text, modified: $event})"
|
||||
@toggleEdit="toggleItemNotesEditState"
|
||||
@ -101,6 +109,7 @@
|
||||
<div
|
||||
v-if="notes.html"
|
||||
v-html="notes.html"
|
||||
data-testid="CategoryItem-NotesSection-Content"
|
||||
/>
|
||||
<span v-else> <notes are empty> </span>
|
||||
</div>
|
||||
@ -154,7 +163,7 @@ export default class CategoryItem extends Vue {
|
||||
|
||||
@Watch('isAnyTraitEditing', { immediate: true })
|
||||
updateItemTraitEditingState (newVal, prevVal) {
|
||||
if (newVal !== prevVal) {
|
||||
if (!!newVal !== !!prevVal) {
|
||||
this.$store.dispatch('category/toggleItemProsConsSectionEdit', this.itemUid)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="category-item-toolbar">
|
||||
<div
|
||||
class="category-item-toolbar"
|
||||
data-testid="CategoryItemToolbar"
|
||||
>
|
||||
<div class="category-item-toolbar__header">
|
||||
<v-toolbar
|
||||
color="#dedede"
|
||||
@ -17,14 +20,20 @@
|
||||
v-if="itemLink"
|
||||
class="category-item-name"
|
||||
target="_blank"
|
||||
data-testid="CategoryItemToolbar-ItemName"
|
||||
:href="itemLink"
|
||||
>{{ itemName }}</a>
|
||||
<span class="category-item-name" v-else>{{ itemName }}</span>
|
||||
<span
|
||||
v-else
|
||||
data-testid="CategoryItemToolbar-ItemName"
|
||||
class="category-item-name"
|
||||
>{{ itemName }}</span>
|
||||
<div class="category-item-badges">
|
||||
<a
|
||||
v-if="this.itemHackage"
|
||||
class="hackage-link"
|
||||
target="_blank"
|
||||
data-testid="CategoryItemToolbar-HackageLink"
|
||||
:href="`https://hackage.haskell.org/package/${this.itemHackage}`"
|
||||
>
|
||||
<v-icon
|
||||
@ -38,13 +47,14 @@
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-toolbar-items>
|
||||
<v-toolbar-items class="category-item__actions">
|
||||
<div class="category-item-toolbar-btns">
|
||||
<category-item-btn
|
||||
titleTooltip
|
||||
size="40px"
|
||||
iconSize="18"
|
||||
title="Move item up"
|
||||
data-testid="CategoryItemToolbar-MoveUpBtn"
|
||||
icon="arrow-up"
|
||||
@click="moveItem('up')"
|
||||
/>
|
||||
@ -53,6 +63,7 @@
|
||||
size="40px"
|
||||
iconSize="18"
|
||||
title="Move item down"
|
||||
data-testid="CategoryItemToolbar-MoveDownBtn"
|
||||
icon="arrow-down"
|
||||
@click="moveItem('down')"
|
||||
/>
|
||||
@ -61,6 +72,7 @@
|
||||
size="40px"
|
||||
iconSize="18"
|
||||
title="Edit item info"
|
||||
data-testid="CategoryItemToolbar-EditInfoBtn"
|
||||
icon="cog"
|
||||
@click="toggleEditItemInfoMenu"
|
||||
>
|
||||
@ -78,11 +90,17 @@
|
||||
iconSize="18"
|
||||
title="Delete item"
|
||||
icon="trash-alt"
|
||||
data-testid="CategoryItemToolbar-DeleteBtn"
|
||||
@click="deleteItem"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-menu bottom left offset-y>
|
||||
<v-menu
|
||||
bottom
|
||||
left
|
||||
offset-y
|
||||
attach=".category-item__actions"
|
||||
>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
icon
|
||||
@ -91,6 +109,7 @@
|
||||
aria-label="Actions"
|
||||
v-tooltip="'Actions'"
|
||||
class="category-toolbar-mobile-menu-btn"
|
||||
data-testid="CategoryItemToolbar-MobileMenuBtn"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon
|
||||
@ -109,6 +128,7 @@
|
||||
iconSize="18"
|
||||
title="Move item up"
|
||||
icon="arrow-up"
|
||||
data-testid="CategoryItemToolbar-MoveUpBtn"
|
||||
@click="moveItem('up')"
|
||||
/>
|
||||
</v-list-item>
|
||||
@ -120,6 +140,7 @@
|
||||
iconSize="18"
|
||||
title="Move item down"
|
||||
icon="arrow-down"
|
||||
data-testid="CategoryItemToolbar-MoveDownBtn"
|
||||
@click="moveItem('down')"
|
||||
/>
|
||||
</v-list-item>
|
||||
@ -131,6 +152,7 @@
|
||||
iconSize="18"
|
||||
title="Edit item info"
|
||||
icon="cog"
|
||||
data-testid="CategoryItemToolbar-EditInfoBtn"
|
||||
@click="toggleEditItemInfoMenu"
|
||||
>
|
||||
<v-icon
|
||||
@ -149,6 +171,7 @@
|
||||
iconSize="18"
|
||||
title="Delete item"
|
||||
icon="trash-alt"
|
||||
data-testid="CategoryItemToolbar-DeleteBtn"
|
||||
@click="deleteItem"
|
||||
/>
|
||||
</v-list-item>
|
||||
@ -171,14 +194,17 @@
|
||||
>
|
||||
<v-text-field
|
||||
v-model="itemNameEdit"
|
||||
data-testid="CategoryItemToolbar-InfoEdit-NameInput"
|
||||
label="Name"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="itemHackageEdit"
|
||||
data-testid="CategoryItemToolbar-InfoEdit-HackageInput"
|
||||
label="Name on Hackage (optional)"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="itemLinkEdit"
|
||||
data-testid="CategoryItemToolbar-InfoEdit-LinkInput"
|
||||
label="Site (optional)"
|
||||
/>
|
||||
</v-form>
|
||||
@ -195,6 +221,7 @@
|
||||
color="info"
|
||||
aria-label="Save"
|
||||
:disabled="!isInfoSaveEnabled"
|
||||
data-testid="CategoryItemToolbar-InfoEdit-SaveBtn"
|
||||
@click="updateItemInfo"
|
||||
>
|
||||
Save
|
||||
@ -286,7 +313,8 @@ export default class CategoryItemToolbar extends Vue {
|
||||
text: 'delete this item',
|
||||
confirmBtnText: 'Delete',
|
||||
confirmBtnProps: {
|
||||
color: 'error'
|
||||
'color': 'error',
|
||||
'data-testid': 'ItemDeleteDialog-ConfirmBtn'
|
||||
}
|
||||
})
|
||||
async deleteItem (): Promise<void> {
|
||||
|
@ -28,12 +28,14 @@
|
||||
autofocus
|
||||
class="mb-2"
|
||||
label="Title"
|
||||
data-testid="CategorySettings-TitleInput"
|
||||
:rules="inputValidationRules"
|
||||
v-model="title"
|
||||
/>
|
||||
<v-text-field
|
||||
class="mb-2"
|
||||
label="Group"
|
||||
data-testid="CategorySettings-GroupInput"
|
||||
:rules="inputValidationRules"
|
||||
v-model="group"
|
||||
/>
|
||||
@ -50,6 +52,7 @@
|
||||
hide-details
|
||||
color="info"
|
||||
class="category-settings-dialog__checkbox"
|
||||
data-testid="CategorySettings-ItemTraitsSectionCheckbox"
|
||||
label="Pros/cons section"
|
||||
value="ItemProsConsSection"
|
||||
:inputValue="sections"
|
||||
@ -59,6 +62,7 @@
|
||||
hide-details
|
||||
color="info"
|
||||
class="category-settings-dialog__checkbox"
|
||||
data-testid="CategorySettings-ItemEcosystemSectionCheckbox"
|
||||
label="Ecosystem section"
|
||||
value="ItemEcosystemSection"
|
||||
:inputValue="sections"
|
||||
@ -68,6 +72,7 @@
|
||||
hide-details
|
||||
color="info"
|
||||
class="category-settings-dialog__checkbox"
|
||||
data-testid="CategorySettings-ItemNotesSectionCheckbox"
|
||||
label="Notes section"
|
||||
value="ItemNotesSection"
|
||||
:inputValue="sections"
|
||||
@ -88,6 +93,7 @@
|
||||
<v-btn
|
||||
color="info"
|
||||
aria-label="Submit"
|
||||
data-testid="CategorySettings-SubmitBtn"
|
||||
:disabled="!isValid || !hasChanges"
|
||||
@click="updateCategoryInfo"
|
||||
>
|
||||
|
@ -14,7 +14,10 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div class="conflict-box">
|
||||
<div
|
||||
data-testid="ConflictDialog"
|
||||
class="conflict-box"
|
||||
>
|
||||
<div class="conflict-item">
|
||||
<h2 class="mt-0">Your version</h2>
|
||||
<v-card
|
||||
@ -25,6 +28,7 @@
|
||||
</v-card>
|
||||
<v-btn
|
||||
class="conflict-dialog-btn"
|
||||
data-testid="ConflictDialog-SubmitLocalBtn"
|
||||
aria-label="Submit this version, disregard changes on server"
|
||||
@click="save(modified)"
|
||||
>
|
||||
@ -41,6 +45,7 @@
|
||||
</v-card>
|
||||
<v-btn
|
||||
class="conflict-dialog-btn"
|
||||
data-testid="ConflictDialog-SubmitServerBtn"
|
||||
aria-label="Submit this version, disregard my changes"
|
||||
@click="save(serverModified)"
|
||||
>
|
||||
@ -60,6 +65,7 @@
|
||||
/>
|
||||
<v-btn
|
||||
class="conflict-dialog-btn"
|
||||
data-testid="ConflictDialog-SubmitMergedBtn"
|
||||
aria-label="Submit merged version"
|
||||
@click="save(mergedEdit)"
|
||||
>
|
||||
|
@ -13,7 +13,10 @@
|
||||
@keydown.exact.esc="cancel"
|
||||
v-show="editor && isReady"
|
||||
>
|
||||
<textarea ref="editor" />
|
||||
<textarea
|
||||
data-testid="MarkdownEditor-OriginalTextarea"
|
||||
ref="editor"
|
||||
/>
|
||||
|
||||
<v-toolbar
|
||||
flat
|
||||
@ -36,6 +39,7 @@
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn
|
||||
data-testid="MarkdownEditor-SaveBtn"
|
||||
color="info"
|
||||
aria-label="Save"
|
||||
@click="save"
|
||||
@ -110,6 +114,7 @@ export default class MarkdownEditor extends Vue {
|
||||
const EasyMDE = (await import('easymde')).default
|
||||
this.editor = new EasyMDE({
|
||||
element: this.$refs.editor,
|
||||
forceSync: true,
|
||||
autofocus: true,
|
||||
initialValue: this.value,
|
||||
spellChecker: false,
|
||||
|
3
front/client/helpers/categoryPathToId.js
Normal file
3
front/client/helpers/categoryPathToId.js
Normal file
@ -0,0 +1,3 @@
|
||||
export default function (path) {
|
||||
return path.split('#').shift().split('-').pop()
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import Router from 'vue-router'
|
||||
import categoryPathToId from 'client/helpers/categoryPathToId'
|
||||
|
||||
function createRouter (store) {
|
||||
return new Router({
|
||||
@ -31,7 +32,7 @@ function createRouter (store) {
|
||||
path: '/haskell/:category',
|
||||
name: 'Category',
|
||||
component: () => import('../page/CategoryPage.vue'),
|
||||
props: (route) => ({ categoryId: route.params.category.split('#').shift().split('-').pop() })
|
||||
props: (route) => ({ categoryId: categoryPathToId(route.params.category) })
|
||||
},
|
||||
{
|
||||
path: '/haskell/search/results/',
|
||||
|
@ -1,54 +0,0 @@
|
||||
import { Selector, ClientFunction } from 'testcafe'
|
||||
import VueSelector from 'testcafe-vue-selectors'
|
||||
|
||||
const baseUrl = `http://localhost:5000`
|
||||
const getLocation = ClientFunction(() => document.location.href)
|
||||
// !!! Testcafe-vue-selectors currently dont support vue cumponents loaded by vue-loader
|
||||
|
||||
fixture`Index`
|
||||
.page(baseUrl)
|
||||
|
||||
test('Navigate to category page', async t => {
|
||||
await t
|
||||
.click('.category-title')
|
||||
.expect(getLocation()).contains(`${baseUrl}/haskell/data-structures-fum5aqch`)
|
||||
})
|
||||
|
||||
test('Test search input', async inputSearch => {
|
||||
|
||||
const searchInput = Selector('input[aria-label="Search"]')
|
||||
|
||||
await inputSearch
|
||||
.typeText(searchInput, 'Haskell')
|
||||
.expect(searchInput.value).eql('Haskell')
|
||||
.pressKey('enter')
|
||||
.expect(getLocation()).eql(`${baseUrl}/haskell/search/results?query=Haskell`)
|
||||
})
|
||||
|
||||
test('Add category', async t => {
|
||||
const categoryGroups = Selector('.category-group')
|
||||
const groupsCount = await categoryGroups.count
|
||||
|
||||
if (!categoryGroups || !groupsCount) {
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < groupsCount; i++) {
|
||||
let currentGroup = categoryGroups.nth(i)
|
||||
let categoryGroupName = await currentGroup.child('.category-group-name').innerText
|
||||
let groupInput = Selector('input[aria-label="Group"]').value;
|
||||
|
||||
let addButton = currentGroup.child('.add-category-btn')
|
||||
|
||||
let newCategoryName = 'mytest-' + new Date().toISOString()
|
||||
await t.click(addButton)
|
||||
await t.expect(groupInput).eql(categoryGroupName)
|
||||
await t
|
||||
.typeText('input[aria-label="Category name"]', newCategoryName)
|
||||
.click('.add-category-submit-btn')
|
||||
// Cause after adding category it opens in new tab we need to navigate back, because TestCafe doest support opening new tabs
|
||||
// https://github.com/DevExpress/testcafe/issues/2293
|
||||
await t.navigateTo(`${baseUrl}`)
|
||||
await t.expect(currentGroup.find(node => node.innerText === newCategoryName).exists)
|
||||
}
|
||||
})
|
@ -1,44 +0,0 @@
|
||||
import { Selector } from 'testcafe'
|
||||
|
||||
fixture`ItemAddDelete`
|
||||
.page`http://localhost:5000/haskell/data-structures-fum5aqch`
|
||||
|
||||
const newItemName = 'mytest-' + new Date().toISOString()
|
||||
|
||||
test('Add New Item to category', async t => {
|
||||
const addItemBtn = Selector('.add-item-btn')
|
||||
const addItemInput = Selector('input[aria-label="Item name"]')
|
||||
|
||||
await t
|
||||
.click(addItemBtn)
|
||||
.typeText(addItemInput, newItemName)
|
||||
.pressKey('enter')
|
||||
|
||||
const articleHeadings = Selector('.article-hd-textlg')
|
||||
const articleHeadingsCount = await articleHeadings.count
|
||||
|
||||
// for (let i = 0; i < articleHeadingsCount; i++) {
|
||||
// await t.expect(Selector('.article-hd-textlg').nth(i).innerText).contains(newItemName)
|
||||
// }
|
||||
await t.expect(Selector('.article-hd-textlg').nth(articleHeadingsCount - 1).innerText).contains(newItemName)
|
||||
})
|
||||
|
||||
test('Delete Item from category', async t => {
|
||||
const delItemBtn = Selector(() => {
|
||||
const buttons = document.querySelectorAll('.item-del-btn')
|
||||
let last = buttons[buttons.length - 1]
|
||||
return last
|
||||
})
|
||||
|
||||
const delSubmitBtn = Selector('.confirm-btn')
|
||||
await t
|
||||
.click(delItemBtn)
|
||||
.click(delSubmitBtn)
|
||||
|
||||
const articleHeadings = Selector('.article-hd-textlg')
|
||||
const articleHeadingsCount = await articleHeadings.count
|
||||
|
||||
for (let i = 0; i < articleHeadingsCount; i++) {
|
||||
await t.expect(articleHeadings.nth(i).innerText).notContains(newItemName)
|
||||
}
|
||||
})
|
593
front/package-lock.json
generated
593
front/package-lock.json
generated
@ -977,12 +977,40 @@
|
||||
"glob-to-regexp": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz",
|
||||
"integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@nodelib/fs.stat": "2.0.3",
|
||||
"run-parallel": "^1.1.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz",
|
||||
"integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@nodelib/fs.stat": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
|
||||
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
|
||||
"dev": true
|
||||
},
|
||||
"@nodelib/fs.walk": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz",
|
||||
"integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@nodelib/fs.scandir": "2.1.3",
|
||||
"fastq": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"@types/error-stack-parser": {
|
||||
"version": "1.3.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/error-stack-parser/-/error-stack-parser-1.3.18.tgz",
|
||||
@ -1013,9 +1041,9 @@
|
||||
}
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.136",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.136.tgz",
|
||||
"integrity": "sha512-0GJhzBdvsW2RUccNHOBkabI8HZVdOXmXbXhuKlDEd5Vv12P7oAVGfomGp3Ne21o5D/qu1WmthlNKFaoZJJeErA==",
|
||||
"version": "4.14.144",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.144.tgz",
|
||||
"integrity": "sha512-ogI4g9W5qIQQUhXAclq6zhqgqNUr7UlFaqDHbch7WLSLeeM/7d3CRaw7GLajxvyFvhJqw4Rpcz5bhoaYtIx6Tg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/minimatch": {
|
||||
@ -1025,9 +1053,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "10.14.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.13.tgz",
|
||||
"integrity": "sha512-yN/FNNW1UYsRR1wwAoyOwqvDuLDtVXnaJTZ898XIw/Q5cCaeVAlVwvsmXLX5PuiScBYwZsZU4JYSHB3TvfdwvQ==",
|
||||
"version": "10.14.22",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.22.tgz",
|
||||
"integrity": "sha512-9taxKC944BqoTVjE+UT3pQH0nHZlTvITwfsOZqyc+R3sfJuxaTtxWjfn1K2UlxyPcKHf0rnaXcVFrS9F9vf0bw==",
|
||||
"dev": true
|
||||
},
|
||||
"@vue/component-compiler-utils": {
|
||||
@ -1360,14 +1388,32 @@
|
||||
"dev": true
|
||||
},
|
||||
"acorn-hammerhead": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-hammerhead/-/acorn-hammerhead-0.2.0.tgz",
|
||||
"integrity": "sha512-kbX1s/0ZikW0WEBY6IrooFgX3AP2D9ycTg0OhxRYLF0Tew/bDK2+8lTxFR4cDdoCZm6Ax8eVf8EV6gbTtr8EYQ==",
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-hammerhead/-/acorn-hammerhead-0.3.0.tgz",
|
||||
"integrity": "sha512-Izrr9mXONhWc7q8fqUe6ijQy+KjmyQlgdWARgaCVjds+nPpoSS298FY8uSVN/to8nKVTtkJpafNUlACWxwZS5w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/estree": "^0.0.39"
|
||||
}
|
||||
},
|
||||
"aggregate-error": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz",
|
||||
"integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"clean-stack": "^2.0.0",
|
||||
"indent-string": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"indent-string": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.10.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
|
||||
@ -1532,6 +1578,21 @@
|
||||
"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
|
||||
"dev": true
|
||||
},
|
||||
"asar": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/asar/-/asar-2.0.1.tgz",
|
||||
"integrity": "sha512-Vo9yTuUtyFahkVMFaI6uMuX6N7k5DWa6a/8+7ov0/f8Lq9TVR0tUjzSzxQSxT1Y+RJIZgnP7BVb6Uhi+9cjxqA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chromium-pickle-js": "^0.2.0",
|
||||
"commander": "^2.20.0",
|
||||
"cuint": "^0.2.2",
|
||||
"glob": "^7.1.3",
|
||||
"minimatch": "^3.0.4",
|
||||
"mkdirp": "^0.5.1",
|
||||
"tmp-promise": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
||||
@ -3269,6 +3330,12 @@
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"chromium-pickle-js": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz",
|
||||
"integrity": "sha1-BKEGZywYsIWrd02YPfo+oTjyIgU=",
|
||||
"dev": true
|
||||
},
|
||||
"ci-info": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz",
|
||||
@ -3308,6 +3375,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"clean-stack": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
||||
"dev": true
|
||||
},
|
||||
"cli-boxes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz",
|
||||
@ -3315,29 +3388,60 @@
|
||||
"dev": true
|
||||
},
|
||||
"cliui": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
|
||||
"integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
|
||||
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"string-width": "^2.1.1",
|
||||
"strip-ansi": "^4.0.0",
|
||||
"wrap-ansi": "^2.0.0"
|
||||
"string-width": "^3.1.0",
|
||||
"strip-ansi": "^5.2.0",
|
||||
"wrap-ansi": "^5.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
|
||||
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
|
||||
"dev": true
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
|
||||
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
|
||||
"ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^3.0.0"
|
||||
"color-convert": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"string-width": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
|
||||
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"emoji-regex": "^7.0.1",
|
||||
"is-fullwidth-code-point": "^2.0.0",
|
||||
"strip-ansi": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
|
||||
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
|
||||
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^3.2.0",
|
||||
"string-width": "^3.0.0",
|
||||
"strip-ansi": "^5.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3521,6 +3625,12 @@
|
||||
"yargs": "^12.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
|
||||
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
@ -3552,6 +3662,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"cliui": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
|
||||
"integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"string-width": "^2.1.1",
|
||||
"strip-ansi": "^4.0.0",
|
||||
"wrap-ansi": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"get-caller-file": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
|
||||
"integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
|
||||
"dev": true
|
||||
},
|
||||
"require-main-filename": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
|
||||
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
|
||||
"dev": true
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
|
||||
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz",
|
||||
@ -3568,6 +3710,36 @@
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"yargs": {
|
||||
"version": "12.0.5",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",
|
||||
"integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cliui": "^4.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^3.0.0",
|
||||
"get-caller-file": "^1.0.1",
|
||||
"os-locale": "^3.0.0",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^1.0.1",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^2.0.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^3.2.1 || ^4.0.0",
|
||||
"yargs-parser": "^11.1.1"
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz",
|
||||
"integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -3802,9 +3974,9 @@
|
||||
}
|
||||
},
|
||||
"core-js": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz",
|
||||
"integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==",
|
||||
"version": "2.6.10",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.10.tgz",
|
||||
"integrity": "sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA==",
|
||||
"dev": true
|
||||
},
|
||||
"core-js-compat": {
|
||||
@ -4063,6 +4235,12 @@
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true
|
||||
},
|
||||
"cuint": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
|
||||
"integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=",
|
||||
"dev": true
|
||||
},
|
||||
"currently-unhandled": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
|
||||
@ -4602,9 +4780,9 @@
|
||||
}
|
||||
},
|
||||
"esotope-hammerhead": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/esotope-hammerhead/-/esotope-hammerhead-0.3.0.tgz",
|
||||
"integrity": "sha512-qI6ZlQaf4yBPZhHETT24vk93INsNj++mRKBHOpOtkx7F/N7UvuTD8neeTbcbRQUntGksvs8/v63uGfDCdgt5YQ==",
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/esotope-hammerhead/-/esotope-hammerhead-0.4.0.tgz",
|
||||
"integrity": "sha512-TAmc7OhAiWeovbzE9GGenU2vwuB9tzKHlW0hTH4rZsLmCNEKo8wIZ9qbEnw8nyXeNTjCmBYOYXUyaN+eZht7Tg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/estree": "^0.0.39"
|
||||
@ -4841,6 +5019,15 @@
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
|
||||
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
|
||||
},
|
||||
"fastq": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz",
|
||||
"integrity": "sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"reusify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"fibers": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fibers/-/fibers-4.0.1.tgz",
|
||||
@ -5715,9 +5902,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"get-caller-file": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
|
||||
"integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true
|
||||
},
|
||||
"get-func-name": {
|
||||
@ -7309,9 +7496,9 @@
|
||||
}
|
||||
},
|
||||
"merge2": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.3.tgz",
|
||||
"integrity": "sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz",
|
||||
"integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==",
|
||||
"dev": true
|
||||
},
|
||||
"microevent.ts": {
|
||||
@ -7480,7 +7667,8 @@
|
||||
"moment": {
|
||||
"version": "2.24.0",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
|
||||
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
|
||||
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==",
|
||||
"dev": true
|
||||
},
|
||||
"moment-duration-format-commonjs": {
|
||||
"version": "1.0.0",
|
||||
@ -9450,9 +9638,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"require-main-filename": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
|
||||
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"dev": true
|
||||
},
|
||||
"resolve": {
|
||||
@ -9553,6 +9741,12 @@
|
||||
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
|
||||
"dev": true
|
||||
},
|
||||
"reusify": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||
"dev": true
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
|
||||
@ -9572,6 +9766,12 @@
|
||||
"inherits": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"run-parallel": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz",
|
||||
"integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"run-queue": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
|
||||
@ -9610,9 +9810,9 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"sanitize-filename": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.1.tgz",
|
||||
"integrity": "sha1-YS2hyWRz+gLczaktzVtKsWSmdyo=",
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
|
||||
"integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"truncate-utf8-bytes": "^1.0.0"
|
||||
@ -10335,9 +10535,9 @@
|
||||
}
|
||||
},
|
||||
"testcafe": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/testcafe/-/testcafe-1.3.3.tgz",
|
||||
"integrity": "sha512-UdFPD3+IW8SgCM97VAOasIFBqpsy70cDp+Sw+Cq2QAk9IeeJPGct4A7Eealdyhota7EmLTqXnlwXIZd0Jk+xbw==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/testcafe/-/testcafe-1.6.0.tgz",
|
||||
"integrity": "sha512-jlydNbQ6m/LdM6o40EzDwMXBKWV8evZV2Xa1YzzJ9r6H58Y4FHpeYjDtv5gDaBkpV7NslDFXoOkpwnZgRvAjcw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "^10.12.19",
|
||||
@ -10380,6 +10580,7 @@
|
||||
"log-update-async-hook": "^2.0.2",
|
||||
"make-dir": "^1.3.0",
|
||||
"map-reverse": "^1.0.1",
|
||||
"mime-db": "^1.41.0",
|
||||
"moment": "^2.10.3",
|
||||
"moment-duration-format-commonjs": "^1.0.0",
|
||||
"mustache": "^2.1.2",
|
||||
@ -10399,8 +10600,8 @@
|
||||
"sanitize-filename": "^1.6.0",
|
||||
"source-map-support": "^0.5.5",
|
||||
"strip-bom": "^2.0.0",
|
||||
"testcafe-browser-tools": "1.6.8",
|
||||
"testcafe-hammerhead": "14.6.13",
|
||||
"testcafe-browser-tools": "1.7.0",
|
||||
"testcafe-hammerhead": "14.9.2",
|
||||
"testcafe-legacy-api": "3.1.11",
|
||||
"testcafe-reporter-json": "^2.1.0",
|
||||
"testcafe-reporter-list": "^2.1.0",
|
||||
@ -10470,6 +10671,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.42.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz",
|
||||
"integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==",
|
||||
"dev": true
|
||||
},
|
||||
"pify": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
@ -10491,17 +10698,19 @@
|
||||
}
|
||||
},
|
||||
"testcafe-browser-tools": {
|
||||
"version": "1.6.8",
|
||||
"resolved": "https://registry.npmjs.org/testcafe-browser-tools/-/testcafe-browser-tools-1.6.8.tgz",
|
||||
"integrity": "sha512-xFgwmcAOutSJR6goqO8uUFGF5IF2xRC/Ssh4pB5QZ+bTjYsN5amnjgM+813bDBLelC+HmXKqylviz7Dzxbtbcw==",
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/testcafe-browser-tools/-/testcafe-browser-tools-1.7.0.tgz",
|
||||
"integrity": "sha512-85CabhVhxrVriOCqwm5rGLA5LQ/tzuMYhPPmLE0eZhHHHX+qh1a8vbDfwJIn2TFgX0bNq9ZmCPV8RQfU8P0UAQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-find": "^1.0.0",
|
||||
"babel-runtime": "^5.6.15",
|
||||
"del": "^5.1.0",
|
||||
"graceful-fs": "^4.1.11",
|
||||
"linux-platform-info": "^0.0.3",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mustache": "^2.1.2",
|
||||
"nanoid": "^2.1.3",
|
||||
"os-family": "^1.0.0",
|
||||
"pify": "^2.3.0",
|
||||
"pinkie": "^2.0.1",
|
||||
@ -10509,6 +10718,18 @@
|
||||
"which-promise": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz",
|
||||
"integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==",
|
||||
"dev": true
|
||||
},
|
||||
"array-union": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||
"dev": true
|
||||
},
|
||||
"babel-runtime": {
|
||||
"version": "5.8.38",
|
||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-5.8.38.tgz",
|
||||
@ -10518,32 +10739,201 @@
|
||||
"core-js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"core-js": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
|
||||
"integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=",
|
||||
"dev": true
|
||||
},
|
||||
"del": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/del/-/del-5.1.0.tgz",
|
||||
"integrity": "sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"globby": "^10.0.1",
|
||||
"graceful-fs": "^4.2.2",
|
||||
"is-glob": "^4.0.1",
|
||||
"is-path-cwd": "^2.2.0",
|
||||
"is-path-inside": "^3.0.1",
|
||||
"p-map": "^3.0.0",
|
||||
"rimraf": "^3.0.0",
|
||||
"slash": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"graceful-fs": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz",
|
||||
"integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"path-type": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.1.0.tgz",
|
||||
"integrity": "sha512-TrUz3THiq2Vy3bjfQUB2wNyPdGBeGmdjbzzBLhfHN4YFurYptCKwGq/TfiRavbGywFRzY6U2CdmQ1zmsY5yYaw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"glob-parent": "^5.1.0",
|
||||
"merge2": "^1.3.0",
|
||||
"micromatch": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz",
|
||||
"integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"globby": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz",
|
||||
"integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/glob": "^7.1.1",
|
||||
"array-union": "^2.1.0",
|
||||
"dir-glob": "^3.0.1",
|
||||
"fast-glob": "^3.0.3",
|
||||
"glob": "^7.1.3",
|
||||
"ignore": "^5.1.1",
|
||||
"merge2": "^1.2.3",
|
||||
"slash": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"ignore": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz",
|
||||
"integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==",
|
||||
"dev": true
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true
|
||||
},
|
||||
"is-path-cwd": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
|
||||
"integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-path-inside": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz",
|
||||
"integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==",
|
||||
"dev": true
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
|
||||
"integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"braces": "^3.0.1",
|
||||
"picomatch": "^2.0.5"
|
||||
}
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.4.tgz",
|
||||
"integrity": "sha512-PijW88Ry+swMFfArOrm7uRAdVmJilLbej7WwVY6L5QwLDckqxSOinGGMV596yp5C8+MH3VvCXCSZ6AodGtKrYQ==",
|
||||
"dev": true
|
||||
},
|
||||
"p-map": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
|
||||
"integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"aggregate-error": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"path-type": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||
"dev": true
|
||||
},
|
||||
"pify": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
|
||||
"dev": true
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz",
|
||||
"integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"testcafe-hammerhead": {
|
||||
"version": "14.6.13",
|
||||
"resolved": "https://registry.npmjs.org/testcafe-hammerhead/-/testcafe-hammerhead-14.6.13.tgz",
|
||||
"integrity": "sha512-uwIZ3sSQh2LdgbLjCM2z7jmVfR/EZ63WIId0C34FxaOd5E00+V1NdVWHofYYJvr8MBec9X8iRRGtir4nYU9H2g==",
|
||||
"version": "14.9.2",
|
||||
"resolved": "https://registry.npmjs.org/testcafe-hammerhead/-/testcafe-hammerhead-14.9.2.tgz",
|
||||
"integrity": "sha512-0rO9NTTueDXPqeWASThKEHX5AGF0FDhiPVRdkkWnKUOAtfPJz/qovZnIEpoC7q0DdmgbYYvK3if9Si8SdFNk0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"acorn-hammerhead": "^0.2.0",
|
||||
"acorn-hammerhead": "^0.3.0",
|
||||
"asar": "^2.0.1",
|
||||
"bowser": "1.6.0",
|
||||
"brotli": "^1.3.1",
|
||||
"crypto-md5": "^1.0.0",
|
||||
"css": "2.2.3",
|
||||
"esotope-hammerhead": "0.3.0",
|
||||
"esotope-hammerhead": "^0.4.0",
|
||||
"iconv-lite": "0.4.11",
|
||||
"lodash": "^4.17.13",
|
||||
"lru-cache": "2.6.3",
|
||||
@ -10554,7 +10944,6 @@
|
||||
"nanoid": "^0.2.2",
|
||||
"os-family": "^1.0.0",
|
||||
"parse5": "2.2.3",
|
||||
"pify": "^2.3.0",
|
||||
"pinkie": "1.0.0",
|
||||
"read-file-relative": "^1.2.0",
|
||||
"semver": "5.5.0",
|
||||
@ -10593,12 +10982,6 @@
|
||||
"integrity": "sha1-DE/EHBAAxea5PUiwP4CDg3g06fY=",
|
||||
"dev": true
|
||||
},
|
||||
"pify": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
|
||||
"dev": true
|
||||
},
|
||||
"pinkie": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-1.0.0.tgz",
|
||||
@ -10714,12 +11097,6 @@
|
||||
"integrity": "sha1-5tZsVyzhWvJmcGrw/WELKoQd1EM=",
|
||||
"dev": true
|
||||
},
|
||||
"testcafe-vue-selectors": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/testcafe-vue-selectors/-/testcafe-vue-selectors-3.1.0.tgz",
|
||||
"integrity": "sha512-BdJHD7Stns7ZXdQ1arahp2hcwkZ3u6Q2npYQgFMYHSyWDyExUdNSsYUmXg4EU7MrVXkAv+sE3eTvNmH1XeyrzA==",
|
||||
"dev": true
|
||||
},
|
||||
"text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@ -10814,6 +11191,27 @@
|
||||
"os-tmpdir": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"tmp-promise": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-1.1.0.tgz",
|
||||
"integrity": "sha512-8+Ah9aB1IRXCnIOxXZ0uFozV1nMU5xiu7hhFVUSxZ3bYu+psD4TzagCzVbexUCgNNGJnsmNDQlS4nG3mTyoNkw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bluebird": "^3.5.0",
|
||||
"tmp": "0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tmp": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz",
|
||||
"integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"rimraf": "^2.6.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"to-arraybuffer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
|
||||
@ -11239,6 +11637,12 @@
|
||||
"integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
|
||||
"dev": true
|
||||
},
|
||||
"uniqid": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/uniqid/-/uniqid-5.0.3.tgz",
|
||||
"integrity": "sha512-R2qx3X/LYWSdGRaluio4dYrPXAJACTqyUjuyXHoJLBUOIfmMcnYOyY2d6Y4clZcIz5lK6ZaI0Zzmm0cPfsIqzQ==",
|
||||
"dev": true
|
||||
},
|
||||
"unique-filename": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
|
||||
@ -12240,29 +12644,56 @@
|
||||
"dev": true
|
||||
},
|
||||
"yargs": {
|
||||
"version": "12.0.5",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",
|
||||
"integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==",
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-14.0.0.tgz",
|
||||
"integrity": "sha512-ssa5JuRjMeZEUjg7bEL99AwpitxU/zWGAGpdj0di41pOEmJti8NR6kyUIJBkR78DTYNPZOU08luUo0GTHuB+ow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cliui": "^4.0.0",
|
||||
"cliui": "^5.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^3.0.0",
|
||||
"get-caller-file": "^1.0.1",
|
||||
"os-locale": "^3.0.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^1.0.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^2.0.0",
|
||||
"string-width": "^3.0.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^3.2.1 || ^4.0.0",
|
||||
"yargs-parser": "^11.1.1"
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^13.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
|
||||
"dev": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
|
||||
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"emoji-regex": "^7.0.1",
|
||||
"is-fullwidth-code-point": "^2.0.0",
|
||||
"strip-ansi": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
|
||||
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^4.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz",
|
||||
"integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==",
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz",
|
||||
"integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"camelcase": "^5.0.0",
|
||||
|
@ -9,7 +9,11 @@
|
||||
"build:src": "concurrently \"npm run build:client\" \"npm run build:server\"",
|
||||
"build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.conf.js --hide-modules",
|
||||
"build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.conf.js --hide-modules",
|
||||
"test": "testcafe chrome client/tests/*.test.js --app \"npm run start:dev\" --app-init-delay 10000"
|
||||
"test": "ts-node -O {\\\"module\\\":\\\"commonjs\\\"} ./tests/index.ts",
|
||||
"test:dev": "cross-env NODE_ENV=development npm run test",
|
||||
"test:prod": "cross-env NODE_ENV=production npm run test",
|
||||
"test:dev:mobile": "cross-env NODE_ENV=development npm run test -- --mobile",
|
||||
"test:prod:mobile": "cross-env NODE_ENV=production npm run test -- --mobile"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ext": "ts",
|
||||
@ -40,7 +44,6 @@
|
||||
"koa-proxy": "^0.9.0",
|
||||
"koa-static": "^5.0.0",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.24.0",
|
||||
"normalize-url": "^4.3.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"v-tooltip": "^2.0.2",
|
||||
@ -55,7 +58,6 @@
|
||||
"vuex-router-sync": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"fibers": "^4.0.1",
|
||||
"@babel/core": "^7.5.5",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/plugin-transform-runtime": "^7.5.5",
|
||||
@ -69,6 +71,7 @@
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^3.1.0",
|
||||
"deepmerge": "^4.0.0",
|
||||
"fibers": "^4.0.1",
|
||||
"file-loader": "^4.1.0",
|
||||
"fork-ts-checker-webpack-plugin": "^1.4.3",
|
||||
"friendly-errors-webpack-plugin": "^1.7.0",
|
||||
@ -81,12 +84,12 @@
|
||||
"rimraf": "^2.6.3",
|
||||
"sass": "^1.22.9",
|
||||
"sass-loader": "^7.1.0",
|
||||
"testcafe": "^1.3.3",
|
||||
"testcafe-vue-selectors": "^3.1.0",
|
||||
"testcafe": "^1.6.0",
|
||||
"ts-loader": "^6.0.4",
|
||||
"ts-node": "^8.3.0",
|
||||
"tslint": "^5.18.0",
|
||||
"typescript": "^3.5.3",
|
||||
"uniqid": "^5.0.3",
|
||||
"url-loader": "^2.1.0",
|
||||
"vue-loader": "^15.7.1",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
@ -96,6 +99,7 @@
|
||||
"webpack-cli": "^3.3.6",
|
||||
"webpack-merge": "^4.2.1",
|
||||
"webpack-node-externals": "^1.7.2",
|
||||
"webpackbar": "^3.2.0"
|
||||
"webpackbar": "^3.2.0",
|
||||
"yargs": "^14.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,34 +1,41 @@
|
||||
import Koa from 'koa'
|
||||
import bodyparser from 'koa-bodyparser'
|
||||
import proxy from 'koa-proxy'
|
||||
import DefferedPromise from './utils/DeferredPromise'
|
||||
import config from './config.js'
|
||||
|
||||
const { port, apiUrl } = config
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
export default (async () => {
|
||||
const promise = new DefferedPromise()
|
||||
try {
|
||||
const { port, apiUrl } = config
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
async function start () {
|
||||
const app = new Koa()
|
||||
const app = new Koa()
|
||||
|
||||
// TODO replace proxy lib or write own middleware for log and flexibility
|
||||
app.use(proxy({
|
||||
requestOptions: {
|
||||
strictSSL: false
|
||||
},
|
||||
host: apiUrl,
|
||||
match: /^\/api\//,
|
||||
map: (path: string) => path.replace('/api', '')
|
||||
}))
|
||||
app.use(bodyparser())
|
||||
// TODO replace proxy lib or write own middleware for log and flexibility
|
||||
app.use(proxy({
|
||||
requestOptions: {
|
||||
strictSSL: false
|
||||
},
|
||||
host: apiUrl,
|
||||
match: /^\/api\//,
|
||||
map: (path: string) => path.replace('/api', '')
|
||||
}))
|
||||
app.use(bodyparser())
|
||||
|
||||
const setupServer = isProduction
|
||||
? (await import('./build/setupProdServer')).default
|
||||
: (await import('./build/setupDevServer')).default
|
||||
const setupServer = isProduction
|
||||
? (await import('./build/setupProdServer')).default
|
||||
: (await import('./build/setupDevServer')).default
|
||||
|
||||
await setupServer(app)
|
||||
await setupServer(app)
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`[Info] Server is on at ${port}.`)
|
||||
})
|
||||
}
|
||||
app.listen(port, () => {
|
||||
console.log(`[Info] Server is on at ${port}.`)
|
||||
promise.resolve()
|
||||
})
|
||||
|
||||
start()
|
||||
return promise
|
||||
} catch (e) {
|
||||
promise.reject(e)
|
||||
}
|
||||
})()
|
||||
|
8
front/tests/config.tests.js
Normal file
8
front/tests/config.tests.js
Normal file
@ -0,0 +1,8 @@
|
||||
import config from '../config.js'
|
||||
|
||||
const baseUrl = `http://localhost:${config.port}`
|
||||
|
||||
export default {
|
||||
...config,
|
||||
baseUrl
|
||||
}
|
27
front/tests/index.ts
Normal file
27
front/tests/index.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import createTestCafe from 'testcafe'
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const startingServer = isProduction
|
||||
// @ts-ignore cause typescript throws compilation error if can't find the file
|
||||
? (await import('../dist/server'))
|
||||
: (await import('../server'))
|
||||
|
||||
await startingServer.default
|
||||
const testcafe = await createTestCafe('localhost')
|
||||
|
||||
const failedCount = await testcafe
|
||||
.createRunner()
|
||||
.src('tests/tests.js')
|
||||
.browsers('chrome')
|
||||
.run()
|
||||
|
||||
testcafe.close()
|
||||
const exitCode = failedCount ? 1 : 0
|
||||
process.exit(exitCode)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
73
front/tests/models.js
Normal file
73
front/tests/models.js
Normal file
@ -0,0 +1,73 @@
|
||||
import { sel, testIdAttribute } from './utils'
|
||||
|
||||
class CategoryItemModel {
|
||||
constructor (t, { name, hackage, link }) {
|
||||
this.t = t
|
||||
this.name = name
|
||||
this.hackage = hackage
|
||||
this.linkValue = link
|
||||
}
|
||||
get nameTitle () {
|
||||
return sel('CategoryItemToolbar-ItemName').withText(this.name)
|
||||
}
|
||||
get toolbar () {
|
||||
return this.nameTitle.parent(testIdAttribute('CategoryItemToolbar'))
|
||||
}
|
||||
get linkHref () {
|
||||
return this.nameTitle.getAttribute('href')
|
||||
}
|
||||
get hackageLink () {
|
||||
return this.toolbar.find(testIdAttribute('CategoryItemToolbar-HackageLink'))
|
||||
}
|
||||
get hackageLinkHref () {
|
||||
return this.hackageLink.getAttribute('href')
|
||||
}
|
||||
get mobileMenuBtn () {
|
||||
return this.toolbar.find(testIdAttribute('CategoryItemToolbar-MobileMenuBtn'))
|
||||
}
|
||||
get moveDownBtn () {
|
||||
return this.toolbar.find(testIdAttribute('CategoryItemToolbar-MoveDownBtn'))
|
||||
}
|
||||
get moveUpBtn () {
|
||||
return this.toolbar.find(testIdAttribute('CategoryItemToolbar-MoveUpBtn'))
|
||||
}
|
||||
get editInfoBtn () {
|
||||
return this.toolbar.find(testIdAttribute('CategoryItemToolbar-EditInfoBtn'))
|
||||
}
|
||||
get deleteBtn () {
|
||||
return this.toolbar.find(testIdAttribute('CategoryItemToolbar-DeleteBtn'))
|
||||
}
|
||||
|
||||
async openMobileActionsMenu () {
|
||||
const isMobile = await this.mobileMenuBtn.visible
|
||||
if (isMobile) {
|
||||
await this.t.click(this.mobileMenuBtn)
|
||||
}
|
||||
}
|
||||
|
||||
async clickActionBtn (btn) {
|
||||
await this.openMobileActionsMenu()
|
||||
await this.t.click(btn.filterVisible())
|
||||
}
|
||||
|
||||
async moveUp () {
|
||||
await this.clickActionBtn(this.moveUpBtn)
|
||||
}
|
||||
|
||||
async moveDown () {
|
||||
await this.clickActionBtn(this.moveDownBtn)
|
||||
}
|
||||
|
||||
async delete () {
|
||||
await this.clickActionBtn(this.deleteBtn)
|
||||
await this.t.click(sel('ItemDeleteDialog-ConfirmBtn'))
|
||||
}
|
||||
|
||||
async toggleInfoEdit () {
|
||||
await this.clickActionBtn(this.editInfoBtn)
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
CategoryItemModel
|
||||
}
|
301
front/tests/testFunctions.js
Normal file
301
front/tests/testFunctions.js
Normal file
@ -0,0 +1,301 @@
|
||||
import uniqid from 'uniqid'
|
||||
import axios from 'axios'
|
||||
import yargs from 'yargs'
|
||||
import categoryPathToId from '../client/helpers/categoryPathToId'
|
||||
import config from './config.tests'
|
||||
import { sel, testIdAttribute, getLocation, goBack } from './utils'
|
||||
import { CategoryItemModel } from './models'
|
||||
|
||||
const baseUrl = config.baseUrl
|
||||
|
||||
async function createCategory (t, { categoryName, groupName } = {}) {
|
||||
const createCategoryBtn = sel('CreateCategoryBtn').nth(0)
|
||||
const nameInput = sel('AddCategoryDialog-NameInput')
|
||||
const groupInput = sel('AddCategoryDialog-GroupInput')
|
||||
const submitBtn = sel('AddCategoryDialog-SubmitBtn')
|
||||
|
||||
categoryName = categoryName || `testCategory-${uniqid()}`
|
||||
groupName = groupName || `testGroup-${uniqid()}`
|
||||
|
||||
await t
|
||||
.click(createCategoryBtn)
|
||||
.typeText(nameInput, categoryName, { replace: true })
|
||||
.typeText(groupInput, groupName, { replace: true })
|
||||
.click(submitBtn)
|
||||
|
||||
// wait for redirection to category page happens
|
||||
const redirectionWaitTime = 2500
|
||||
await t.wait(redirectionWaitTime)
|
||||
const categoryParams = { categoryName, groupName }
|
||||
const duplicateCategoryDialog = sel('DuplicateCategoryDialog', { timeout: 2000 })
|
||||
const isDuplicateCategory = await duplicateCategoryDialog() && await duplicateCategoryDialog.exists && await duplicateCategoryDialog.visible
|
||||
if (isDuplicateCategory) {
|
||||
await t.click(sel('DuplicateCategoryDialog-ConfirmBtn'))
|
||||
await t.wait(redirectionWaitTime)
|
||||
}
|
||||
|
||||
return categoryParams
|
||||
}
|
||||
|
||||
async function clickBtnIfVisible (t, btnSelector) {
|
||||
const btn = sel(btnSelector)
|
||||
const isVisible = await btn.visible
|
||||
|
||||
if (isVisible) {
|
||||
await t.click(btn)
|
||||
}
|
||||
}
|
||||
|
||||
async function openCategoryMobileActionsMenu (t) {
|
||||
await clickBtnIfVisible(t, 'CategoryHeader-MobileMenuBtn')
|
||||
}
|
||||
|
||||
async function createItem (t, { name, hackage, link } = {}) {
|
||||
name = name || `testItem-${uniqid()}`
|
||||
hackage = hackage === false
|
||||
? hackage
|
||||
: hackage || `${uniqid()}`
|
||||
link = link === false
|
||||
? link
|
||||
: link || `https://aelve.com/${uniqid()}`
|
||||
|
||||
const typeOptions = { replace: true }
|
||||
|
||||
await openCategoryMobileActionsMenu(t)
|
||||
|
||||
await t
|
||||
.click(sel('CategoryHeader-NewItemBtn'))
|
||||
.typeText(sel('AddItemDialog-NameInput'), name, typeOptions)
|
||||
if (hackage) {
|
||||
await t.typeText(sel('AddItemDialog-HackageInput'), hackage, typeOptions)
|
||||
}
|
||||
if (link) {
|
||||
await t.typeText(sel('AddItemDialog-LinkInput'), link, typeOptions)
|
||||
}
|
||||
await t.click(sel('AddItemDialog-SubmitBtn'))
|
||||
|
||||
return new CategoryItemModel(t, { name, hackage, link })
|
||||
}
|
||||
|
||||
const testFunctions = {
|
||||
async resizeWindowIfMobile (t) {
|
||||
const isMobile = yargs.argv.mobile
|
||||
|
||||
if (isMobile) {
|
||||
const mobileWidth = 320
|
||||
const mobileHeight = 568
|
||||
await t.resizeWindow(mobileWidth, mobileHeight)
|
||||
}
|
||||
},
|
||||
|
||||
async createCategoryAndDuplicate (t) {
|
||||
const { groupName, categoryName } = await createCategory(t)
|
||||
// after creating category it automatically navigates to category page
|
||||
await goBack()
|
||||
await createCategory(t, { categoryName, groupName })
|
||||
await goBack()
|
||||
|
||||
const groupTitle = sel('Categories-CategoryGroup-Title').withText(groupName)
|
||||
const group = groupTitle.parent(testIdAttribute('Categories-CategoryGroup'))
|
||||
|
||||
const numberOfCategories = group
|
||||
.find(testIdAttribute('Categories-CategoryTitle'))
|
||||
.withText(categoryName)
|
||||
.count
|
||||
await t
|
||||
.expect(numberOfCategories)
|
||||
.eql(2)
|
||||
},
|
||||
|
||||
async editCategory (t) {
|
||||
const { categoryName } = await createCategory(t)
|
||||
const item = await createItem(t)
|
||||
|
||||
const traitsSection = sel('CategoryItem-TraitsSection')
|
||||
const ecosystemSection = sel('CategoryItem-EcosystemSection')
|
||||
const notesSection = sel('CategoryItem-NotesSection')
|
||||
|
||||
await t
|
||||
.expect(item.nameTitle.exists).ok()
|
||||
.expect(traitsSection.exists).ok()
|
||||
.expect(ecosystemSection.exists).ok()
|
||||
.expect(notesSection.exists).ok()
|
||||
|
||||
await openCategoryMobileActionsMenu(t)
|
||||
await t.click(sel('CategoryHeader-CategorySettingsBtn'))
|
||||
|
||||
const newTitle = categoryName + 'a'
|
||||
const newGroup = `group-${uniqid()}`
|
||||
await t
|
||||
.typeText(sel('CategorySettings-TitleInput'), newTitle, { replace: true })
|
||||
.typeText(sel('CategorySettings-GroupInput'), newGroup, { replace: true })
|
||||
.click(sel('CategorySettings-ItemTraitsSectionCheckbox'))
|
||||
.click(sel('CategorySettings-ItemEcosystemSectionCheckbox'))
|
||||
.click(sel('CategorySettings-ItemNotesSectionCheckbox'))
|
||||
.click(sel('CategorySettings-SubmitBtn'))
|
||||
.expect(sel('CategoryHeader-Title').innerText).eql(newTitle)
|
||||
.expect(sel('CategoryHeader-Group').innerText).eql(newGroup)
|
||||
.expect(traitsSection.exists).notOk()
|
||||
.expect(ecosystemSection.exists).notOk()
|
||||
.expect(notesSection.exists).notOk()
|
||||
// TODO check status changing
|
||||
},
|
||||
|
||||
async deleteCategory (t) {
|
||||
const { categoryName } = await createCategory(t)
|
||||
await goBack()
|
||||
|
||||
const createdCategoryTitle = sel('Categories-CategoryTitle').withText(categoryName)
|
||||
const categoryDeleteBtn = sel('CategoryHeader-CategoryDeleteBtn')
|
||||
const submitCategoryDeleteBtn = sel('DeleteCategoryDialog-ConfirmBtn')
|
||||
await t
|
||||
.expect(createdCategoryTitle.exists).ok()
|
||||
.click(createdCategoryTitle)
|
||||
await openCategoryMobileActionsMenu(t)
|
||||
await t
|
||||
.click(categoryDeleteBtn)
|
||||
.click(submitCategoryDeleteBtn)
|
||||
.navigateTo(baseUrl)
|
||||
.expect(createdCategoryTitle.exists).notOk()
|
||||
},
|
||||
|
||||
async createItemWithOptionalParams (t) {
|
||||
await createCategory(t)
|
||||
const item = await createItem(t)
|
||||
|
||||
await t
|
||||
.expect(item.nameTitle.exists).ok()
|
||||
.expect(item.linkHref).eql(item.linkValue)
|
||||
.expect(item.hackageLinkHref).contains(item.hackage)
|
||||
},
|
||||
|
||||
async createItemNoOptionalParams (t) {
|
||||
await createCategory(t)
|
||||
const item = await createItem(t, { hackage: false, link: false })
|
||||
|
||||
await t
|
||||
.expect(item.nameTitle.exists).ok()
|
||||
.expect(item.linkHref).eql(undefined)
|
||||
.expect(item.hackageLink.exists).notOk()
|
||||
},
|
||||
|
||||
async deleteItem (t) {
|
||||
await createCategory(t)
|
||||
const item = await createItem(t)
|
||||
|
||||
await t.expect(item.nameTitle.exists).ok()
|
||||
await item.delete()
|
||||
await t.expect(item.nameTitle.exists).notOk()
|
||||
},
|
||||
|
||||
async editItem (t) {
|
||||
await createCategory(t)
|
||||
const item = await createItem(t)
|
||||
|
||||
const editedItem = {
|
||||
name: `testItem-${uniqid()}`,
|
||||
hackage: `testItemHackage-${uniqid()}`,
|
||||
link: `https://aelve.com/${uniqid()}`
|
||||
}
|
||||
|
||||
await t.expect(item.nameTitle.exists).ok()
|
||||
await item.toggleInfoEdit()
|
||||
await t
|
||||
.typeText(sel('CategoryItemToolbar-InfoEdit-NameInput'), editedItem.name, { replace: true })
|
||||
.typeText(sel('CategoryItemToolbar-InfoEdit-HackageInput'), editedItem.hackage, { replace: true })
|
||||
.typeText(sel('CategoryItemToolbar-InfoEdit-LinkInput'), editedItem.link, { replace: true })
|
||||
.click(sel('CategoryItemToolbar-InfoEdit-SaveBtn'))
|
||||
item.name = editedItem.name
|
||||
item.hackage = editedItem.hackage
|
||||
item.linkValue = editedItem.linkValue
|
||||
await t
|
||||
.expect(item.nameTitle.exists).ok()
|
||||
.expect(item.hackageLinkHref).contains(editedItem.hackage)
|
||||
.expect(item.linkHref).eql(editedItem.link)
|
||||
},
|
||||
|
||||
async moveItems (t) {
|
||||
await createCategory(t)
|
||||
const firstItem = await createItem(t)
|
||||
const secondItem = await createItem(t)
|
||||
|
||||
const itemTitle = sel('CategoryItemToolbar-ItemName')
|
||||
const firstFoundItemName = itemTitle.nth(0).innerText
|
||||
const secondFoundItemName = itemTitle.nth(1).innerText
|
||||
|
||||
await t
|
||||
.expect(firstFoundItemName).eql(firstItem.name)
|
||||
.expect(secondFoundItemName).eql(secondItem.name)
|
||||
|
||||
await firstItem.moveDown()
|
||||
await t
|
||||
.expect(firstFoundItemName).eql(secondItem.name)
|
||||
.expect(secondFoundItemName).eql(firstItem.name)
|
||||
await firstItem.moveUp()
|
||||
await t
|
||||
.expect(firstFoundItemName).eql(firstItem.name)
|
||||
.expect(secondFoundItemName).eql(secondItem.name)
|
||||
},
|
||||
|
||||
async mergeConflicts (t) {
|
||||
await createCategory(t)
|
||||
|
||||
const categoryPath = (await getLocation()).split('/').pop()
|
||||
const categoryId = categoryPathToId(categoryPath)
|
||||
|
||||
const editDescriptionBtn = sel('Category-EditDescriptionBtn')
|
||||
const descriptionEditor = sel('CategoryDescription-Editor').find('.CodeMirror textarea')
|
||||
const descriptionEditorSaveBtn = sel('CategoryDescription-Editor').find(testIdAttribute('MarkdownEditor-SaveBtn'))
|
||||
const descriptionText = sel('CategoryDescription-Content').innerText
|
||||
const conflictDialog = sel('ConflictDialog')
|
||||
|
||||
// because testCafe doesn't support multiple tabs/windows, to emulate merged conflict api requests executed manually
|
||||
// https://github.com/DevExpress/testcafe/issues/912
|
||||
const updateDescription = ({ original, modified }) => axios
|
||||
.put(`${baseUrl}/api/category/${categoryId}/notes`, {
|
||||
original,
|
||||
modified
|
||||
})
|
||||
|
||||
const description = `testDescription-${uniqid()}`
|
||||
await t
|
||||
.click(editDescriptionBtn)
|
||||
.typeText(descriptionEditor, description)
|
||||
.click(descriptionEditorSaveBtn)
|
||||
.expect(descriptionText).eql(description)
|
||||
|
||||
const newDescription = `testDescription-${uniqid()}`
|
||||
// update description manually so in browser it's still equal to "description" variable
|
||||
await updateDescription({ original: description, modified: newDescription })
|
||||
|
||||
// when we save current description even without modifying conflict dialog should popup,
|
||||
// cause original version on server is different
|
||||
await t
|
||||
.click(editDescriptionBtn)
|
||||
.click(descriptionEditorSaveBtn)
|
||||
.click(sel('ConflictDialog-SubmitLocalBtn'))
|
||||
.expect(descriptionText).eql(description)
|
||||
|
||||
await updateDescription({ original: description, modified: newDescription })
|
||||
|
||||
await t
|
||||
.click(editDescriptionBtn)
|
||||
.click(descriptionEditorSaveBtn)
|
||||
.click(sel('ConflictDialog-SubmitServerBtn'))
|
||||
.expect(descriptionText).eql(newDescription)
|
||||
|
||||
await updateDescription({ original: newDescription, modified: description })
|
||||
|
||||
await t
|
||||
.click(editDescriptionBtn)
|
||||
.click(descriptionEditorSaveBtn)
|
||||
|
||||
const mergedText = await conflictDialog
|
||||
.find(testIdAttribute('MarkdownEditor-OriginalTextarea'))
|
||||
.value
|
||||
|
||||
await t.click(sel('ConflictDialog-SubmitMergedBtn'))
|
||||
await t.expect(descriptionText).eql(mergedText)
|
||||
}
|
||||
}
|
||||
export default testFunctions
|
20
front/tests/tests.js
Normal file
20
front/tests/tests.js
Normal file
@ -0,0 +1,20 @@
|
||||
import testFunctions from './testFunctions'
|
||||
import config from './config.tests'
|
||||
|
||||
fixture`Index`
|
||||
.page(config.baseUrl)
|
||||
.beforeEach(async t => {
|
||||
// sometimes it takes long for index page to load and tests can fail
|
||||
await t.wait(2000)
|
||||
})
|
||||
|
||||
test('Resize window if mobile test', testFunctions.resizeWindowIfMobile)
|
||||
test('Create category, create category with duplicate name', testFunctions.createCategoryAndDuplicate)
|
||||
test('Edit category', testFunctions.editCategory)
|
||||
test('Delete category', testFunctions.deleteCategory)
|
||||
test('Create item with optional parameters', testFunctions.createItemWithOptionalParams)
|
||||
test('Create item without optional parameters', testFunctions.createItemNoOptionalParams)
|
||||
test('Delete item', testFunctions.deleteItem)
|
||||
test('Edit item', testFunctions.editItem)
|
||||
test('Move items', testFunctions.moveItems)
|
||||
test('Merge conflicts', testFunctions.mergeConflicts)
|
13
front/tests/utils.js
Normal file
13
front/tests/utils.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { Selector, ClientFunction } from 'testcafe'
|
||||
|
||||
const getLocation = ClientFunction(() => document.location.href)
|
||||
const goBack = ClientFunction(() => window.history.back())
|
||||
const testIdAttribute = id => `[data-testid="${id}"]`
|
||||
const sel = (id, options) => Selector(testIdAttribute(id), options)
|
||||
|
||||
export {
|
||||
getLocation,
|
||||
goBack,
|
||||
testIdAttribute,
|
||||
sel
|
||||
}
|
@ -24,6 +24,6 @@
|
||||
"dom",
|
||||
"es2017",
|
||||
"es5"
|
||||
],
|
||||
]
|
||||
}
|
||||
}
|
4
scripts/ci-functions.sh
Normal file
4
scripts/ci-functions.sh
Normal file
@ -0,0 +1,4 @@
|
||||
function run_back() {
|
||||
containerID=$(docker run -d -p 4400:4400 "quay.io/aelve/guide:$1--back")
|
||||
until docker logs $containerID 2>&1 | grep -m 1 "API is running"; do :; done
|
||||
}
|
Loading…
Reference in New Issue
Block a user