1
1
mirror of https://github.com/aelve/guide.git synced 2024-11-22 03:12:58 +03:00

Frontend E2E tests (#404)

Frontend E2E tests
This commit is contained in:
avele 2019-10-29 00:54:54 +04:00 committed by GitHub
parent d334d7e355
commit 0336942072
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1246 additions and 294 deletions

View File

@ -11,8 +11,7 @@ services:
- postgresql - postgresql
before_install: before_install:
- # start your web application and listen on `localhost` - nvm install --lts
- google-chrome-stable --headless --disable-gpu --remote-debugging-port=5000 http://localhost &
cache: cache:
directories: directories:
@ -23,22 +22,6 @@ cache:
jobs: jobs:
include: 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" - stage: "Build the backend and upload a Docker image"
before_script: before_script:
- sudo apt-get install -y libgmp-dev - sudo apt-get install -y libgmp-dev
@ -97,10 +80,68 @@ jobs:
exit 1 exit 1
fi fi
before_cache: 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" - stage: "Test the backend"
before_script: before_script:
- google-chrome-stable --headless --dis able-gpu --remote-debugging-port=5000 http://localhost &
- sudo apt-get install -y libgmp-dev fluxbox - sudo apt-get install -y libgmp-dev fluxbox
- curl -sSL https://get.haskellstack.org/ | sh - curl -sSL https://get.haskellstack.org/ | sh
# travis_retry works around https://github.com/commercialhaskell/stack/issues/4888 # travis_retry works around https://github.com/commercialhaskell/stack/issues/4888
@ -123,14 +164,7 @@ jobs:
- make back/test-db - make back/test-db
- make back/load-into-postgres - make back/load-into-postgres
before_cache: 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: "Test the frontend"
# - sleep 10
# - fluxbox >/dev/null 2>&1 &
# script:
# # Run testcafe e2e testing
# - npm run test
notifications: notifications:
slack: slack:

View File

@ -2,6 +2,7 @@ import path from 'path'
import fs from 'fs' import fs from 'fs'
import { createBundleRenderer } from 'vue-server-renderer' import { createBundleRenderer } from 'vue-server-renderer'
// TODO move 'src' prefix to config // 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 // TODO find a way to get rid of ts ignore and still build without errors
// @ts-ignore // @ts-ignore
import bundle from '../src/vue-ssr-server-bundle.json' import bundle from '../src/vue-ssr-server-bundle.json'

View 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>

View File

@ -30,11 +30,13 @@
ref="categoryNameInput" ref="categoryNameInput"
class="mb-3" class="mb-3"
label="Category name" label="Category name"
data-testid="add-category-dialog_category-name-input" data-testid="AddCategoryDialog-NameInput"
:rules="categoryValidationRules" :rules="categoryValidationRules"
v-model="categoryName" v-model="categoryName"
/> />
<v-text-field <v-text-field
data-testid="AddCategoryDialog-GroupInput"
:rules="groupValidationRules"
v-model="groupNameInternal" v-model="groupNameInternal"
label="Group" label="Group"
/> />
@ -54,7 +56,7 @@
aria-label="Submit" aria-label="Submit"
color="info" color="info"
class="add-category-submit-btn" class="add-category-submit-btn"
data-testid="add-category-dialog_submit-btn" data-testid="AddCategoryDialog-SubmitBtn"
:disabled="!isValid" :disabled="!isValid"
@click.native="submit" @click.native="submit"
> >
@ -71,25 +73,26 @@
<ConfirmDialog <ConfirmDialog
eager eager
ref="duplicateConfirm" ref="duplicateConfirm"
data-testid="duplicate-category-dialog"
:confirmBtnProps="{ :confirmBtnProps="{
'data-testid': 'duplicate-category-dialog_confirm-btn' 'data-testid': 'DuplicateCategoryDialog-ConfirmBtn'
}" }"
max-width="500px" max-width="500px"
:value="isDuplicateConfirmShow" :value="isDuplicateConfirmShow"
> >
This group already has categories with the same name: <div data-testid="DuplicateCategoryDialog">
<ul class="duplicate-categories-list"> This group already has categories with the same name:
<li <ul class="duplicate-categories-list">
v-for="category in sameNameCategories" <li
:key="category.id" v-for="category in sameNameCategories"
> :key="category.id"
<router-link >
:to="`/haskell/${getCategoryUrl(category)}`" <router-link
target="_blank" :to="`/haskell/${getCategoryUrl(category)}`"
>{{ category.title }}</router-link> target="_blank"
</li> >{{ category.title }}</router-link>
</ul> </li>
</ul>
</div>
</ConfirmDialog> </ConfirmDialog>
</v-dialog> </v-dialog>
@ -118,36 +121,44 @@ export default class AddCategoryDialog extends Vue {
categoryValidationRules: Array<(x: string) => boolean | string> = [ categoryValidationRules: Array<(x: string) => boolean | string> = [
(x: string) => !!x || `Category name can't be empty` (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 isValid: boolean = false
isDuplicateConfirmShow: boolean = false isDuplicateConfirmShow: boolean = false
sameNameCategories = []
get categories () { get categories () {
return this.$store.state.category.categoryList 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') @Watch('value')
onOpen (newVal: boolean) { onOpen (newVal: boolean) {
this.categoryName = '' // wait for vue to render form
this.groupNameInternal = this.groupName this.$nextTick(() => {
this.$refs.form.resetValidation()
this.categoryName = ''
this.groupNameInternal = this.groupName
})
} }
close () { close () {
this.$emit('input', false) 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 () { async submit () {
if (!this.$refs.form.validate()) { if (!this.$refs.form.validate()) {
return return
} }
this.sameNameCategories = this.calcSameNameCategories()
if (this.sameNameCategories.length) { if (this.sameNameCategories.length) {
const isDuplicationConfirmed = await this.confirmDuplicate() const isDuplicationConfirmed = await this.confirmDuplicate()
if (!isDuplicationConfirmed) { if (!isDuplicationConfirmed) {

View File

@ -28,17 +28,20 @@
autofocus autofocus
class="mb-2" class="mb-2"
label="Item name" label="Item name"
data-testid="AddItemDialog-NameInput"
:rules="nameValidationRules" :rules="nameValidationRules"
v-model="name" v-model="name"
/> />
<v-text-field <v-text-field
class="mb-2" class="mb-2"
label="Name on Hackage (optional)" label="Name on Hackage (optional)"
data-testid="AddItemDialog-HackageInput"
v-model="hackage" v-model="hackage"
/> />
<v-text-field <v-text-field
class="mb-2" class="mb-2"
label="Link to the official site, if exists" label="Link to the official site, if exists"
data-testid="AddItemDialog-LinkInput"
v-model="link" v-model="link"
/> />
@ -57,6 +60,7 @@
<v-btn <v-btn
color="info" color="info"
:disabled="!isValid" :disabled="!isValid"
data-testid="AddItemDialog-SubmitBtn"
aria-label="Create" aria-label="Create"
@click.native="submit" @click.native="submit"
> >

View File

@ -2,18 +2,32 @@
<div class="categories"> <div class="categories">
<div class="categories-flex-container"> <div class="categories-flex-container">
<AddCategoryBtn
class="mt-4"
v-if="!categories || !categories.length"
@click="openAddCategoryDialog()"
/>
<div <div
class="categories-column" class="categories-column"
v-for="(groupCategories, groupName, index) in groups" v-for="(groupCategories, groupName, index) in groups"
:key="index" :key="index"
> >
<div class="category-group"> <div
<h2 class="mt-0 mb-2 group-title"> class="category-group"
data-testid="Categories-CategoryGroup"
>
<h2
class="mt-0 mb-2 group-title"
data-testid="Categories-CategoryGroup-Title"
>
{{ groupName }} {{ groupName }}
</h2> </h2>
<!-- TODO remove duplication of category title links (move to component or refactor v-for) -->
<router-link <router-link
class="category-title ml-2" class="category-title ml-2"
data-testid="Categories-CategoryTitle"
v-for="category in groupCategories[CategoryStatus.finished]" v-for="category in groupCategories[CategoryStatus.finished]"
:key="category.id" :key="category.id"
:to="`/haskell/${getCategoryUrl(category)}`" :to="`/haskell/${getCategoryUrl(category)}`"
@ -27,6 +41,7 @@
</h3> </h3>
<router-link <router-link
class="category-title ml-3" class="category-title ml-3"
data-testid="Categories-CategoryTitle"
v-for="category in groupCategories[CategoryStatus.inProgress]" v-for="category in groupCategories[CategoryStatus.inProgress]"
:key="category.id" :key="category.id"
:to="`/haskell/${getCategoryUrl(category)}`" :to="`/haskell/${getCategoryUrl(category)}`"
@ -42,6 +57,7 @@
<router-link <router-link
class="category-title ml-3" class="category-title ml-3"
data-testid="Categories-CategoryTitle"
v-for="category in groupCategories[CategoryStatus.toBeWritten]" v-for="category in groupCategories[CategoryStatus.toBeWritten]"
:key="category.id" :key="category.id"
:to="`/haskell/${getCategoryUrl(category)}`" :to="`/haskell/${getCategoryUrl(category)}`"
@ -50,16 +66,7 @@
</router-link> </router-link>
</template> </template>
<v-btn <AddCategoryBtn @click="openAddCategoryDialog(groupName)"/>
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>
</div> </div>
</div> </div>
@ -76,6 +83,7 @@
import Vue from 'vue' import Vue from 'vue'
import Component from 'vue-class-component' import Component from 'vue-class-component'
import AddCategoryDialog from 'client/components/AddCategoryDialog.vue' import AddCategoryDialog from 'client/components/AddCategoryDialog.vue'
import AddCategoryBtn from 'client/components/AddCategoryBtn.vue'
import _groupBy from 'lodash/groupBy' import _groupBy from 'lodash/groupBy'
import _sortBy from 'lodash/sortBy' import _sortBy from 'lodash/sortBy'
import _fromPairs from 'lodash/fromPairs' import _fromPairs from 'lodash/fromPairs'
@ -84,7 +92,8 @@ import { ICategoryInfo, CategoryStatus } from 'client/service/Category'
@Component({ @Component({
components: { components: {
AddCategoryDialog AddCategoryDialog,
AddCategoryBtn
} }
}) })
export default class Categories extends Vue { export default class Categories extends Vue {

View File

@ -6,12 +6,14 @@
v-else v-else
v-html="categoryDescription" v-html="categoryDescription"
class="category-description__content" class="category-description__content"
data-testid="CategoryDescription-Content"
/> />
</div> </div>
<markdown-editor <markdown-editor
v-else v-else
toolbar toolbar
data-testid="CategoryDescription-Editor"
:value="categoryDescriptionRaw" :value="categoryDescriptionRaw"
@cancel="toggleEditDescription" @cancel="toggleEditDescription"
@save="updateDescription({ original: categoryDescriptionRaw, modified: $event})" @save="updateDescription({ original: categoryDescriptionRaw, modified: $event})"
@ -19,6 +21,7 @@
<v-btn <v-btn
text text
data-testid="Category-EditDescriptionBtn"
v-if="!isEditDescription" v-if="!isEditDescription"
color="grey darken-2" color="grey darken-2"
class="mt-3" class="mt-3"

View File

@ -10,34 +10,47 @@
> >
<v-icon class="rss-link-icon">$vuetify.icons.rss</v-icon> <v-icon class="rss-link-icon">$vuetify.icons.rss</v-icon>
</a> </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>
<div class="category-header__second-row"> <div class="category-header__second-row">
<div class="category-group-title-wrap"> <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>
<div class="category-actions"> <div class="category-actions">
<CategoryHeaderBtn <CategoryHeaderBtn
class="mr-1"
text="New item" text="New item"
icon="plus" icon="plus"
class="mr-1" data-testid="CategoryHeader-NewItemBtn"
@click="openAddItemDialog" @click="openAddItemDialog"
/> />
<CategoryHeaderBtn <CategoryHeaderBtn
text="Category settings"
icon="cog "
class="mr-1" class="mr-1"
text="Category settings"
icon="cog"
data-testid="CategoryHeader-CategorySettingsBtn"
@click="openCategorySettingsEditDialog" @click="openCategorySettingsEditDialog"
/> />
<CategoryHeaderBtn <CategoryHeaderBtn
text="Delete category" text="Delete category"
icon="trash-alt" icon="trash-alt"
data-testid="CategoryHeader-CategoryDeleteBtn"
@click="deleteCategory" @click="deleteCategory"
/> />
</div> </div>
<!-- TODO create responsive toolbar component to get rid of duplication of buttons -->
<v-menu bottom left offset-y> <v-menu bottom left offset-y>
<template v-slot:activator="{ on }"> <template v-slot:activator="{ on }">
<v-btn <v-btn
@ -45,6 +58,7 @@
icon icon
aria-label="Actions" aria-label="Actions"
v-tooltip="'Actions'" v-tooltip="'Actions'"
data-testid="CategoryHeader-MobileMenuBtn"
class="category-actions-menu-btn" class="category-actions-menu-btn"
v-on="on" v-on="on"
> >
@ -58,17 +72,19 @@
<v-list class="category-actions-menu-list"> <v-list class="category-actions-menu-list">
<v-list-item> <v-list-item>
<CategoryHeaderBtn <CategoryHeaderBtn
block block
text="New item" text="New item"
icon="plus" icon="plus"
data-testid="CategoryHeader-NewItemBtn"
@click="openAddItemDialog" @click="openAddItemDialog"
/> />
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<CategoryHeaderBtn <CategoryHeaderBtn
block block
text="Category settings" text="Category settings"
icon="cog" icon="cog"
data-testid="CategoryHeader-CategorySettingsBtn"
@click="openCategorySettingsEditDialog" @click="openCategorySettingsEditDialog"
/> />
</v-list-item> </v-list-item>
@ -77,6 +93,7 @@
block block
text="Delete category" text="Delete category"
icon="trash-alt" icon="trash-alt"
data-testid="CategoryHeader-CategoryDeleteBtn"
@click="deleteCategory" @click="deleteCategory"
/> />
</v-list-item> </v-list-item>
@ -129,7 +146,8 @@ export default class CategoryHeader extends Vue {
text: 'delete this category', text: 'delete this category',
confirmBtnText: 'Delete', confirmBtnText: 'Delete',
confirmBtnProps: { confirmBtnProps: {
color: 'error' 'color': 'error',
'data-testid': 'DeleteCategoryDialog-ConfirmBtn'
} }
}) })
async deleteCategory () { async deleteCategory () {

View File

@ -18,17 +18,20 @@
<category-item-section <category-item-section
title="Summary" title="Summary"
class="mb-3" class="mb-3"
data-testid="CategoryItem-SummarySection"
:editText="summary.text" :editText="summary.text"
@save="updateSummary({original: summary.text, modified: $event})" @save="updateSummary({original: summary.text, modified: $event})"
> >
<div <div
class="mb-2" class="mb-2"
data-testid="CategoryItem-SummarySection-Content"
v-html="summary.html" v-html="summary.html"
/> />
</category-item-section> </category-item-section>
<div <div
v-if="isSectionEnabled('ItemProsConsSection')" v-if="isSectionEnabled('ItemProsConsSection')"
data-testid="CategoryItem-TraitsSection"
class="category-item-traits mb-3" class="category-item-traits mb-3"
> >
<category-item-traits <category-item-traits
@ -49,17 +52,22 @@
v-if="isSectionEnabled('ItemEcosystemSection')" v-if="isSectionEnabled('ItemEcosystemSection')"
title="Ecosystem" title="Ecosystem"
class="mb-3" class="mb-3"
data-testid="CategoryItem-EcosystemSection"
:editText="ecosystem.text" :editText="ecosystem.text"
@save="updateEcosystem({original: ecosystem.text, modified: $event})" @save="updateEcosystem({original: ecosystem.text, modified: $event})"
@toggleEdit="toggleItemEcosystemEditState" @toggleEdit="toggleItemEcosystemEditState"
> >
<div v-html="ecosystem.html" /> <div
v-html="ecosystem.html"
data-testid="CategoryItem-EcosystemSection-Content"
/>
</category-item-section> </category-item-section>
<category-item-section <category-item-section
v-if="isSectionEnabled('ItemNotesSection')" v-if="isSectionEnabled('ItemNotesSection')"
title="Notes" title="Notes"
class="mb-3" class="mb-3"
data-testid="CategoryItem-NotesSection"
:editText="notes.text" :editText="notes.text"
@save="updateNotes({original: notes.text, modified: $event})" @save="updateNotes({original: notes.text, modified: $event})"
@toggleEdit="toggleItemNotesEditState" @toggleEdit="toggleItemNotesEditState"
@ -101,6 +109,7 @@
<div <div
v-if="notes.html" v-if="notes.html"
v-html="notes.html" v-html="notes.html"
data-testid="CategoryItem-NotesSection-Content"
/> />
<span v-else> &lt;notes are empty&gt; </span> <span v-else> &lt;notes are empty&gt; </span>
</div> </div>
@ -154,7 +163,7 @@ export default class CategoryItem extends Vue {
@Watch('isAnyTraitEditing', { immediate: true }) @Watch('isAnyTraitEditing', { immediate: true })
updateItemTraitEditingState (newVal, prevVal) { updateItemTraitEditingState (newVal, prevVal) {
if (newVal !== prevVal) { if (!!newVal !== !!prevVal) {
this.$store.dispatch('category/toggleItemProsConsSectionEdit', this.itemUid) this.$store.dispatch('category/toggleItemProsConsSectionEdit', this.itemUid)
} }
} }

View File

@ -1,5 +1,8 @@
<template> <template>
<div class="category-item-toolbar"> <div
class="category-item-toolbar"
data-testid="CategoryItemToolbar"
>
<div class="category-item-toolbar__header"> <div class="category-item-toolbar__header">
<v-toolbar <v-toolbar
color="#dedede" color="#dedede"
@ -17,14 +20,20 @@
v-if="itemLink" v-if="itemLink"
class="category-item-name" class="category-item-name"
target="_blank" target="_blank"
data-testid="CategoryItemToolbar-ItemName"
:href="itemLink" :href="itemLink"
>{{ itemName }}</a> >{{ 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"> <div class="category-item-badges">
<a <a
v-if="this.itemHackage" v-if="this.itemHackage"
class="hackage-link" class="hackage-link"
target="_blank" target="_blank"
data-testid="CategoryItemToolbar-HackageLink"
:href="`https://hackage.haskell.org/package/${this.itemHackage}`" :href="`https://hackage.haskell.org/package/${this.itemHackage}`"
> >
<v-icon <v-icon
@ -38,13 +47,14 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-toolbar-items> <v-toolbar-items class="category-item__actions">
<div class="category-item-toolbar-btns"> <div class="category-item-toolbar-btns">
<category-item-btn <category-item-btn
titleTooltip titleTooltip
size="40px" size="40px"
iconSize="18" iconSize="18"
title="Move item up" title="Move item up"
data-testid="CategoryItemToolbar-MoveUpBtn"
icon="arrow-up" icon="arrow-up"
@click="moveItem('up')" @click="moveItem('up')"
/> />
@ -53,6 +63,7 @@
size="40px" size="40px"
iconSize="18" iconSize="18"
title="Move item down" title="Move item down"
data-testid="CategoryItemToolbar-MoveDownBtn"
icon="arrow-down" icon="arrow-down"
@click="moveItem('down')" @click="moveItem('down')"
/> />
@ -61,6 +72,7 @@
size="40px" size="40px"
iconSize="18" iconSize="18"
title="Edit item info" title="Edit item info"
data-testid="CategoryItemToolbar-EditInfoBtn"
icon="cog" icon="cog"
@click="toggleEditItemInfoMenu" @click="toggleEditItemInfoMenu"
> >
@ -78,11 +90,17 @@
iconSize="18" iconSize="18"
title="Delete item" title="Delete item"
icon="trash-alt" icon="trash-alt"
data-testid="CategoryItemToolbar-DeleteBtn"
@click="deleteItem" @click="deleteItem"
/> />
</div> </div>
<v-menu bottom left offset-y> <v-menu
bottom
left
offset-y
attach=".category-item__actions"
>
<template v-slot:activator="{ on }"> <template v-slot:activator="{ on }">
<v-btn <v-btn
icon icon
@ -91,6 +109,7 @@
aria-label="Actions" aria-label="Actions"
v-tooltip="'Actions'" v-tooltip="'Actions'"
class="category-toolbar-mobile-menu-btn" class="category-toolbar-mobile-menu-btn"
data-testid="CategoryItemToolbar-MobileMenuBtn"
v-on="on" v-on="on"
> >
<v-icon <v-icon
@ -109,6 +128,7 @@
iconSize="18" iconSize="18"
title="Move item up" title="Move item up"
icon="arrow-up" icon="arrow-up"
data-testid="CategoryItemToolbar-MoveUpBtn"
@click="moveItem('up')" @click="moveItem('up')"
/> />
</v-list-item> </v-list-item>
@ -120,6 +140,7 @@
iconSize="18" iconSize="18"
title="Move item down" title="Move item down"
icon="arrow-down" icon="arrow-down"
data-testid="CategoryItemToolbar-MoveDownBtn"
@click="moveItem('down')" @click="moveItem('down')"
/> />
</v-list-item> </v-list-item>
@ -131,6 +152,7 @@
iconSize="18" iconSize="18"
title="Edit item info" title="Edit item info"
icon="cog" icon="cog"
data-testid="CategoryItemToolbar-EditInfoBtn"
@click="toggleEditItemInfoMenu" @click="toggleEditItemInfoMenu"
> >
<v-icon <v-icon
@ -149,6 +171,7 @@
iconSize="18" iconSize="18"
title="Delete item" title="Delete item"
icon="trash-alt" icon="trash-alt"
data-testid="CategoryItemToolbar-DeleteBtn"
@click="deleteItem" @click="deleteItem"
/> />
</v-list-item> </v-list-item>
@ -171,14 +194,17 @@
> >
<v-text-field <v-text-field
v-model="itemNameEdit" v-model="itemNameEdit"
data-testid="CategoryItemToolbar-InfoEdit-NameInput"
label="Name" label="Name"
/> />
<v-text-field <v-text-field
v-model="itemHackageEdit" v-model="itemHackageEdit"
data-testid="CategoryItemToolbar-InfoEdit-HackageInput"
label="Name on Hackage (optional)" label="Name on Hackage (optional)"
/> />
<v-text-field <v-text-field
v-model="itemLinkEdit" v-model="itemLinkEdit"
data-testid="CategoryItemToolbar-InfoEdit-LinkInput"
label="Site (optional)" label="Site (optional)"
/> />
</v-form> </v-form>
@ -195,6 +221,7 @@
color="info" color="info"
aria-label="Save" aria-label="Save"
:disabled="!isInfoSaveEnabled" :disabled="!isInfoSaveEnabled"
data-testid="CategoryItemToolbar-InfoEdit-SaveBtn"
@click="updateItemInfo" @click="updateItemInfo"
> >
Save Save
@ -286,7 +313,8 @@ export default class CategoryItemToolbar extends Vue {
text: 'delete this item', text: 'delete this item',
confirmBtnText: 'Delete', confirmBtnText: 'Delete',
confirmBtnProps: { confirmBtnProps: {
color: 'error' 'color': 'error',
'data-testid': 'ItemDeleteDialog-ConfirmBtn'
} }
}) })
async deleteItem (): Promise<void> { async deleteItem (): Promise<void> {

View File

@ -28,12 +28,14 @@
autofocus autofocus
class="mb-2" class="mb-2"
label="Title" label="Title"
data-testid="CategorySettings-TitleInput"
:rules="inputValidationRules" :rules="inputValidationRules"
v-model="title" v-model="title"
/> />
<v-text-field <v-text-field
class="mb-2" class="mb-2"
label="Group" label="Group"
data-testid="CategorySettings-GroupInput"
:rules="inputValidationRules" :rules="inputValidationRules"
v-model="group" v-model="group"
/> />
@ -50,6 +52,7 @@
hide-details hide-details
color="info" color="info"
class="category-settings-dialog__checkbox" class="category-settings-dialog__checkbox"
data-testid="CategorySettings-ItemTraitsSectionCheckbox"
label="Pros/cons section" label="Pros/cons section"
value="ItemProsConsSection" value="ItemProsConsSection"
:inputValue="sections" :inputValue="sections"
@ -59,6 +62,7 @@
hide-details hide-details
color="info" color="info"
class="category-settings-dialog__checkbox" class="category-settings-dialog__checkbox"
data-testid="CategorySettings-ItemEcosystemSectionCheckbox"
label="Ecosystem section" label="Ecosystem section"
value="ItemEcosystemSection" value="ItemEcosystemSection"
:inputValue="sections" :inputValue="sections"
@ -68,6 +72,7 @@
hide-details hide-details
color="info" color="info"
class="category-settings-dialog__checkbox" class="category-settings-dialog__checkbox"
data-testid="CategorySettings-ItemNotesSectionCheckbox"
label="Notes section" label="Notes section"
value="ItemNotesSection" value="ItemNotesSection"
:inputValue="sections" :inputValue="sections"
@ -88,6 +93,7 @@
<v-btn <v-btn
color="info" color="info"
aria-label="Submit" aria-label="Submit"
data-testid="CategorySettings-SubmitBtn"
:disabled="!isValid || !hasChanges" :disabled="!isValid || !hasChanges"
@click="updateCategoryInfo" @click="updateCategoryInfo"
> >

View File

@ -14,7 +14,10 @@
/> />
</template> </template>
<div class="conflict-box"> <div
data-testid="ConflictDialog"
class="conflict-box"
>
<div class="conflict-item"> <div class="conflict-item">
<h2 class="mt-0">Your version</h2> <h2 class="mt-0">Your version</h2>
<v-card <v-card
@ -25,6 +28,7 @@
</v-card> </v-card>
<v-btn <v-btn
class="conflict-dialog-btn" class="conflict-dialog-btn"
data-testid="ConflictDialog-SubmitLocalBtn"
aria-label="Submit this version, disregard changes on server" aria-label="Submit this version, disregard changes on server"
@click="save(modified)" @click="save(modified)"
> >
@ -41,6 +45,7 @@
</v-card> </v-card>
<v-btn <v-btn
class="conflict-dialog-btn" class="conflict-dialog-btn"
data-testid="ConflictDialog-SubmitServerBtn"
aria-label="Submit this version, disregard my changes" aria-label="Submit this version, disregard my changes"
@click="save(serverModified)" @click="save(serverModified)"
> >
@ -60,6 +65,7 @@
/> />
<v-btn <v-btn
class="conflict-dialog-btn" class="conflict-dialog-btn"
data-testid="ConflictDialog-SubmitMergedBtn"
aria-label="Submit merged version" aria-label="Submit merged version"
@click="save(mergedEdit)" @click="save(mergedEdit)"
> >

View File

@ -13,7 +13,10 @@
@keydown.exact.esc="cancel" @keydown.exact.esc="cancel"
v-show="editor && isReady" v-show="editor && isReady"
> >
<textarea ref="editor" /> <textarea
data-testid="MarkdownEditor-OriginalTextarea"
ref="editor"
/>
<v-toolbar <v-toolbar
flat flat
@ -36,6 +39,7 @@
Cancel Cancel
</v-btn> </v-btn>
<v-btn <v-btn
data-testid="MarkdownEditor-SaveBtn"
color="info" color="info"
aria-label="Save" aria-label="Save"
@click="save" @click="save"
@ -110,6 +114,7 @@ export default class MarkdownEditor extends Vue {
const EasyMDE = (await import('easymde')).default const EasyMDE = (await import('easymde')).default
this.editor = new EasyMDE({ this.editor = new EasyMDE({
element: this.$refs.editor, element: this.$refs.editor,
forceSync: true,
autofocus: true, autofocus: true,
initialValue: this.value, initialValue: this.value,
spellChecker: false, spellChecker: false,

View File

@ -0,0 +1,3 @@
export default function (path) {
return path.split('#').shift().split('-').pop()
}

View File

@ -1,4 +1,5 @@
import Router from 'vue-router' import Router from 'vue-router'
import categoryPathToId from 'client/helpers/categoryPathToId'
function createRouter (store) { function createRouter (store) {
return new Router({ return new Router({
@ -31,7 +32,7 @@ function createRouter (store) {
path: '/haskell/:category', path: '/haskell/:category',
name: 'Category', name: 'Category',
component: () => import('../page/CategoryPage.vue'), 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/', path: '/haskell/search/results/',

View File

@ -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)
}
})

View File

@ -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
View File

@ -977,12 +977,40 @@
"glob-to-regexp": "^0.3.0" "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": { "@nodelib/fs.stat": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
"dev": true "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": { "@types/error-stack-parser": {
"version": "1.3.18", "version": "1.3.18",
"resolved": "https://registry.npmjs.org/@types/error-stack-parser/-/error-stack-parser-1.3.18.tgz", "resolved": "https://registry.npmjs.org/@types/error-stack-parser/-/error-stack-parser-1.3.18.tgz",
@ -1013,9 +1041,9 @@
} }
}, },
"@types/lodash": { "@types/lodash": {
"version": "4.14.136", "version": "4.14.144",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.136.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.144.tgz",
"integrity": "sha512-0GJhzBdvsW2RUccNHOBkabI8HZVdOXmXbXhuKlDEd5Vv12P7oAVGfomGp3Ne21o5D/qu1WmthlNKFaoZJJeErA==", "integrity": "sha512-ogI4g9W5qIQQUhXAclq6zhqgqNUr7UlFaqDHbch7WLSLeeM/7d3CRaw7GLajxvyFvhJqw4Rpcz5bhoaYtIx6Tg==",
"dev": true "dev": true
}, },
"@types/minimatch": { "@types/minimatch": {
@ -1025,9 +1053,9 @@
"dev": true "dev": true
}, },
"@types/node": { "@types/node": {
"version": "10.14.13", "version": "10.14.22",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.13.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.22.tgz",
"integrity": "sha512-yN/FNNW1UYsRR1wwAoyOwqvDuLDtVXnaJTZ898XIw/Q5cCaeVAlVwvsmXLX5PuiScBYwZsZU4JYSHB3TvfdwvQ==", "integrity": "sha512-9taxKC944BqoTVjE+UT3pQH0nHZlTvITwfsOZqyc+R3sfJuxaTtxWjfn1K2UlxyPcKHf0rnaXcVFrS9F9vf0bw==",
"dev": true "dev": true
}, },
"@vue/component-compiler-utils": { "@vue/component-compiler-utils": {
@ -1360,14 +1388,32 @@
"dev": true "dev": true
}, },
"acorn-hammerhead": { "acorn-hammerhead": {
"version": "0.2.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/acorn-hammerhead/-/acorn-hammerhead-0.2.0.tgz", "resolved": "https://registry.npmjs.org/acorn-hammerhead/-/acorn-hammerhead-0.3.0.tgz",
"integrity": "sha512-kbX1s/0ZikW0WEBY6IrooFgX3AP2D9ycTg0OhxRYLF0Tew/bDK2+8lTxFR4cDdoCZm6Ax8eVf8EV6gbTtr8EYQ==", "integrity": "sha512-Izrr9mXONhWc7q8fqUe6ijQy+KjmyQlgdWARgaCVjds+nPpoSS298FY8uSVN/to8nKVTtkJpafNUlACWxwZS5w==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/estree": "^0.0.39" "@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": { "ajv": {
"version": "6.10.2", "version": "6.10.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
@ -1532,6 +1578,21 @@
"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
"dev": true "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": { "asn1": {
"version": "0.2.4", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
@ -3269,6 +3330,12 @@
"tslib": "^1.9.0" "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": { "ci-info": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", "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": { "cli-boxes": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz",
@ -3315,29 +3388,60 @@
"dev": true "dev": true
}, },
"cliui": { "cliui": {
"version": "4.1.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"dev": true, "dev": true,
"requires": { "requires": {
"string-width": "^2.1.1", "string-width": "^3.1.0",
"strip-ansi": "^4.0.0", "strip-ansi": "^5.2.0",
"wrap-ansi": "^2.0.0" "wrap-ansi": "^5.1.0"
}, },
"dependencies": { "dependencies": {
"ansi-regex": { "ansi-regex": {
"version": "3.0.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true "dev": true
}, },
"strip-ansi": { "ansi-styles": {
"version": "4.0.0", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true, "dev": true,
"requires": { "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" "yargs": "^12.0.1"
}, },
"dependencies": { "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": { "ansi-styles": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "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": { "supports-color": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz",
@ -3568,6 +3710,36 @@
"dev": true "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": { "core-js": {
"version": "2.6.9", "version": "2.6.10",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.10.tgz",
"integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==", "integrity": "sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA==",
"dev": true "dev": true
}, },
"core-js-compat": { "core-js-compat": {
@ -4063,6 +4235,12 @@
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true "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": { "currently-unhandled": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
@ -4602,9 +4780,9 @@
} }
}, },
"esotope-hammerhead": { "esotope-hammerhead": {
"version": "0.3.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/esotope-hammerhead/-/esotope-hammerhead-0.3.0.tgz", "resolved": "https://registry.npmjs.org/esotope-hammerhead/-/esotope-hammerhead-0.4.0.tgz",
"integrity": "sha512-qI6ZlQaf4yBPZhHETT24vk93INsNj++mRKBHOpOtkx7F/N7UvuTD8neeTbcbRQUntGksvs8/v63uGfDCdgt5YQ==", "integrity": "sha512-TAmc7OhAiWeovbzE9GGenU2vwuB9tzKHlW0hTH4rZsLmCNEKo8wIZ9qbEnw8nyXeNTjCmBYOYXUyaN+eZht7Tg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/estree": "^0.0.39" "@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", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" "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": { "fibers": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/fibers/-/fibers-4.0.1.tgz", "resolved": "https://registry.npmjs.org/fibers/-/fibers-4.0.1.tgz",
@ -5715,9 +5902,9 @@
"dev": true "dev": true
}, },
"get-caller-file": { "get-caller-file": {
"version": "1.0.3", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true "dev": true
}, },
"get-func-name": { "get-func-name": {
@ -7309,9 +7496,9 @@
} }
}, },
"merge2": { "merge2": {
"version": "1.2.3", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.3.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz",
"integrity": "sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==", "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==",
"dev": true "dev": true
}, },
"microevent.ts": { "microevent.ts": {
@ -7480,7 +7667,8 @@
"moment": { "moment": {
"version": "2.24.0", "version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", "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": { "moment-duration-format-commonjs": {
"version": "1.0.0", "version": "1.0.0",
@ -9450,9 +9638,9 @@
"dev": true "dev": true
}, },
"require-main-filename": { "require-main-filename": {
"version": "1.0.1", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true "dev": true
}, },
"resolve": { "resolve": {
@ -9553,6 +9741,12 @@
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
"dev": true "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": { "rimraf": {
"version": "2.6.3", "version": "2.6.3",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
@ -9572,6 +9766,12 @@
"inherits": "^2.0.1" "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": { "run-queue": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
@ -9610,9 +9810,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"sanitize-filename": { "sanitize-filename": {
"version": "1.6.1", "version": "1.6.3",
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.1.tgz", "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
"integrity": "sha1-YS2hyWRz+gLczaktzVtKsWSmdyo=", "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
"dev": true, "dev": true,
"requires": { "requires": {
"truncate-utf8-bytes": "^1.0.0" "truncate-utf8-bytes": "^1.0.0"
@ -10335,9 +10535,9 @@
} }
}, },
"testcafe": { "testcafe": {
"version": "1.3.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/testcafe/-/testcafe-1.3.3.tgz", "resolved": "https://registry.npmjs.org/testcafe/-/testcafe-1.6.0.tgz",
"integrity": "sha512-UdFPD3+IW8SgCM97VAOasIFBqpsy70cDp+Sw+Cq2QAk9IeeJPGct4A7Eealdyhota7EmLTqXnlwXIZd0Jk+xbw==", "integrity": "sha512-jlydNbQ6m/LdM6o40EzDwMXBKWV8evZV2Xa1YzzJ9r6H58Y4FHpeYjDtv5gDaBkpV7NslDFXoOkpwnZgRvAjcw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "^10.12.19", "@types/node": "^10.12.19",
@ -10380,6 +10580,7 @@
"log-update-async-hook": "^2.0.2", "log-update-async-hook": "^2.0.2",
"make-dir": "^1.3.0", "make-dir": "^1.3.0",
"map-reverse": "^1.0.1", "map-reverse": "^1.0.1",
"mime-db": "^1.41.0",
"moment": "^2.10.3", "moment": "^2.10.3",
"moment-duration-format-commonjs": "^1.0.0", "moment-duration-format-commonjs": "^1.0.0",
"mustache": "^2.1.2", "mustache": "^2.1.2",
@ -10399,8 +10600,8 @@
"sanitize-filename": "^1.6.0", "sanitize-filename": "^1.6.0",
"source-map-support": "^0.5.5", "source-map-support": "^0.5.5",
"strip-bom": "^2.0.0", "strip-bom": "^2.0.0",
"testcafe-browser-tools": "1.6.8", "testcafe-browser-tools": "1.7.0",
"testcafe-hammerhead": "14.6.13", "testcafe-hammerhead": "14.9.2",
"testcafe-legacy-api": "3.1.11", "testcafe-legacy-api": "3.1.11",
"testcafe-reporter-json": "^2.1.0", "testcafe-reporter-json": "^2.1.0",
"testcafe-reporter-list": "^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": { "pify": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
@ -10491,17 +10698,19 @@
} }
}, },
"testcafe-browser-tools": { "testcafe-browser-tools": {
"version": "1.6.8", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/testcafe-browser-tools/-/testcafe-browser-tools-1.6.8.tgz", "resolved": "https://registry.npmjs.org/testcafe-browser-tools/-/testcafe-browser-tools-1.7.0.tgz",
"integrity": "sha512-xFgwmcAOutSJR6goqO8uUFGF5IF2xRC/Ssh4pB5QZ+bTjYsN5amnjgM+813bDBLelC+HmXKqylviz7Dzxbtbcw==", "integrity": "sha512-85CabhVhxrVriOCqwm5rGLA5LQ/tzuMYhPPmLE0eZhHHHX+qh1a8vbDfwJIn2TFgX0bNq9ZmCPV8RQfU8P0UAQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"array-find": "^1.0.0", "array-find": "^1.0.0",
"babel-runtime": "^5.6.15", "babel-runtime": "^5.6.15",
"del": "^5.1.0",
"graceful-fs": "^4.1.11", "graceful-fs": "^4.1.11",
"linux-platform-info": "^0.0.3", "linux-platform-info": "^0.0.3",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mustache": "^2.1.2", "mustache": "^2.1.2",
"nanoid": "^2.1.3",
"os-family": "^1.0.0", "os-family": "^1.0.0",
"pify": "^2.3.0", "pify": "^2.3.0",
"pinkie": "^2.0.1", "pinkie": "^2.0.1",
@ -10509,6 +10718,18 @@
"which-promise": "^1.0.0" "which-promise": "^1.0.0"
}, },
"dependencies": { "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": { "babel-runtime": {
"version": "5.8.38", "version": "5.8.38",
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-5.8.38.tgz", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-5.8.38.tgz",
@ -10518,32 +10739,201 @@
"core-js": "^1.0.0" "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": { "core-js": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
"integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=", "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=",
"dev": true "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": { "pify": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true "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": { "testcafe-hammerhead": {
"version": "14.6.13", "version": "14.9.2",
"resolved": "https://registry.npmjs.org/testcafe-hammerhead/-/testcafe-hammerhead-14.6.13.tgz", "resolved": "https://registry.npmjs.org/testcafe-hammerhead/-/testcafe-hammerhead-14.9.2.tgz",
"integrity": "sha512-uwIZ3sSQh2LdgbLjCM2z7jmVfR/EZ63WIId0C34FxaOd5E00+V1NdVWHofYYJvr8MBec9X8iRRGtir4nYU9H2g==", "integrity": "sha512-0rO9NTTueDXPqeWASThKEHX5AGF0FDhiPVRdkkWnKUOAtfPJz/qovZnIEpoC7q0DdmgbYYvK3if9Si8SdFNk0A==",
"dev": true, "dev": true,
"requires": { "requires": {
"acorn-hammerhead": "^0.2.0", "acorn-hammerhead": "^0.3.0",
"asar": "^2.0.1",
"bowser": "1.6.0", "bowser": "1.6.0",
"brotli": "^1.3.1", "brotli": "^1.3.1",
"crypto-md5": "^1.0.0", "crypto-md5": "^1.0.0",
"css": "2.2.3", "css": "2.2.3",
"esotope-hammerhead": "0.3.0", "esotope-hammerhead": "^0.4.0",
"iconv-lite": "0.4.11", "iconv-lite": "0.4.11",
"lodash": "^4.17.13", "lodash": "^4.17.13",
"lru-cache": "2.6.3", "lru-cache": "2.6.3",
@ -10554,7 +10944,6 @@
"nanoid": "^0.2.2", "nanoid": "^0.2.2",
"os-family": "^1.0.0", "os-family": "^1.0.0",
"parse5": "2.2.3", "parse5": "2.2.3",
"pify": "^2.3.0",
"pinkie": "1.0.0", "pinkie": "1.0.0",
"read-file-relative": "^1.2.0", "read-file-relative": "^1.2.0",
"semver": "5.5.0", "semver": "5.5.0",
@ -10593,12 +10982,6 @@
"integrity": "sha1-DE/EHBAAxea5PUiwP4CDg3g06fY=", "integrity": "sha1-DE/EHBAAxea5PUiwP4CDg3g06fY=",
"dev": true "dev": true
}, },
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
},
"pinkie": { "pinkie": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-1.0.0.tgz", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-1.0.0.tgz",
@ -10714,12 +11097,6 @@
"integrity": "sha1-5tZsVyzhWvJmcGrw/WELKoQd1EM=", "integrity": "sha1-5tZsVyzhWvJmcGrw/WELKoQd1EM=",
"dev": true "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": { "text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -10814,6 +11191,27 @@
"os-tmpdir": "~1.0.1" "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": { "to-arraybuffer": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
@ -11239,6 +11637,12 @@
"integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
"dev": true "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": { "unique-filename": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
@ -12240,29 +12644,56 @@
"dev": true "dev": true
}, },
"yargs": { "yargs": {
"version": "12.0.5", "version": "14.0.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.0.0.tgz",
"integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", "integrity": "sha512-ssa5JuRjMeZEUjg7bEL99AwpitxU/zWGAGpdj0di41pOEmJti8NR6kyUIJBkR78DTYNPZOU08luUo0GTHuB+ow==",
"dev": true, "dev": true,
"requires": { "requires": {
"cliui": "^4.0.0", "cliui": "^5.0.0",
"decamelize": "^1.2.0", "decamelize": "^1.2.0",
"find-up": "^3.0.0", "find-up": "^3.0.0",
"get-caller-file": "^1.0.1", "get-caller-file": "^2.0.1",
"os-locale": "^3.0.0",
"require-directory": "^2.1.1", "require-directory": "^2.1.1",
"require-main-filename": "^1.0.1", "require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0", "set-blocking": "^2.0.0",
"string-width": "^2.0.0", "string-width": "^3.0.0",
"which-module": "^2.0.0", "which-module": "^2.0.0",
"y18n": "^3.2.1 || ^4.0.0", "y18n": "^4.0.0",
"yargs-parser": "^11.1.1" "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": { "yargs-parser": {
"version": "11.1.1", "version": "13.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz",
"integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"camelcase": "^5.0.0", "camelcase": "^5.0.0",

View File

@ -9,7 +9,11 @@
"build:src": "concurrently \"npm run build:client\" \"npm run build:server\"", "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: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", "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": { "nodemonConfig": {
"ext": "ts", "ext": "ts",
@ -40,7 +44,6 @@
"koa-proxy": "^0.9.0", "koa-proxy": "^0.9.0",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"moment": "^2.24.0",
"normalize-url": "^4.3.0", "normalize-url": "^4.3.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"v-tooltip": "^2.0.2", "v-tooltip": "^2.0.2",
@ -55,7 +58,6 @@
"vuex-router-sync": "^5.0.0" "vuex-router-sync": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"fibers": "^4.0.1",
"@babel/core": "^7.5.5", "@babel/core": "^7.5.5",
"@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.5.5", "@babel/plugin-transform-runtime": "^7.5.5",
@ -69,6 +71,7 @@
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"css-loader": "^3.1.0", "css-loader": "^3.1.0",
"deepmerge": "^4.0.0", "deepmerge": "^4.0.0",
"fibers": "^4.0.1",
"file-loader": "^4.1.0", "file-loader": "^4.1.0",
"fork-ts-checker-webpack-plugin": "^1.4.3", "fork-ts-checker-webpack-plugin": "^1.4.3",
"friendly-errors-webpack-plugin": "^1.7.0", "friendly-errors-webpack-plugin": "^1.7.0",
@ -81,12 +84,12 @@
"rimraf": "^2.6.3", "rimraf": "^2.6.3",
"sass": "^1.22.9", "sass": "^1.22.9",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"testcafe": "^1.3.3", "testcafe": "^1.6.0",
"testcafe-vue-selectors": "^3.1.0",
"ts-loader": "^6.0.4", "ts-loader": "^6.0.4",
"ts-node": "^8.3.0", "ts-node": "^8.3.0",
"tslint": "^5.18.0", "tslint": "^5.18.0",
"typescript": "^3.5.3", "typescript": "^3.5.3",
"uniqid": "^5.0.3",
"url-loader": "^2.1.0", "url-loader": "^2.1.0",
"vue-loader": "^15.7.1", "vue-loader": "^15.7.1",
"vue-style-loader": "^4.1.2", "vue-style-loader": "^4.1.2",
@ -96,6 +99,7 @@
"webpack-cli": "^3.3.6", "webpack-cli": "^3.3.6",
"webpack-merge": "^4.2.1", "webpack-merge": "^4.2.1",
"webpack-node-externals": "^1.7.2", "webpack-node-externals": "^1.7.2",
"webpackbar": "^3.2.0" "webpackbar": "^3.2.0",
"yargs": "^14.0.0"
} }
} }

View File

@ -1,34 +1,41 @@
import Koa from 'koa' import Koa from 'koa'
import bodyparser from 'koa-bodyparser' import bodyparser from 'koa-bodyparser'
import proxy from 'koa-proxy' import proxy from 'koa-proxy'
import DefferedPromise from './utils/DeferredPromise'
import config from './config.js' import config from './config.js'
const { port, apiUrl } = config export default (async () => {
const isProduction = process.env.NODE_ENV === 'production' 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 // TODO replace proxy lib or write own middleware for log and flexibility
app.use(proxy({ app.use(proxy({
requestOptions: { requestOptions: {
strictSSL: false strictSSL: false
}, },
host: apiUrl, host: apiUrl,
match: /^\/api\//, match: /^\/api\//,
map: (path: string) => path.replace('/api', '') map: (path: string) => path.replace('/api', '')
})) }))
app.use(bodyparser()) app.use(bodyparser())
const setupServer = isProduction const setupServer = isProduction
? (await import('./build/setupProdServer')).default ? (await import('./build/setupProdServer')).default
: (await import('./build/setupDevServer')).default : (await import('./build/setupDevServer')).default
await setupServer(app) await setupServer(app)
app.listen(port, () => { app.listen(port, () => {
console.log(`[Info] Server is on at ${port}.`) console.log(`[Info] Server is on at ${port}.`)
}) promise.resolve()
} })
start() return promise
} catch (e) {
promise.reject(e)
}
})()

View 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
View 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
View 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
}

View 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
View 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
View 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
}

View File

@ -24,6 +24,6 @@
"dom", "dom",
"es2017", "es2017",
"es5" "es5"
], ]
} }
} }

4
scripts/ci-functions.sh Normal file
View 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
}