Introduce Cypress

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2019-10-05 19:07:50 +02:00
parent 674d162510
commit 7f65428b38
27 changed files with 1041 additions and 845 deletions

View File

@ -4,6 +4,7 @@ stages:
- deps - deps
- front - front
- back - back
- e2e
- deploy - deploy
variables: variables:
@ -19,6 +20,7 @@ variables:
MOBILIZON_DATABASE_DBNAME: $POSTGRES_DB MOBILIZON_DATABASE_DBNAME: $POSTGRES_DB
MOBILIZON_DATABASE_HOST: $POSTGRES_HOST MOBILIZON_DATABASE_HOST: $POSTGRES_HOST
GEOLITE_CITIES_PATH: "/usr/share/GeoIP/GeoLite2-City.mmdb" GEOLITE_CITIES_PATH: "/usr/share/GeoIP/GeoLite2-City.mmdb"
MOBILIZON_INSTANCE_REGISTRATIONS_OPEN: "true"
setup_elixir_deps: setup_elixir_deps:
stage: deps stage: deps
@ -144,3 +146,26 @@ mix:
paths: paths:
- deps - deps
- _build - _build
e2e:
stage: e2e
services:
- name: mdillon/postgis:10
alias: postgres
script:
- mix deps.get
- cd js
- yarn install
- yarn run build
- cd ../
- MIX_ENV=e2e mix ecto.create
- MIX_ENV=e2e mix ecto.migrate
- MIX_ENV=e2e mix phx.server &
- cd js
- npx wait-on http://localhost:4000
- npx cypress run --record --parallel --key $CYPRESS_KEY
artifacts:
expire_in: 2 day
paths:
- js/tests/e2e/screenshots/**/*.png
- js/tests/e2e/videos/**/*.mp4

View File

@ -3,7 +3,7 @@
# #
# This configuration file is loaded before any dependency and # This configuration file is loaded before any dependency and
# is restricted to this project. # is restricted to this project.
use Mix.Config import Config
# General application configuration # General application configuration
config :mobilizon, config :mobilizon,
@ -71,10 +71,6 @@ config :logger, :console,
format: "$time $metadata[$level] $message\n", format: "$time $metadata[$level] $message\n",
metadata: [:request_id] metadata: [:request_id]
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
config :mobilizon, MobilizonWeb.Guardian, config :mobilizon, MobilizonWeb.Guardian,
issuer: "mobilizon", issuer: "mobilizon",
secret_key: "ty0WM7YBE3ojvxoUQxo8AERrNpfbXnIJ82ovkPdqbUFw31T5LcK8wGjaOiReVQjo" secret_key: "ty0WM7YBE3ojvxoUQxo8AERrNpfbXnIJ82ovkPdqbUFw31T5LcK8wGjaOiReVQjo"
@ -136,3 +132,7 @@ config :mobilizon, Mobilizon.Service.Geospatial.GoogleMaps,
config :mobilizon, Mobilizon.Service.Geospatial.MapQuest, config :mobilizon, Mobilizon.Service.Geospatial.MapQuest,
api_key: System.get_env("GEOSPATIAL_MAP_QUEST_API_KEY") || nil api_key: System.get_env("GEOSPATIAL_MAP_QUEST_API_KEY") || nil
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

View File

@ -1,4 +1,4 @@
use Mix.Config import Config
# For development, we disable any cache and enable # For development, we disable any cache and enable
# debugging and code reloading. # debugging and code reloading.

24
config/e2e.exs Normal file
View File

@ -0,0 +1,24 @@
import Config
import_config "dev.exs"
config :mobilizon, MobilizonWeb.Endpoint,
http: [
port: 4000
],
url: [
host: "localhost",
port: 4000,
scheme: "http"
],
debug_errors: true,
code_reloader: false,
check_origin: false,
# Somehow this can't be merged properly with the dev config some we got this…
watchers: [
yarn: [cd: Path.expand("../js", __DIR__)]
]
config :mobilizon, sql_sandbox: true
config :mobilizon, Mobilizon.Storage.Repo, pool: Ecto.Adapters.SQL.Sandbox

