Began transition to Vue3.

Implemented logging in and logging out.

Implemented authenticated http client.

Laid some groundwork for SCSS.
This commit is contained in:
2021-07-09 21:47:40 -05:00
parent 54b1565537
commit 3d3c43b944
284 changed files with 20487 additions and 38338 deletions

View File

@@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

View File

@@ -0,0 +1,6 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
quote_type = double

View File

@@ -0,0 +1,33 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/vue3-essential',
'@vue/standard'
],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'quotes': ['error', 'double', 'avoid-escape'],
'semi': ['error', 'always'],
'indent': ['error', 4],
'comma-dangle': ['error', 'only-multiline'],
'space-before-function-paren': ['error', 'never']
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)'
],
env: {
mocha: true
}
}
]
}

25
MultiShop/client/.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
.DS_Store
node_modules
/dist
/tests/e2e/logs/
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,34 @@
# multishop
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Run your unit tests
```
npm run test:unit
```
### Run your end-to-end tests
```
npm run test:e2e
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View File

@@ -0,0 +1,5 @@
{
"include": [
"./src/**/*"
]
}

18567
MultiShop/client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
{
"name": "multishop",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.21.1",
"bootstrap": "^5.0.2",
"bootstrap-icons": "^1.5.0",
"core-js": "^3.6.5",
"oidc-client": "^1.11.5",
"register-service-worker": "^1.7.1",
"vue": "^3.0.0",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-e2e-webdriverio": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-pwa": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-unit-mocha": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"@vue/eslint-config-standard": "^5.1.2",
"@vue/test-utils": "^2.0.0-0",
"babel-eslint": "^10.1.0",
"chai": "^4.1.2",
"chromedriver": "91",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^7.0.0",
"sass": "^1.35.1",
"sass-loader": "^8.0.2",
"wdio-chromedriver-service": "^6.0.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00251 14.9297L0 1.07422H6.14651L8.00251 4.27503L9.84583 1.07422H16L8.00251 14.9297Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 215 B

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow:

View File

@@ -0,0 +1,70 @@
<template>
<div class="content light">
<nav class="navbar" id="nav">
<div class="container-fluid">
<router-link class="navbar-brand" to="/">MultiShop</router-link>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarContent"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<router-link
active-class="active"
class="nav-link"
to="/"
>Home</router-link
>
</li>
<li class="nav-item">
<router-link
active-class="active"
class="nav-link"
to="/about"
>About</router-link
>
</li>
</ul>
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
<li class="nav-item">
<ProfileDisplay>
</ProfileDisplay>
</li>
<li class="nav-item">
<ProfileLogIn>
</ProfileLogIn>
</li>
<li class="nav-item">
<ProfileLogOut>
</ProfileLogOut>
</li>
</ul>
</div>
</div>
</nav>
<router-view />
</div>
</template>
<script>
import "bootstrap/js/dist/collapse";
import "./assets/scss/main.scss";
import ProfileDisplay from "./components/ProfileDisplay.vue";
import ProfileLogIn from "./components/ProfileLogIn.vue";
import ProfileLogOut from "./components/ProfileLogOut.vue";
export default {
components: {
ProfileDisplay,
ProfileLogIn,
ProfileLogOut
},
mounted() {
this.$store.dispatch("attemptSilentAuthentication");
}
};
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,10 @@
@use "../../../node_modules/bootstrap/scss/bootstrap";
#app .content.light {
// Add light theme specific scss here.
nav.navbar {
@extend .navbar-expand-lg;
@extend .navbar-light;
@extend .bg-light;
}
}

View File

@@ -0,0 +1,12 @@
@use "light";
@import "../../../node_modules/bootstrap-icons/font/bootstrap-icons.css";
html, body, #app, #app > .content {
min-height: 100%;
height: 100%;
}
.content {
display: flex;
flex-direction: column;
}

View File

@@ -0,0 +1,50 @@
<template>
<a v-if="visible" href="authentication/profile" class="btn">
<slot v-if="username" :displayName="username" name="username">
{{ username }}
</slot>
<slot v-else-if="isProfileLoading" name="loading">
<WaitCircle class="spinner-grow-sm"></WaitCircle>
</slot>
<slot v-else name="unauthenticated">
<span>Not Logged In</span>
</slot>
</a>
</template>
<script>
import WaitCircle from "@/components/WaitCircle.vue";
export default {
components: {
WaitCircle
},
name: "ProfileDisplay",
props: {
showUnauthenticated: {
type: Boolean,
default: false,
required: false
}
},
data() {
return {};
},
computed: {
username() {
return this.$store.getters.username &&
!this.$store.state.identity.loading
? this.$store.getters.username
: null;
},
visible() {
return !this.showUnauthenticated
? this.$store.getters.isAuthenticated ||
this.$store.state.identity.loading
: true;
},
isProfileLoading() {
return this.$store.state.identity.loading;
}
}
};
</script>

View File

@@ -0,0 +1,21 @@
<template>
<button v-if="visible" @click="onClick" type="button" class="btn">
<slot>Log In</slot>
</button>
</template>
<script>
export default {
name: "ProfileLogin",
computed: {
visible() {
return !this.$store.getters.isAuthenticated && !this.$store.state.identity.loading;
}
},
methods: {
onClick() {
this.$store.dispatch("beginAuthentication");
}
}
};
</script>

View File

@@ -0,0 +1,21 @@
<template>
<button v-if="visible" @click="onClick" type="button" class="btn">
<slot>Log Out</slot>
</button>
</template>
<script>
export default {
name: "ProfileLogOut",
computed: {
visible() {
return this.$store.getters.isAuthenticated && !this.$store.state.identity.loading;
}
},
methods: {
onClick() {
this.$store.dispatch("beginDeauthentication");
}
}
};
</script>

View File

@@ -0,0 +1,39 @@
<template>
<form autocomplete="off">
<div class="input-group">
<input type="text" class="form-control" :placeholder="placeholder" v-model="query" @keyup.enter="onEnter">
<slot name="append"></slot>
<button class="btn btn-outline-primary" type="button" @click="onClick">
<i class="bi bi-search"></i>
</button>
</div>
</form>
</template>
<script>
export default {
props: {
placeholder: String,
searchCallback: Function
},
data() {
return {
query: "",
disabled: false
};
},
methods: {
onClick() {
this.disabled = true;
this.searchCallback(this.query);
this.disabled = false;
},
onEnter() {
this.disabled = true;
this.searchCallback(this.query);
this.disabled = false;
}
}
};
</script>

View File

@@ -0,0 +1,24 @@
<template>
<div>
<div class="mx-auto" id="spinner-container">
<div class="spinner-grow" role="status" v-bind="$attrs">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div>
<slot></slot>
</div>
</div>
</template>
<script>
export default {
inheritAttrs: false
};
</script>
<style lang="scss" scoped>
#spinner-container {
width: fit-content;
}
</style>

View File

@@ -0,0 +1,13 @@
import { createApp } from "vue";
import App from "./App.vue";
import "./registerServiceWorker";
import router from "./router";
import store from "./store";
import "./assets/scss/main.scss";
const app = createApp(App);
app.use(store);
app.use(router);
app.mount("#app");

View File

@@ -0,0 +1,32 @@
/* eslint-disable no-console */
import { register } from "register-service-worker";
if (process.env.NODE_ENV === "production") {
register(`${process.env.BASE_URL}service-worker.js`, {
ready() {
console.log(
"App is being served from cache by a service worker.\n" +
"For more details, visit https://goo.gl/AFskqB"
);
},
registered() {
console.log("Service worker has been registered.");
},
cached() {
console.log("Content has been cached for offline use.");
},
updatefound() {
console.log("New content is downloading.");
},
updated() {
console.log("New content is available; please refresh.");
},
offline() {
console.log("No internet connection found. App is running in offline mode.");
},
error(error) {
console.error("Error during service worker registration:", error);
}
});
}

View File

@@ -0,0 +1,13 @@
function authenticationGuard(app, to, from, next) {
if (to.authenticationRequired) {
if (app.$store.getters.identity.isAuthenticated) {
next();
} else {
app.$store.dispatch("beginAuthentication");
}
} else {
next();
}
}
export { authenticationGuard };

View File

@@ -0,0 +1,11 @@
import { createRouter, createWebHistory } from "vue-router";
import { authenticationGuard } from "./guards";
import { routes } from "./routes";
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
});
router.beforeEach((to, from, next) => authenticationGuard(router, to, from, next));
export default router;

View File

@@ -0,0 +1,25 @@
import Home from "../views/Home.vue";
import Authentication from "../views/Authentication.vue";
const routes = [
{
path: "/",
name: "Home",
component: Home
},
{
path: "/about",
name: "About",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ "../views/About.vue")
},
{
path: "/authentication/:action?",
name: "Authentication",
component: Authentication,
props: true,
}
];
export { routes };

View File

@@ -0,0 +1,13 @@
import { UserManager, WebStorageStateStore } from "oidc-client";
const userManager = new UserManager({
authority: window.location.origin,
client_id: "MultiShop",
redirect_uri: window.location.origin + "/authentication/login-callback",
post_logout_redirect_uri: window.location.origin + "/authentication/logout-callback",
response_type: "code",
scope: "openid profile",
userStore: new WebStorageStateStore({ store: window.localStorage }),
});
export { userManager };

View File

@@ -0,0 +1,23 @@
import axios from "axios";
let currentAuthorizationInterceptorID = null;
const http = axios.create({
baseURL: window.location.origin + "/api",
timeout: 2000,
});
function addBearerTokenInterceptor(token) {
currentAuthorizationInterceptorID = http.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${token}`;
return config;
}, (err) => {
console.error(err);
});
}
function removeBearerTokenInterceptor() {
http.interceptors.request.eject(currentAuthorizationInterceptorID);
}
export { http, addBearerTokenInterceptor, removeBearerTokenInterceptor };