View File

@ -1,4 +1,4 @@
use Mix.Config import Config
config :mobilizon, MobilizonWeb.Endpoint, config :mobilizon, MobilizonWeb.Endpoint,
http: [:inet6, port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4000], http: [:inet6, port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4000],

View File

@ -1,4 +1,4 @@
use Mix.Config import Config
config :mobilizon, :instance, config :mobilizon, :instance,
name: "Test instance", name: "Test instance",

View File

@ -1,10 +1,10 @@
FROM elixir:latest FROM elixir:latest
LABEL maintainer="Thomas Citharel <tcit@tcit.fr>" LABEL maintainer="Thomas Citharel <tcit@tcit.fr>"
ENV REFRESHED_AT=2019-07-03 ENV REFRESHED_AT=2019-10-06
RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg xvfb libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash && apt-get install nodejs -yq RUN curl -sL https://deb.nodesource.com/setup_10.x | bash && apt-get install nodejs -yq
RUN npm install -g yarn RUN npm install -g yarn wait-on
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN mix local.hex --force && mix local.rebar --force RUN mix local.hex --force && mix local.rebar --force
RUN curl http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz --output GeoLite2-City.tar.gz -s && tar zxf GeoLite2-City.tar.gz && mkdir -p /usr/share/GeoIP && mv GeoLite2-City_*/GeoLite2-City.mmdb /usr/share/GeoIP/GeoLite2-City.mmdb RUN curl http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz --output GeoLite2-City.tar.gz -s && tar zxf GeoLite2-City.tar.gz && mkdir -p /usr/share/GeoIP && mv GeoLite2-City_*/GeoLite2-City.mmdb /usr/share/GeoIP/GeoLite2-City.mmdb

2
js/.gitignore vendored
View File

@ -3,6 +3,8 @@ node_modules
/dist /dist
/tests/e2e/reports/ /tests/e2e/reports/
/tests/e2e/screenshots/
/tests/e2e/videos/
selenium-debug.log selenium-debug.log
# local env files # local env files

7
js/cypress.json Normal file
View File

@ -0,0 +1,7 @@
{
"pluginsFile": "tests/e2e/plugins/index.js",
"projectId": "86dpkx",
"baseUrl": "http://localhost:4000",
"viewportWidth": 1920,
"viewportHeight": 1080
}

View File