View File

@@ -0,0 +1,31 @@
const prefix = "MultiShop";
function put(key, value) {
if (value == null) return false;
try {
localStorage.setItem(prefix + ":" + key, JSON.stringify(value));
} catch (error) {
console.error(error);
return false;
}
return true;
}
function get(key) {
if (!exists(key)) return null;
try {
return JSON.parse(localStorage.getItem(prefix + ":" + key));
} catch (error) {
console.error(error);
return null;
}
}
function remove(key) {
localStorage.removeItem(prefix + ":" + key);
}
function exists(key) {
return localStorage.getItem(prefix + ":" + key) != null;
}
export { put, get, remove, exists };

View File

@@ -0,0 +1,99 @@
import router from "../router";
import { userManager } from "../services/authentication";
import { addBearerTokenInterceptor, removeBearerTokenInterceptor } from "../services/http";
import { get, put } from "../services/persistence";
const identity = {
state: () => ({
user: null,
loading: true,
callbackPath: null,
}),
getters: {
isAuthenticated(state) {
return state.user ? !state.user.expired : false;
},
username(state) {
return (state.user && state.user.profile.name) ? state.user.profile.name : null;
},
authCallbackLocation(state) {
return state.callbackPath ? state.callbackPath : get("callbackPath");
}
},
mutations: {
login(state, { user }) {
if (!user) return;
state.user = user;
addBearerTokenInterceptor(user.access_token);
},
logout(state) {
state.user = null;
removeBearerTokenInterceptor();
},
beginAuthenticating(state) {
state.loading = true;
},
endAuthenticating(state) {
state.loading = false;
},
authCallbackLocation(state) {
state.callbackPath = window.location.pathname;
put("callbackPath", state.callbackPath);
}
},
actions: {
async loadUser(context) {
context.commit("beginAuthenticating");
const user = await userManager.getUser();
if (user != null) {
context.commit("login", { user });
}
context.commit("endAuthenticating");
},
async attemptSilentAuthentication(context) {
if (context.getters.isAuthenticated) return;
context.commit("beginAuthenticating");
context.dispatch("loadUser");
if (!context.getters.isAuthenticated) {
try {
const user = await userManager.signinSilent({
redirect_uri: window.location.origin + "/authentication/silent-login-callback"
});
context.commit("login", { user });
} catch { }
context.commit("endAuthenticating");
}
},
async beginAuthentication(context) {
if (context.getters.isAuthenticated) return;
context.commit("beginAuthenticating");
context.dispatch("loadUser");
if (!context.getters.isAuthenticated) {
context.commit("authCallbackLocation");
userManager.signinRedirect();
}
},
async beginDeauthentication(context) {
if (!context.getters.isAuthenticated) return;
context.commit("beginAuthenticating");
context.commit("authCallbackLocation");
await userManager.removeUser();
userManager.signoutRedirect();
},
async completeAuthentication(context, { user }) {
if (!user) return;
context.commit("login", { user });
router.push(context.getters.authCallbackLocation);
context.commit("endAuthenticating");
},
async completeDeauthentication(context) {
if (!context.getters.isAuthenticated) return;
context.commit("logout");
router.push(context.getters.authCallbackLocation);
context.commit("endAuthenticating");
}
},
};
export { identity };

View File

@@ -0,0 +1,16 @@
import { createStore } from "vuex";
import { identity } from "./identity";
export default createStore({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
identity: identity,
},
});

View File

@@ -0,0 +1,5 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

View File

@@ -0,0 +1,68 @@
<template>
<div class="info">
<WaitCircle class="text-primary">
<span class="my-1 text-muted"
>Hang on just a sec! Waiting for authentication stuff to
finish.</span
>
</WaitCircle>
<div class="my-1 text-muted" v-show="longTimePassed">
This seems to be taking longer than expected...
<router-link to="/">Return home</router-link>?
</div>
</div>
</template>
<script>
import WaitCircle from "@/components/WaitCircle.vue";
import { userManager } from "../services/authentication";
export default {
name: "Authentication",
props: {
action: String
},
components: {
WaitCircle
},
data() {
return {
longTimePassed: false
};
},
mounted() {
setTimeout(() => (this.longTimePassed = true), 5000);
this.completeCallback();
},
methods: {
async completeCallback() {
if (this.action === "login-callback") {
const user = await userManager.signinRedirectCallback();
this.$store.dispatch("completeAuthentication", { user });
} else if (this.action === "silent-login-callback") {
await userManager.signinSilentCallback();
} else if (this.action === "logout-callback") {
await userManager.signoutRedirectCallback();
this.$store.dispatch("completeDeauthentication");
} else {
console.warn("Unknown callback: " + this.action);
}
}
},
watch: {
async action() {
this.completeCallback();
}
}
};
</script>
<style lang="scss" scoped>
.info {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
</style>

View File

@@ -0,0 +1,13 @@
<template>
<div class="home">
<img alt="Vue logo" src="../assets/images/logo.png">
</div>
</template>
<script>
export default {
name: "Home",
components: {
}
};
</script>

View File

@@ -0,0 +1,10 @@
module.exports = {
plugins: ['wdio'],
extends: 'plugin:wdio/recommended',
env: {
mocha: true
},
rules: {
strict: 'off'
}
}

View File

@@ -0,0 +1,15 @@
class App {
/**
* elements
*/
get heading () { return $('h1') }
/**
* methods
*/
open (path = '/') {
browser.url(path)
}
}
module.exports = new App()

View File

@@ -0,0 +1,8 @@
const App = require('../pageobjects/app.page')
describe('Vue.js app', () => {
it('should open and render', () => {
App.open()
expect(App.heading).toHaveText('Welcome to Your Vue.js App')
})
})

View File

@@ -0,0 +1,13 @@
import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallowMount(HelloWorld, {
props: { msg }
})
expect(wrapper.text()).to.include(msg)
})
})