@ -4,11 +4,11 @@
"private": true, "private": true,
"scripts": { "scripts": {
"build": "vue-cli-service build", "build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e",
"lint": "vue-cli-service lint", "lint": "vue-cli-service lint",
"analyze-bundle": "yarn run build -- --report-json && webpack-bundle-analyzer ../priv/static/report.json", "analyze-bundle": "yarn run build -- --report-json && webpack-bundle-analyzer ../priv/static/report.json",
"dev": "vue-cli-service build --watch", "dev": "vue-cli-service build --watch",
"test:e2e": "vue-cli-service test:e2e",
"test:unit": "vue-cli-service test:unit",
"vue-i18n-extract": "vue-i18n-extract" "vue-i18n-extract": "vue-i18n-extract"
}, },
"dependencies": { "dependencies": {
@ -44,7 +44,7 @@
"@types/lodash": "^4.14.141", "@types/lodash": "^4.14.141",
"@types/mocha": "^5.2.6", "@types/mocha": "^5.2.6",
"@vue/cli-plugin-babel": "^3.6.0", "@vue/cli-plugin-babel": "^3.6.0",
"@vue/cli-plugin-e2e-nightwatch": "^3.6.0", "@vue/cli-plugin-e2e-cypress": "^4.0.0-rc.7",
"@vue/cli-plugin-pwa": "^3.6.0", "@vue/cli-plugin-pwa": "^3.6.0",
"@vue/cli-plugin-typescript": "^3.6.0", "@vue/cli-plugin-typescript": "^3.6.0",
"@vue/cli-plugin-unit-mocha": "^3.6.0", "@vue/cli-plugin-unit-mocha": "^3.6.0",

View File

@ -87,6 +87,7 @@ export default class App extends Vue {
} }
body { body {
background: #f6f7f8; // background: #f7f8fa;
background: #ebebeb;
} }
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<b-navbar type="is-secondary" shadow wrapper-class="container"> <b-navbar type="is-secondary" wrapper-class="container">
<template slot="brand"> <template slot="brand">
<b-navbar-item tag="router-link" :to="{ name: RouteName.HOME }"><logo /></b-navbar-item> <b-navbar-item tag="router-link" :to="{ name: RouteName.HOME }"><logo /></b-navbar-item>
</template> </template>

View File

@ -1,86 +1,99 @@
<template> <template>
<div class="container" v-if="config"> <div>
<section class="hero is-info" v-if="!currentUser.id || !currentActor"> <section class="hero is-medium is-light is-bold" v-if="!currentUser.id || !currentActor.id">
<div class="hero-body"> <div class="hero-body">
<div> <div class="container">
<h1 class="title">{{ config.name }}</h1> <div class="columns">
<h2 class="subtitle">{{ config.description }}</h2> <div class="column">
<router-link class="button" :to="{ name: RouteName.REGISTER }" v-if="config.registrationsOpen"> <h1 class="title">{{ config.name }}</h1>
{{ $t('Sign up') }} <h2 class="subtitle">{{ config.description }}</h2>
</router-link> <router-link class="button" :to="{ name: RouteName.REGISTER }" v-if="config.registrationsOpen">
<p v-else> {{ $t('Sign up') }}
{{ $t("This instance isn't opened to registrations, but you can register on other instances.") }} </router-link>
</p> <p v-else>
{{ $t("This instance isn't opened to registrations, but you can register on other instances.") }}
</p>
</div>
<div class="column">
<div class="card-image">
<figure class="image is-square">
<img src="https://joinmobilizon.org/img/en/events-mobilizon.png" />
</figure>
</div>
</div>
</div>
</div>
</div> </div>
</div> </section>
</section> <div class="container" v-if="config">
<section v-else-if="currentActor"> <section v-if="currentActor.id">
<b-message type="is-info"> <b-message type="is-info">
{{ $t('Welcome back {username}', { username: currentActor.displayName() }) }} {{ $t('Welcome back {username}', { username: currentActor.displayName() }) }}
</b-message> </b-message>
</section> </section>
<section v-else-if="currentActor && goingToEvents.size > 0" class="container"> <section v-else-if="currentActor && goingToEvents.size > 0" class="container">
<h3 class="title"> <h3 class="title">
{{ $t("Upcoming") }} {{ $t("Upcoming") }}
</h3> </h3>
<b-loading :active.sync="$apollo.loading"></b-loading> <b-loading :active.sync="$apollo.loading"></b-loading>
<div v-for="row in goingToEvents" class="upcoming-events"> <div v-for="row in goingToEvents" class="upcoming-events" :key="row[0]">
<span class="date-component-container" v-if="isInLessThanSevenDays(row[0])"> <span class="date-component-container" v-if="isInLessThanSevenDays(row[0])">
<date-component :date="row[0]"></date-component> <date-component :date="row[0]"></date-component>
<h3 class="subtitle" <h3 class="subtitle"
v-if="isToday(row[0])"> v-if="isToday(row[0])">
{{ $tc('You have one event today.', row[1].length, {count: row[1].length}) }} {{ $tc('You have one event today.', row[1].length, {count: row[1].length}) }}
</h3> </h3>
<h3 class="subtitle" <h3 class="subtitle"
v-else-if="isTomorrow(row[0])"> v-else-if="isTomorrow(row[0])">
{{ $tc('You have one event tomorrow.', row[1].length, {count: row[1].length}) }} {{ $tc('You have one event tomorrow.', row[1].length, {count: row[1].length}) }}
</h3> </h3>
<h3 class="subtitle" <h3 class="subtitle"
v-else-if="isInLessThanSevenDays(row[0])"> v-else-if="isInLessThanSevenDays(row[0])">
{{ $tc('You have one event in {days} days.', row[1].length, {count: row[1].length, days: calculateDiffDays(row[0])}) }} {{ $tc('You have one event in {days} days.', row[1].length, {count: row[1].length, days: calculateDiffDays(row[0])}) }}
</h3> </h3>
</span>
<div>
<EventListCard
v-for="participation in row[1]"
v-if="isInLessThanSevenDays(row[0])"
:key="participation[1].event.uuid"
:participation="participation[1]"
/>
</div>
</div>
<span class="view-all">
<router-link :to=" { name: RouteName.MY_EVENTS }">{{ $t('View everything')}} >></router-link>
</span> </span>
</section>
<section v-if="currentActor && lastWeekEvents.length > 0">
<h3 class="title">
{{ $t("Last week") }}
</h3>
<b-loading :active.sync="$apollo.loading"></b-loading>
<div> <div>
<EventListCard <EventListCard
v-for="participation in row[1]" v-for="participation in lastWeekEvents"
v-if="isInLessThanSevenDays(row[0])" :key="participation.id"
:key="participation[1].event.uuid" :participation="participation"
:participation="participation[1]" :options="{ hideDate: false }"
/> />
</div> </div>
</div> </section>
<span class="view-all"> <section>
<router-link :to=" { name: RouteName.MY_EVENTS }">{{ $t('View everything')}} >></router-link> <h3 class="events-nearby title">{{ $t('Events nearby you') }}</h3>
</span> <b-loading :active.sync="$apollo.loading"></b-loading>
</section> <div v-if="events.length > 0" class="columns is-multiline">
<section v-if="currentActor && lastWeekEvents.length > 0"> <div class="column is-one-third-desktop" v-for="event in events.slice(0, 6)" :key="event.uuid">
<h3 class="title"> <EventCard
{{ $t("Last week") }} :event="event"
</h3> />
<b-loading :active.sync="$apollo.loading"></b-loading> </div>
<div>
<EventListCard
v-for="participation in lastWeekEvents"
:key="participation.id"
:participation="participation"
:options="{ hideDate: false }"
/>
</div>
</section>
<section>
<h3 class="events-nearby title">{{ $t('Events nearby you') }}</h3>
<b-loading :active.sync="$apollo.loading"></b-loading>
<div v-if="events.length > 0" class="columns is-multiline">
<div class="column is-one-third-desktop" v-for="event in events.slice(0, 6)" :key="event.uuid">
<EventCard
:event="event"
/>
</div> </div>
</div> <b-message v-else type="is-danger">
<b-message v-else type="is-danger"> {{ $t('No events found') }}
{{ $t('No events found') }} </b-message>
</b-message> </section>
</section> </div>
</div> </div>
</template> </template>
@ -260,6 +273,8 @@ export default class Home extends Vue {
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped> <style lang="scss" scoped>
@import "@/variables.scss";
.search-autocomplete { .search-autocomplete {
border: 1px solid #dbdbdb; border: 1px solid #dbdbdb;
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
@ -292,4 +307,14 @@ export default class Home extends Vue {
text-decoration: underline; text-decoration: underline;
} }
} }
section.hero {
margin-top: -3px;
background: lighten($secondary, 20%);
.column figure.image img {
width: 480px;
height: 350px;
}
}
</style> </style>

View File

@ -8,95 +8,93 @@
</div> </div>
</section> </section>
<section> <section>
<div class="container"> <div class="columns">
<div class="columns is-mobile"> <div class="column">
<div class="column"> <div class="content">
<div class="content"> <h3 class="title">{{ $t('Features') }}</h3>
<h3 class="title">{{ $t('Features') }}</h3> <ul>
<ul> <li>{{ $t('Create your communities and your events') }}</li>
<li>{{ $t('Create your communities and your events') }}</li> <li>{{ $t('Other stuff…') }}</li>
<li>{{ $t('Other stuff…') }}</li> </ul>
</ul>
</div>
<i18n path="Learn more on" tag="p">
<a target="_blank" href="https://joinmobilizon.org">joinmobilizon.org</a>
</i18n>
<hr>
<div class="content">
<h3 class="title">{{ $t('About this instance') }}</h3>
<p>
{{ $t("Your local administrator resumed it's policy:") }}
</p>
<ul>
<li>{{ $t('Please be nice to each other') }}</li>
<li>{{ $t('meditate a bit') }}</li>
</ul>
<p>
{{ $t('Please read the full rules') }}
</p>
</div>
</div> </div>
<div class="column"> <i18n path="Learn more on" tag="p">
<form @submit="submit"> <a target="_blank" href="https://joinmobilizon.org">joinmobilizon.org</a>
<b-field </i18n>
:label="$t('Email')" <hr>
:type="errors.email ? 'is-danger' : null" <div class="content">
:message="errors.email" <h3 class="title">{{ $t('About this instance') }}</h3>
> <p>
<b-input {{ $t("Your local administrator resumed it's policy:") }}
aria-required="true" </p>
required <ul>
type="email" <li>{{ $t('Please be nice to each other') }}</li>
v-model="credentials.email" <li>{{ $t('meditate a bit') }}</li>
@blur="showGravatar = true" </ul>
@focus="showGravatar = false" <p>
/> {{ $t('Please read the full rules') }}
</b-field> </p>
</div>
</div>
<div class="column">
<form @submit="submit">
<b-field
:label="$t('Email')"
:type="errors.email ? 'is-danger' : null"
:message="errors.email"
>
<b-input
aria-required="true"
required
type="email"
v-model="credentials.email"
@blur="showGravatar = true"
@focus="showGravatar = false"
/>
</b-field>
<b-field <b-field
:label="$t('Password')" :label="$t('Password')"
:type="errors.password ? 'is-danger' : null" :type="errors.password ? 'is-danger' : null"
:message="errors.password" :message="errors.password"
> >
<b-input <b-input
aria-required="true" aria-required="true"
required required
type="password" type="password"
password-reveal password-reveal
minlength="6" minlength="6"
v-model="credentials.password" v-model="credentials.password"
/> />
</b-field> </b-field>
<b-field grouped> <b-field grouped>
<div class="control"> <div class="control">
<button type="button" class="button is-primary" @click="submit()"> <button type="button" class="button is-primary" @click="submit()">
{{ $t('Register') }} {{ $t('Register') }}
</button> </button>
</div> </div>
<div class="control"> <div class="control">
<router-link <router-link
class="button is-text" class="button is-text"
:to="{ name: RouteName.RESEND_CONFIRMATION, params: { email: credentials.email }}" :to="{ name: RouteName.RESEND_CONFIRMATION, params: { email: credentials.email }}"
> >
{{ $t("Didn't receive the instructions ?") }} {{ $t("Didn't receive the instructions ?") }}
</router-link> </router-link>
</div> </div>
<div class="control"> <div class="control">
<router-link <router-link
class="button is-text" class="button is-text"
:to="{ name: RouteName.LOGIN, params: { email: credentials.email, password: credentials.password }}" :to="{ name: RouteName.LOGIN, params: { email: credentials.email, password: credentials.password }}"
:disabled="sendingValidation" :disabled="sendingValidation"
> >
{{ $t('Login') }} {{ $t('Login') }}
</router-link> </router-link>
</div> </div>
</b-field> </b-field>
</form> </form>
<div v-if="errors.length > 0"> <div v-if="errors.length > 0">
<b-message type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message> <b-message type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,24 @@
// https://docs.cypress.io/guides/guides/plugins-guide.html
// if you need a custom webpack configuration you can uncomment the following import
// and then use the `file:preprocessor` event
// as explained in the cypress docs
// https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples
/* eslint-disable import/no-extraneous-dependencies, global-require, arrow-body-style */
// const webpack = require('@cypress/webpack-preprocessor')
module.exports = (on, config) => {
// on('file:preprocessor', webpack({
// webpackOptions: require('@vue/cli-service/webpack.config'),
// watchOptions: {}
// }))
return Object.assign({}, config, {
fixturesFolder: 'tests/e2e/fixtures',
integrationFolder: 'tests/e2e/specs',
screenshotsFolder: 'tests/e2e/screenshots',
videosFolder: 'tests/e2e/videos',
supportFile: 'tests/e2e/support/index.js'
})
}

View File

@ -0,0 +1,2 @@
// Set the en-US language just in case
export const onBeforeLoad = (window) => Object.defineProperty(window.navigator, 'language', { value: 'en-US' });

View File

@ -0,0 +1,42 @@
// https://docs.cypress.io/api/introduction/api.html
import { onBeforeLoad } from './browser-language';
beforeEach(() => {
cy.restoreLocalStorage();
});
afterEach(() => {
cy.saveLocalStorage();
});
describe('Homepage', () => {
it('Checks the footer', () => {
cy.visit('/', { onBeforeLoad });
cy.get('#mobilizon').find('footer').contains('The Mobilizon Contributors');
cy.contains('About').should('have.attr', 'href').and('eq', 'https://joinmobilizon.org');
cy.contains('License').should('have.attr', 'href').and('eq', 'https://framagit.org/framasoft/mobilizon/blob/master/LICENSE');
});
it('Tries to register from the hero section', () => {
cy.visit('/', { onBeforeLoad });
cy.get('.hero-body').contains('Sign up').click();
cy.url().should('include', '/register/user');
});
it('Tries to register from the navbar', () => {
cy.visit('/', { onBeforeLoad });
cy.get('nav.navbar').contains('Sign up').click();
cy.url().should('include', '/register/user');
});
it('Tries to connect from the navbar', () => {
cy.visit('/', { onBeforeLoad });
cy.get('nav.navbar').contains('Log in').click();
cy.url().should('include', '/login');
});
});

View File

@ -0,0 +1,45 @@
import { onBeforeLoad } from './browser-language';
beforeEach(() => {
cy.restoreLocalStorage();
});
afterEach(() => {
cy.saveLocalStorage();
});
describe('Login', () => {
it('Tests that everything is present', () => {
cy.visit('/login', { onBeforeLoad });
cy.get('form .field').first().contains('label', 'Email');
cy.get('form .field').last().contains('label', 'Password');
cy.get('form').contains('button.button', 'Login');
cy.get('form').contains('.control a.button', 'Forgot your password ?').click();
cy.url().should('include', '/password-reset/send');
cy.go('back');
cy.get('form').contains('.control a.button', 'Register').click();
cy.url().should('include', '/register/user');
cy.go('back');
});
it('Tries to login with incorrect credentials', () => {
cy.visit('/login', { onBeforeLoad });
cy.get('input[type=email]').type('notanemail').should('have.value', 'notanemail');
cy.get('input[type=password]').click();
cy.contains('button.button.is-primary.is-large', 'Login').click();
cy.get('form .field').first().contains('p.help.is-danger', 'Please include an \'@\' in the email address.');
cy.get('form .field').last().contains('p.help.is-danger', 'Please fill out this field.');
});
it('Tries to login with invalid credentials', () => {
cy.visit('/login', { onBeforeLoad });
cy.get('input[type=email]').type('test@email.com').should('have.value', 'test@email.com');
cy.get('input[type=password]').type('badPassword').should('have.value', 'badPassword');
cy.contains('button.button.is-primary.is-large', 'Login').click();
cy.contains('.message.is-danger', 'User with email not found');
});
});

View File

@ -0,0 +1,69 @@
import { onBeforeLoad } from './browser-language';
beforeEach(() => {
cy.restoreLocalStorage();
cy.checkoutSession();
});
afterEach(() => {
cy.saveLocalStorage();
cy.dropSession();
});
describe('Registration', () => {
it('Tests that everything is present', () => {
cy.visit('/register/user', { onBeforeLoad });
cy.get('form .field').first().contains('label', 'Email');
cy.get('form .field').eq(1).contains('label', 'Password');
cy.get('input[type=email]').click();
cy.get('input[type=password]').type('short').should('have.value', 'short');
cy.get('form').contains('button.button.is-primary', 'Register');
cy.get('form .field').first().contains('p.help.is-danger', 'Please fill out this field.');
cy.get('form').contains('.control a.button', 'Didn\'t receive the instructions ?').click();
cy.url().should('include', '/resend-instructions');
cy.go('back');
cy.get('form').contains('.control a.button', 'Login').click();
cy.url().should('include', '/login');
cy.go('back');
});
it('Tests that registration works', () => {
cy.visit('/register/user', { onBeforeLoad });
cy.get('input[type=email]').type('user@email.com');
cy.get('input[type=password]').type('userPassword');
cy.get('form').contains('button.button.is-primary', 'Register').click();
cy.url().should('include', '/register/profile');
cy.get('form .field').first().contains('label', 'Username').parent().find('input').type('tester');
cy.get('form .field').eq(2).contains('label', 'Displayed name').parent().find('input').type('tester account');
cy.get('form .field').eq(3).contains('label', 'Description').parent().find('textarea').type('This is a test account');
cy.get('form .field').last().contains('button', 'Create my profile').click();
cy.contains('article.message.is-success', 'Your account is nearly ready, tester').contains('A validation email was sent to user@email.com');
cy.visit('/sent_emails');
cy.get('iframe')
.first()
.iframeLoaded()
.its('document')
.getInDocument('a')
.eq(1)
.contains('Activate my account')
.invoke('attr', 'href')
.then(href => {
cy.visit(href);
});
// cy.url().should('include', '/validate/');
// cy.contains('Your account is being validated');
cy.location().should((loc) => {
expect(loc.pathname).to.eq('/');
});
});
});

View File

@ -1,14 +0,0 @@
// For authoring Nightwatch tests, see
// http://nightwatchjs.org/guide#usage
module.exports = {
'default e2e tests': (browser) => {
browser
.url(process.env.VUE_DEV_SERVER_URL)
.waitForElementVisible('#app', 5000)
.assert.elementPresent('.hello')
.assert.containsText('h1', 'Welcome to Your Vue.js App')
.assert.elementCount('img', 1)
.end();
},
};

View File

@ -0,0 +1,129 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
let LOCAL_STORAGE_MEMORY = {};
Cypress.Commands.add("saveLocalStorage", () => {
Object.keys(localStorage).forEach(key => {
LOCAL_STORAGE_MEMORY[key] = localStorage[key];
});
});
Cypress.Commands.add("restoreLocalStorage", () => {
Object.keys(LOCAL_STORAGE_MEMORY).forEach(key => {
localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]);
});
});
Cypress.Commands.add('checkoutSession', async () => {
const response = await fetch('/sandbox', {
cache: 'no-store',
method: 'POST',
});
const sessionId = await response.text();
return Cypress.env('sessionId', sessionId);
});
Cypress.Commands.add('dropSession', () =>
cy.waitForFetches().then(() =>
fetch('/sandbox', {
method: 'DELETE',
headers: { 'x-session-id': Cypress.env('sessionId') },
}),
),
);
const increaseFetches = () => {
const count = Cypress.env('fetchCount') || 0;
Cypress.env('fetchCount', count + 1);
};
const decreaseFetches = () => {
const count = Cypress.env('fetchCount') || 0;
Cypress.env('fetchCount', count - 1);
};
const buildTrackableFetchWithSessionId = fetch => (fetchUrl, fetchOptions) => {
const { headers } = fetchOptions;
const modifiedHeaders = Object.assign(
{ 'x-session-id': Cypress.env('sessionId') },
headers,
);
const modifiedOptions = Object.assign({}, fetchOptions, {
headers: modifiedHeaders,
});
return fetch(fetchUrl, modifiedOptions)
.then(result => {
decreaseFetches();
return Promise.resolve(result);
})
.catch(result => {
decreaseFetches();
return Promise.reject(result);
});
};
Cypress.on('window:before:load', win => {
cy.stub(win, 'fetch', buildTrackableFetchWithSessionId(fetch));
});
Cypress.Commands.add('waitForFetches', () => {
if (Cypress.env('fetchCount') <= 0) {
return;
}
cy.wait(100).then(() => cy.waitForFetches());
});
Cypress.Commands.add(
'iframeLoaded',
{ prevSubject: 'element' },
($iframe) => {
const contentWindow = $iframe.prop('contentWindow')
return new Promise(resolve => {
if (
contentWindow &&
contentWindow.document.readyState === 'complete'
) {
resolve(contentWindow)
} else {
$iframe.on('load', () => {
resolve(contentWindow)
})
}
})
})
Cypress.Commands.add(
'getInDocument',
{ prevSubject: 'document' },
(document, selector) => Cypress.$(selector, document)
)