View File

@@ -0,0 +1,24 @@
const { config } = require('./wdio.shared.conf')
exports.config = {
/**
* base config
*/
...config,
/**
* config for local testing
*/
maxInstances: 1,
services: ['chromedriver'],
capabilities: [
{
browserName: 'chrome',
acceptInsecureCerts: true,
'goog:chromeOptions': {
args: process.argv.includes('--headless')
? ['--headless', '--disable-gpu']
: []
}
}
]
}

View File

@@ -0,0 +1,41 @@
const { config } = require('./wdio.shared.conf')
const BUILD_ID = Math.ceil(Date.now() / 1000)
exports.config = {
/**
* base config
*/
...config,
/**
* config for testing on Sauce Labs
*/
user: process.env.SAUCE_USERNAME,
key: process.env.SAUCE_ACCESS_KEY,
region: 'us',
headless: process.argv.includes('--headless'),
services: [
['sauce', {
sauceConnect: true,
tunnelIdentifier: 'Vue.js Integration tests'
}]
],
maxInstances: 10,
capabilities: [{
browserName: 'firefox',
browserVersion: 'latest',
platformName: 'Windows 10',
'sauce:options': {
build: `Build ${BUILD_ID}`
}
}, {
browserName: 'chrome',
browserVersion: 'latest',
platformName: 'Windows 10',
'sauce:options': {
build: `Build ${BUILD_ID}`
}
}]
}

View File

@@ -0,0 +1,210 @@
/* eslint-disable no-unused-vars */
const path = require('path')
exports.config = {
// ==================
// Specify Test Files
// ==================
// Define which test specs should run. The pattern is relative to the directory
// from which `wdio` was called. Notice that, if you are calling `wdio` from an
// NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
// directory is where your package.json resides, so `wdio` will be called from there.
//
specs: [
path.join(__dirname, '/tests/e2e/**/*.spec.js')
],
// Patterns to exclude.
exclude: [
// 'test/spec/multibrowser/**',
// 'test/spec/mobile/**'
],
//
// ===================
// Test Configurations
// ===================
// Define all options that are relevant for the WebdriverIO instance here
//
// Level of logging verbosity: trace | debug | info | warn | error | silent
logLevel: 'trace',
//
// Set directory to store all logs into
outputDir: path.join(__dirname, 'tests/e2e/logs'),
//
// If you only want to run your tests until a specific amount of tests have failed use
// bail (default is 0 - don't bail, run all tests).
bail: 0,
//
// Default timeout for all waitFor* commands.
waitforTimeout: 1000,
//
// Framework you want to run your specs with.
// The following are supported: Mocha, Jasmine, and Cucumber
// see also: https://webdriver.io/docs/frameworks.html
//
// Make sure you have the wdio adapter package for the specific framework
// installed before running any tests.
framework: 'mocha',
//
// The number of times to retry the entire specfile when it fails as a whole
specFileRetries: 1,
//
// Retried specfiles are inserted at the beginning of the queue and retried immediately
specFileRetriesDeferred: false,
//
// Test reporter for stdout.
// The only one supported by default is 'dot'
// see also: https://webdriver.io/docs/dot-reporter.html
reporters: ['spec'],
//
// Options to be passed to Mocha.
// See the full list at http://mochajs.org/
mochaOpts: {
ui: 'bdd',
timeout: 30000
},
//
// =====
// Hooks
// =====
// WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
// it and to build services around it. You can either apply a single function or an array of
// methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
// resolved to continue.
//
/**
* Gets executed once before all workers get launched.
* @param {Object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
*/
onPrepare: function (config, capabilities) {
},
/**
* Gets executed before a worker process is spawned and can be used to initialise specific service
* for that worker as well as modify runtime environments in an async fashion.
* @param {String} cid capability id (e.g 0-0)
* @param {[type]} caps object containing capabilities for session that will be spawn in the worker
* @param {[type]} specs specs to be run in the worker process
* @param {[type]} args object that will be merged with the main configuration once worker is initialised
* @param {[type]} execArgv list of string arguments passed to the worker process
*/
onWorkerStart: function (cid, caps, specs, args, execArgv) {
},
/**
* Gets executed just before initialising the webdriver session and test framework. It allows you
* to manipulate configurations depending on the capability or spec.
* @param {Object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that are to be run
*/
beforeSession: function (config, capabilities, specs) {
},
/**
* Gets executed before test execution begins. At this point you can access to all global
* variables like `browser`. It is the perfect place to define custom commands.
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that are to be run
*/
before: function (capabilities, specs) {
},
/**
* Hook that gets executed before the suite starts
* @param {Object} suite suite details
*/
beforeSuite: function (suite) {
},
/**
* Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
* beforeEach in Mocha)
* stepData and world are Cucumber framework specific
*/
beforeHook: function (test, context/*, stepData, world */) {
},
/**
* Hook that gets executed _after_ a hook within the suite ends (e.g. runs after calling
* afterEach in Mocha)
* stepData and world are Cucumber framework specific
*/
afterHook: function (test, context, { error, result, duration, passed, retries }/*, stepData, world */) {
},
/**
* Function to be executed before a test (in Mocha/Jasmine) starts.
*/
beforeTest: function (test, context) {
},
//
/**
* Runs before a WebdriverIO command gets executed.
* @param {String} commandName command name
* @param {Array} args arguments that command would receive
*/
beforeCommand: function (commandName, args) {
},
/**
* Runs after a WebdriverIO command gets executed.
* @param {String} commandName hook command name
* @param {Array} args arguments that command would receive
* @param {Number} result 0 - command success, 1 - command error
* @param {Object} error error object if any
*/
afterCommand: function (commandName, args, result, error) {
},
/**
* Function to be executed after a test (in Mocha/Jasmine) ends.
*/
afterTest: function (test, context, { error, result, duration, passed, retries }) {
},
/**
* Hook that gets executed after the suite has ended
* @param {Object} suite suite details
*/
afterSuite: function (suite) {
},
/**
* Gets executed after all tests are done. You still have access to all global variables from
* the test.
* @param {Number} result 0 - test pass, 1 - test fail
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that ran
*/
after: function (result, capabilities, specs) {
},
/**
* Gets executed right after terminating the webdriver session.
* @param {Object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that ran
*/
afterSession: function (config, capabilities, specs) {
},
/**
* Gets executed after all workers got shut down and the process is about to exit. An error
* thrown in the onComplete hook will result in the test run failing.
* @param {Object} exitCode 0 - success, 1 - fail
* @param {Object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {<Object>} results object containing test results
*/
onComplete: function (exitCode, config, capabilities, results) {
},
/**
* Gets executed when a refresh happens.
* @param {String} oldSessionId session ID of the old session
* @param {String} newSessionId session ID of the new session
*/
onReload: function (oldSessionId, newSessionId) {
},
//
// Cucumber specific hooks
beforeFeature: function (uri, feature, scenarios) {
},
beforeScenario: function (uri, feature, scenario, sourceLocation) {
},
beforeStep: function ({ uri, feature, step }, context) {
},
afterStep: function ({ uri, feature, step }, context, { error, result, duration, passed, retries }) {
},
afterScenario: function (uri, feature, scenario, result, sourceLocation) {
},
afterFeature: function (uri, feature, scenarios) {
}
}