View File

@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"allowJs": true,
"baseUrl": "../node_modules",
"types": [
"cypress"
]
},
"include": [
"**/*.*"
]
}

File diff suppressed because it is too large Load Diff

View File

@ -4,13 +4,21 @@ defmodule MobilizonWeb.Endpoint do
""" """
use Phoenix.Endpoint, otp_app: :mobilizon use Phoenix.Endpoint, otp_app: :mobilizon
# For e2e tests
if Application.get_env(:mobilizon, :sql_sandbox) do
plug(Phoenix.Ecto.SQL.Sandbox,
at: "/sandbox",
header: "x-session-id",
repo: Mobilizon.Storage.Repo
)
end
plug(MobilizonWeb.Plugs.UploadedMedia)
# Serve at "/" the static files from "priv/static" directory. # Serve at "/" the static files from "priv/static" directory.
# #
# You should set gzip to true if you are running phoenix.digest # You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production. # when deploying your static files in production.
plug(MobilizonWeb.Plugs.UploadedMedia)
plug( plug(
Plug.Static, Plug.Static,
at: "/", at: "/",

View File

@ -118,7 +118,7 @@ defmodule MobilizonWeb.Router do
get("/:sig/:url/:filename", MediaProxyController, :remote) get("/:sig/:url/:filename", MediaProxyController, :remote)
end end
if Mix.env() == :dev do if Mix.env() in [:dev, :e2e] do
# If using Phoenix # If using Phoenix
forward("/sent_emails", Bamboo.SentEmailViewerPlug) forward("/sent_emails", Bamboo.SentEmailViewerPlug)
end end

View File

@ -99,7 +99,7 @@ defmodule Mobilizon.Mixfile do
{:html_sanitize_ex, "~> 1.3.0"}, {:html_sanitize_ex, "~> 1.3.0"},
{:ex_cldr_dates_times, "~> 2.0"}, {:ex_cldr_dates_times, "~> 2.0"},
# Dev and test dependencies # Dev and test dependencies
{:phoenix_live_reload, "~> 1.2", only: :dev}, {:phoenix_live_reload, "~> 1.2", only: [:dev, :e2e]},
{:ex_machina, "~> 2.3", only: [:dev, :test]}, {:ex_machina, "~> 2.3", only: [:dev, :test]},
{:excoveralls, "~> 0.10", only: :test}, {:excoveralls, "~> 0.10", only: :test},
{:ex_doc, "~> 0.21.1", only: [:dev, :test], runtime: false}, {:ex_doc, "~> 0.21.1", only: [:dev, :test], runtime: false},