...
 
Commits (158)
......@@ -7,17 +7,14 @@
{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"favicon.ico"
],
"assets": ["assets", "favicon.ico"],
"index": "index.php",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"prefix": "m",
"styles": [
"../node_modules/material-design-lite/dist/material.blue_grey-amber.min.css",
"../node_modules/material-design-icons/iconfont/material-icons.css",
......
......@@ -38,3 +38,5 @@ cypress/videos
!/.drone.yml
!/.gitlab
!/.githooks
!/.prettierrc
!.gitattributes
image: markharding/minds-front-base
services:
- docker:dind
stages:
- test
- build
- prepare
- review
- deploy:staging
- test:e2e
- qa
- deploy:canary
- deploy:production
- cleanup
......@@ -34,9 +31,13 @@ lint:
- prettier --check "src/**/*.scss"
- prettier --check "src/**/*.html"
e2e:chrome:
############
# QA Stage #
############
qa:e2e:
image: cypress/browsers:chrome67
stage: test:e2e
stage: qa
variables:
CYPRESS_INSTALL_BINARY: 3.4.1
script:
......@@ -44,12 +45,14 @@ e2e:chrome:
- >
if [ "$CI_BUILD_REF_NAME" == "master" ]; then
export E2E_DOMAIN=https://www.minds.com
export PRODUCTION=true
else
export E2E_DOMAIN=https://$CI_BUILD_REF_SLUG.$KUBE_INGRESS_BASE_DOMAIN
export PRODUCTION=false
fi
- export CYPRESS_baseUrl=$E2E_DOMAIN
- echo "E2E tests for $CI_BUILD_REF_NAME running against $E2E_DOMAIN with user $CYPRESS_username"
- $(npm bin)/cypress run --browser chrome --record --key $CYPRESS_RECORD_ID --config CYPRESS_baseUrl=$E2E_DOMAIN
- $(npm bin)/cypress run --browser chrome --record --key $CYPRESS_RECORD_ID --config CYPRESS_baseUrl=$E2E_DOMAIN,production=$PRODUCTION
artifacts:
when: always
paths:
......@@ -61,6 +64,17 @@ e2e:chrome:
- cache/Cypress
allow_failure: true #manual inspection in case of timeouts
qa:manual:
stage: qa
script:
- echo "Manually approved"
when: manual
only:
refs:
- master
- test/gitlab-ci
allow_failure: false
###############
# Build Stage #
###############
......@@ -137,6 +151,8 @@ build:production:i18n:
prepare:review:
stage: prepare
image: minds/ci:latest
services:
- docker:dind
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker build -t $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID -f containers/front-init/Dockerfile dist/.
......@@ -162,6 +178,8 @@ prepare:review:sentry:
prepare:production:
stage: prepare
image: minds/ci:latest
services:
- docker:dind
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker build -t $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID -f containers/front-init/Dockerfile dist/.
......@@ -244,6 +262,8 @@ review:stop:
.deploy: &deploy
image: minds/ci:latest
services:
- docker:dind
script:
## Sync assets with CDN
- aws s3 sync dist $S3_REPOSITORY_URL
......@@ -306,6 +326,7 @@ deploy:production:
cleanup:review: # We stop the review site after the e2e tests have run
<<: *cleanup_review
stage: cleanup
when: manual
except:
refs:
- master
......
......@@ -16,10 +16,7 @@
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"assets": [
"src/assets",
"src/favicon.ico"
],
"assets": ["src/assets", "src/favicon.ico"],
"styles": [
"node_modules/material-design-lite/dist/material.blue_grey-amber.min.css",
"node_modules/material-design-icons/iconfont/material-icons.css",
......@@ -36,10 +33,10 @@
"optimization": true,
"outputHashing": "all",
"sourceMap": {
"hidden": true,
"scripts": true,
"styles": true
},
"hidden": true,
"scripts": true,
"styles": true
},
"extractCss": true,
"namedChunks": false,
"aot": true,
......@@ -52,6 +49,14 @@
"with": "src/environments/environment.prod.ts"
}
]
},
"hmr": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.hmr.ts"
}
]
}
}
},
......@@ -63,6 +68,10 @@
"configurations": {
"production": {
"browserTarget": "v5.x:build:production"
},
"hmr": {
"hmr": true,
"browserTarget": "v5.x:build:hmr"
}
}
},
......@@ -83,22 +92,14 @@
"node_modules/medium-editor/dist/js/medium-editor.min.js",
"src/shims/jitsi-api.min.js"
],
"assets": [
"src/assets",
"src/favicon.ico"
]
"assets": ["src/assets", "src/favicon.ico"]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
"tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
}
}
......@@ -118,12 +119,8 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"e2e/tsconfig.e2e.json"
],
"exclude": [
"**/node_modules/**"
]
"tsConfig": ["e2e/tsconfig.e2e.json"],
"exclude": ["**/node_modules/**"]
}
}
}
......@@ -132,11 +129,11 @@
"defaultProject": "v5.x",
"schematics": {
"@schematics/angular:component": {
"prefix": "app",
"prefix": "m",
"styleext": "scss"
},
"@schematics/angular:directive": {
"prefix": "app"
"prefix": "m"
}
}
}
#!/usr/bin/env bash
# Boilerplate generated with create-bash-script (c) 2019 Nikita Skobov
# Available https://github.com/nikita-skobov/create-bash-script
function usage()
{
local just_help=$1
local missing_required=$2
local invalid_argument=$3
local invalid_option=$4
local help="Usage: e2e.sh [OPTIONS]
Intended to serve as an interaction wrapper around Cypress. Ensure that you run from within the project.
Example: ./e2e.sh -u nemofin -p password123 -v true -h http://www.minds.com/
Options (* indicates it is required):"
local help_options="
*\-p ,\--password \<Parameter>\ The password of the user.
\-h ,\--url \<Parameter>\ The URL of the host e.g. https://www.minds.com/ - defaults to use localhost.
\-u ,\--username \<Parameter>\ The username - defaults to cypress_e2e_test.
\-pu ,\--pro_username \<Parameter>\ The pro users username.
\-pp ,\--pro_password \<Parameter>\ The pro users password
\-v ,\---video \<Parameter>\ true if you want video providing.
\-e, \--env \<Parameter>\ add additional env variables e.g. production=true
"
if [ "$missing_required" != "" ]
then
echo "Missing required argument: $missing_required"
fi
if [ "$invalid_option" != "" ] && [ "$invalid_value" = "" ]
then
echo "Invalid option: $invalid_option"
elif [ "$invalid_value" != "" ]
then
echo "Invalid value: $invalid_value for option: --$invalid_option"
fi
echo -e "
"
echo "$help"
echo "$help_options" | column -t -s'\'
return
}
function init_args()
{
REQ_ARGS=( "password" )
# get command line arguments
POSITIONAL=()
# set default arguments
url="http://localhost"
username="minds_cypress_tests"
pro_username="minds_pro_cypress_tests"
pro_password=""
env=""
_video=false
while [[ $# -gt 0 ]]
do
key="$1"
case $key in
-h|--url)
url="$2"
shift 2
;;
-u|--username)
username="$2"
shift 2
;;
-p|--password)
password="$2"
shift 2
;;
-v|--video)
_video="$2"
shift 2
;;
-pu|--pro-username)
pro_username="$2"
shift 2
;;
-pp|--pro-password)
pro_password="$2"
shift 2
;;
-e|--env)
env=",$2"
shift 2
;;
*)
POSITIONAL+=("$1") # saves unknown option in array
shift
;;
esac
done
for i in "${REQ_ARGS[@]}"; do
# $i is the string of the variable name
# ${!i} is a parameter expression to get the value
# of the variable whose name is i.
req_var=${!i}
if [ "$req_var" = "" ]
then
usage "" "--$i"
exit
fi
done
}
init_args $@
# cd to project root.
while [[ $PWD != '/' && ${PWD##*/} != 'front' ]]; do cd ..; done
#run cypress with args.
echo $(npm bin)/cypress open --config baseUrl=$url,video=$_video --env username=$username,password=$password,pro_username=$pro_username,pro_password=$pro_password$env $POSITIONAL
$(npm bin)/cypress open --config baseUrl=$url,video=$_video --env username=$username,password=$password,pro_username=$pro_username,pro_password=$pro_password$env $POSITIONAL
context('Rewards Product Page', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
if (!sessionCookie) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
});
const joinRewards = '.m-marketing__mainWrapper .mf-button';
it('should have a join rewards button', () => {
cy.visit('/rewards');
cy.get(joinRewards)
.should('be.visible')
.should('contain', 'Join Rewards')
.click();
cy.location('pathname').should(
'contains',
'/wallet/tokens/contributions'
);
});
});
context('Token Page', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
if (!sessionCookie) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
cy.visit('/token');
});
it('should have the ability to trigger Buy Tokens modal', () => {
const tokensInput = 'm-blockchain--purchase input[name=amount]';
const buyTokensButton =
'm-blockchain--purchase .m-blockchainTokenPurchase__action .mf-button';
const anyBuyTokensModal =
'm-blockchain--purchase m-modal .m-modal-container';
cy.get(tokensInput)
.focus()
.clear()
.type('0');
cy.get(buyTokensButton).should('be.disabled');
cy.get(tokensInput)
.focus()
.clear()
.type('1');
cy.get(buyTokensButton)
.should('not.be.disabled')
.click();
cy.get('.m-get-metamask--cancel-btn.m-btn').click();
cy.get(anyBuyTokensModal).should('be.visible');
});
it('should have the ability to trigger Buy Eth modal', () => {
const buyEthLink =
'm-blockchain--purchase .m-blockchainTokenPurchase__ethRate a';
const buyEthModal = 'm-blockchain__eth-modal .m-modal-container';
cy.get(buyEthLink).click();
cy.get(buyEthModal).should('be.visible');
});
});
This diff is collapsed.
/**
* @author Ben Hayward
* @desc E2E testing for Minds Boost Console pages.
*/
import generateRandomId from '../../support/utilities';
context('Boost Console', () => {
const postContent = "Test boost, please reject..." + Math.random().toString(36);
const postContent = "Test boost, please reject..." + generateRandomId();
before(() => {
cy.getCookie('minds_sess')
......@@ -8,27 +14,24 @@ context('Boost Console', () => {
return cy.login(true);
}
});
newBoost(postContent, 500);
});
beforeEach(() => {
cy.preserveCookies();
cy.visit('/newsfeed/subscribed');
newBoost(postContent, 100);
cy.server();
cy.route("POST", '**/api/v2/boost/**').as('boostPost');
cy.visit('/boost/console/newsfeed/history');
});
it('should show a new boost in the console', () => {
cy.visit('/boost/console/newsfeed/history');
cy.get('m-boost-console-card:nth-child(1) div.m-boost-card--manager-item.m-boost-card--state')
.should('not.contain', 'revoked');
cy.get('m-boost-console-card:nth-child(1) .m-boost-card--manager-item--buttons > button')
.click();
cy.get('m-boost-console-card:nth-child(1) .m-mature-message span')
.contains(postContent);
});
it('should allow a revoke a boost', () => {
navToConsole();
cy.get('m-boost-console-card:nth-child(1) div.m-boost-card--manager-item.m-boost-card--state')
.should('not.contain', 'revoked');
......@@ -39,15 +42,42 @@ context('Boost Console', () => {
.contains('revoked');
});
function navToConsole() {
cy.visit('/boost/console/newsfeed/history');
cy.location('pathname')
.should('eq', `/boost/console/newsfeed/history`);
}
it('should load show the user content for newsfeed boosts', () => {
cy.route("GET", "**/feeds/container/*/activities**").as("activities");
cy.contains('Create a Boost')
.click()
.location('pathname')
.should('eq', `/boost/console/newsfeed/create`)
.wait('@activities').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
it('should load show the user content for sidebar boosts', () => {
cy.route("GET", "**/api/v2/feeds/container/*/all**").as("all");
cy.visit('/boost/console/content/create')
.location('pathname')
.should('eq', `/boost/console/content/create`)
.wait('@all').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
it('should load show the user content for offers', () => {
cy.route("GET", "**/api/v2/feeds/container/*/activities**").as("all");
cy.visit('/boost/console/offers/create')
.location('pathname')
.should('eq', `/boost/console/offers/create`)
.wait('@all').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
function newBoost(text, views) {
cy.server();
cy.route("POST", '**/api/v2/boost/**').as('boostPost');
cy.visit('/newsfeed/subscribed');
cy.post(text);
cy.get('#boost-actions')
......@@ -61,7 +91,6 @@ context('Boost Console', () => {
.click();
cy.wait('@boostPost').then((xhr) => {
cy.log(xhr);
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
......
context('Boost Creation', () => {
const duplicateError = "There's already an ongoing boost for this entity";
const postContent = "Test boost, please reject..." + Math.random().toString(36).substring(8);
const nonParticipationError = 'Boost target should participate in the Rewards program.'
/**
* @author Ben Hayward
* @desc E2E testing for Minds Boost Console pages.
*/
import generateRandomId from '../../support/utilities';
context('Boost Console', () => {
const postContent = "Test boost, please reject..." + generateRandomId();
before(() => {
cy.server();
})
beforeEach(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.visit('/newsfeed/subscriptions');
cy.location('pathname')
.should('eq', `/newsfeed/subscriptions`);
cy.route("GET", '**/api/v2/boost/prepare/**').as('prepare');
cy.route("POST", '**/api/v2/boost/activity/**').as('activity');
cy.route("GET", '**/api/v2/blockchain/wallet/balance*').as('balance');
cy.route("GET", '**/api/v2/search/suggest/**').as('suggest');
newBoost(postContent, 500);
});
beforeEach(() => {
cy.server();
cy.route("POST", '**/api/v2/boost/**').as('boostPost');
cy.preserveCookies();
cy.visit('/boost/console/newsfeed/history');
});
after(() => {
cy.clearCookies();
});
it('should redirect a user to buy tokens when clicked', () => {
openTopModal();
it('should show a new boost in the console', () => {
cy.get('m-boost-console-card:nth-child(1) div.m-boost-card--manager-item.m-boost-card--state')
.should('not.contain', 'revoked');
cy.get('m-boost-console-card:nth-child(1) .m-mature-message span')
.contains(postContent);
});
cy.get('m-boost--creator-payment-methods li h5 span')
.contains('Buy Tokens')
it('should allow a revoke a boost', () => {
cy.get('m-boost-console-card:nth-child(1) div.m-boost-card--manager-item.m-boost-card--state')
.should('not.contain', 'revoked');
cy.get('m-boost-console-card:nth-child(1) .m-boost-card--manager-item--buttons > button')
.click();
cy.location('pathname', { timeout: 30000 })
.should('eq', `/token`);
cy.get('m-boost-console-card:nth-child(1) div.m-boost-card--manager-item.m-boost-card--state')
.contains('revoked');
});
it('should allow a user to make an offchain boost for 5000 tokens', () => {
cy.post(postContent);
openTopModal();
cy.get('.m-boost--creator-section-amount input')
.type(5000);
cy.get('m-overlay-modal > div.m-overlay-modal > m-boost--creator button')
it('should load show the user content for newsfeed boosts', () => {
cy.route("GET", "**/feeds/container/*/activities**").as("activities");
cy.contains('Create a Boost')
.click()
.wait('@prepare').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
}).wait('@activity').then((xhr) => {
.location('pathname')
.should('eq', `/boost/console/newsfeed/create`)
.wait('@activities').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
cy.get('.m-overlay-modal')
.should('not.be.visible')
});
it('should error if the boost is a duplicate', () => {
openTopModal();
cy.get('.m-boost--creator-section-amount input')
.type(5000);
})
cy.get('m-overlay-modal > div.m-overlay-modal > m-boost--creator button')
.click()
.wait('@prepare').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
}).wait('@activity').then((xhr) => {
it('should load show the user content for sidebar boosts', () => {
cy.route("GET", "**/api/v2/feeds/container/*/all**").as("all");
cy.visit('/boost/console/content/create')
.location('pathname')
.should('eq', `/boost/console/content/create`)
.wait('@all').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("error");
});
cy.get('[data-cy=data-minds-boost-creation-error]')
.contains(duplicateError);
});
it('should display an error if boost offer receiver has not signed up for rewards', () => {
openTopModal();
})
cy.get('h4')
.contains('Offers')
.click();
cy.get('m-boost--creator-p2p-search .m-boost--creator-wide-input input')
.type("minds").wait('@suggest').then((xhr) => {
it('should load show the user content for offers', () => {
cy.route("GET", "**/api/v2/feeds/container/*/activities**").as("all");
cy.visit('/boost/console/offers/create')
.location('pathname')
.should('eq', `/boost/console/offers/create`)
.wait('@all').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
cy.get('.m-boost--creator-autocomplete--results .m-boost--creator-autocomplete--result-content')
.first()
.click({force: true});
})
cy.get('[data-cy=data-minds-boost-creation-error]')
.contains(nonParticipationError);
});
function newBoost(text, views) {
cy.server();
cy.route("POST", '**/api/v2/boost/**').as('boostPost');
cy.visit('/newsfeed/subscribed');
cy.post(text);
function openTopModal() {
cy.get('#boost-actions')
.first()
.click()
.wait('@balance').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
.click();
cy.get('.m-boost--creator-section-amount input')
.type(views);
cy.get('m-overlay-modal > div.m-overlay-modal > m-boost--creator button')
.click();
cy.wait('@boostPost').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
cy.get('.m-overlay-modal')
.should('not.be.visible')
}
})
context('Boost Impressions', () => {
// Cannot test until env behaves consistently else,
// the test will frequently error when it cant see a boost.
context.skip('Boost Impressions', () => {
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
......@@ -7,37 +8,40 @@ context('Boost Impressions', () => {
return cy.login(true);
}
});
cy.visit('/newsfeed/subscriptions');
cy.location('pathname')
.should('eq', `/newsfeed/subscriptions`);
});
beforeEach(()=> {
cy.server();
cy.route("POST", "**api/v2/analytics/views/boost/*").as("analytics");
cy.route("GET", "**/api/v2/feeds/subscribed/activities**").as("activities");
cy.preserveCookies();
cy.visit('/newsfeed/subscriptions')
.location('pathname')
.should('eq', `/newsfeed/subscriptions`)
.wait('@activities').then((xhr) => {
expect(xhr.status).to.equal(200);
});
});
afterEach(()=> {
cy.reload();
})
it('should register views on scroll', () => {
//stub endpoint
cy.server();
cy.route("POST", "**/api/v2/analytics/views/activity/*").as("analytics");
//load, scroll, wait to trigger analytics
cy.scrollTo(0, 500);
//smooth scroll
cy.scrollTo('0', '1%', { duration: 100 });
//assert
cy.wait('@analytics').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body).to.deep.equal({ status: 'success' });
});
});
it('should register views on boost rotate', () => {
//stub endpoint
cy.server();
cy.route("POST", "**/api/v2/analytics/views/boost/*").as("analytics");
//rotate forward and wait to trigger analytics
cy.get('m-newsfeed--boost-rotator > div > ul > li:nth-child(3) > i')
cy.get('m-newsfeed--boost-rotator')
.find('chevron_right')
.click();
//assert
......@@ -47,7 +51,8 @@ context('Boost Impressions', () => {
});
//rotate forward and wait to trigger analytics
cy.get('m-newsfeed--boost-rotator > div > ul > li:nth-child(2) > i')
cy.get('m-newsfeed--boost-rotator')
.find('chevron_left')
.click();
//assert
......
context('Boost Product Page', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
if (!sessionCookie) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
});
const createBoostButton = '.m-marketing__mainWrapper .mf-button';
it('should have a create boost button', () => {
cy.visit('/boost');
cy.get(createBoostButton)
.should('be.visible')
.should('contain', 'Create Boost')
.click();
cy.location('pathname').should(
'contains',
'/boost/console/newsfeed/create'
);
});
});
/**
* Skipping until sandbox behaves consistently as currently when posting,
* on the sandboxes it does not show your latest image after you have posted it.
* The below code should be functioning correctly once this is resolved.
*/
context.skip('Channel image upload', () => {
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.visit('/newsfeed/subscriptions')
.location('pathname')
.should('eq', '/newsfeed/subscriptions');
});
beforeEach(()=> {
cy.preserveCookies();
cy.server();
cy.route("POST", "**/api/v1/newsfeed").as("newsfeedPOST");
cy.route("POST", "**/api/v1/media").as("mediaPOST");
});
it('should post an activity with an image attachment', () => {
cy.get('minds-newsfeed-poster').should('be.visible');
cy.get('minds-newsfeed-poster textarea').type('This is a post with an image');
cy.uploadFile('#attachment-input-poster', '../fixtures/international-space-station-1776401_1920.jpg', 'image/jpg');
//upload image
cy.wait('@mediaPOST');
cy.get('.m-posterActionBar__PostButton').click();
//await response for activity
cy.wait('@newsfeedPOST').then((xhr) => {
expect(xhr.status).to.equal(200);
const uploadedImageGuid = xhr.response.body.activity.entity_guid
const activityGuid = xhr.response.body.guid;
cy.get('.minds-list > minds-activity:first-child .message').contains('This is a post with an image');
// assert image
cy.get('.minds-list > minds-activity:first-child .item-image img').should('be.visible');
cy.visit(`/${Cypress.env().username}`);
let mediaHref = `/media/${uploadedImageGuid}`;
cy.get("m-channels--sorted-module[title='Images']")
.find(`a[href='${mediaHref}']`);
cy.get(`[data-minds-activity-guid='${activityGuid}']`)
.find('m-post-menu .minds-more')
.click();
cy.get(`[data-minds-activity-guid='${activityGuid}']`)
.find("li:contains('Delete')")
.click();
cy.get(`[data-minds-activity-guid='${activityGuid}'] m-post-menu m-modal-confirm .mdl-button--colored`).click();
});
});
});
context('Channel', () => {
// skipped until feat release
context.skip('Channel', () => {
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
......
// skipped until feat release
context.skip('Comment Permissions', () => {
const postMenu = 'minds-activity:first > div > m-post-menu';
const deletePostOption = "m-post-menu > ul > li:visible:contains('Delete')";
const deletePostButton = ".m-modal-confirm-buttons > button:contains('Delete')";
before(() => {
//make a post new.
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.visit('/newsfeed/subscriptions');
cy.location('pathname')
.should('eq', `/newsfeed/subscriptions`);
});
afterEach(() => {
//delete the post
cy.get(postMenu).click();
cy.get(deletePostOption).click();
cy.get(deletePostButton).click();
});
beforeEach(()=> {
cy.preserveCookies();
cy.post('test post');
});
it('should disable comments', () => {
cy.server();
cy.route("POST", "**/api/v2/permissions/comments/**").as("commentToggle");
cy.get(postMenu)
.click()
.find("li:visible:contains('Disable Comments')")
.click();
cy.wait('@commentToggle').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
expect(xhr.response.body.allowed).to.equal(false);
});
//close menu
cy.get(postMenu)
.click();
cy.get('minds-activity:first')
.find("i:contains('speaker_notes_off')")
.click();
});
it('should allow comments', () => {
cy.server();
cy.route("POST", "**/api/v2/permissions/comments/**").as("commentToggle");
cy.get(postMenu)
.click()
.find("li:visible:contains('Disable Comments')")
.click();
cy.wait('@commentToggle').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
expect(xhr.response.body.allowed).to.equal(false);
});
//Menu stays open
cy.get("li:visible:contains('Allow Comments')")
.click();
cy.wait('@commentToggle').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
expect(xhr.response.body.allowed).to.equal(true);
});
//close menu
cy.get(postMenu)
.click();
cy.get('minds-activity:first')
.find("i:contains('chat_bubble')");
});
});
......@@ -6,7 +6,9 @@ context('Discovery', () => {
return cy.login(true);
}
});
cy.visit('/newsfeed/global/top');
cy.visit('/newsfeed/global/top')
.location('pathname')
.should('eq', '/newsfeed/global/top');
});
beforeEach(()=> {
......
......@@ -32,17 +32,21 @@ context('Groups', () => {
// click on hashtags dropdown
cy.get('m-hashtags-selector .m-dropdown--label-container').click();
// select #ART
cy.get('m-hashtags-selector m-dropdown m-form-tags-input > div > span').contains('#art').click();
cy.get('m-hashtags-selector m-dropdown m-form-tags-input > div > span').contains('art').click();
// type in another hashtag manually
cy.get('m-hashtags-selector m-form-tags-input input').type('hashtag{enter}').click();
// click away
cy.get('m-hashtags-selector .minds-bg-overlay').click();
cy.get('.m-groups-save > button').contains('Create').click();
cy.route("POST", "**/api/v1/groups/group/*/banner*").as("postBanner");
cy.wait('@postGroup').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
}).wait('@postBanner').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
});
cy.get('.m-groupInfo__name').contains('test');
......
......@@ -52,6 +52,7 @@ context('Messenger', () => {
after(() => {
cy.deleteUser(testUsername, testPassword);
cy.clearCookies({log: true})
});
it('should allow a new user to set a password and send a message', () => {
......
This diff is collapsed.
context('Pro Product Page', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
if (!sessionCookie) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
});
const contactUsButton = '.m-marketing__mainWrapper .mf-button';
it('should have a contact us button', () => {
cy.visit('/nodes', {
onBeforeLoad(_window) {
cy.stub(_window, 'open');
},
});
cy.get(contactUsButton)
.should('be.visible')
.should('contain', 'Contact us for details')
.click();
cy.window()
.its('open')
.should('be.called');
});
});
......@@ -4,28 +4,25 @@
*/
import generateRandomId from '../support/utilities';
context('Notification', () => {
context.skip('Notification', () => {
//secondary user for testing.
let username = '';
let password = '';
const username = generateRandomId();
const password = generateRandomId()+'X#';
const commentText = 'test comment';
const postText = 'test comment'
const commentText = generateRandomId();
const postText = generateRandomId();
const postCommentButton = 'm-comment__poster > div > div.minds-body > div > div > a.m-post-button';
const commentButton = 'minds-activity > div.tabs > minds-button-comment > a';
const commentInput = 'm-comment__poster minds-textarea > div';
const commentContent ='.m-comment__bubble > p';
const notificationBell = 'm-notifications--topbar-toggle > a > i';
const notification = 'minds-notification';
/**
* Before all, generate username and password, login as the new user and log out.
* Next login to env user, make a post, and log out.
*/
before(() => {
username = generateRandomId();
password = generateRandomId()+'X#';
cy.newUser(username, password);
cy.logout();
......@@ -39,7 +36,9 @@ context('Notification', () => {
*/
after(() => {
cy.clearCookies();
cy.login(true, username, password);
cy.visit(`/${Cypress.env().username}`);
cy.deleteUser(username, password);
});
......@@ -49,6 +48,8 @@ context('Notification', () => {
* then switch users and check for the notification.
*/
beforeEach(() => {
cy.route("GET", '**/api/v1/notifications/all**').as('notifications');
cy.clearCookies();
cy.login(false, username, password);
......@@ -70,13 +71,16 @@ context('Notification', () => {
cy.login();
// Open their notifications
cy.get(notificationBell).click();
cy.get(notificationBell).click()
.wait('@notifications').then((xhr) => {
expect(xhr.status).to.equal(200);
});
/**
* Notifications not working on test env.
* TODO: Check for notification - follow it
* through and check it leads to the post with postText.
*/
cy.get(notification)
.first()
.click();
cy.contains(commentText);
});
})
......@@ -43,28 +43,22 @@ context('Onboarding', () => {
.contains(welcomeText);
});
it('should allow a user to register', () => {
//select topics
cy.get(getTopic(3)).click().should('have.class', 'selected')
cy.get(getTopic(4)).click().should('have.class', 'selected')
cy.get(getTopic(5)).click().should('have.class', 'selected')
it('should allow a user to run through onboarding modals', () => {
//select topics
cy.get(getTopic(3)).click().should('have.class', 'selected')
cy.get(getTopic(4)).click().should('have.class', 'selected')
cy.get(getTopic(5)).click().should('have.class', 'selected')
//click
cy.get(nextButton).click();
});
it('should skip over subscribed channels', () => {
//click
cy.get(nextButton).click();
//TODO: Skipped over for now as subscribed channels is not working on staging environment.
cy.get(nextButton).click();
});
it('should let a user change their display name and description', () => {
cy.get(nameField).clear().type(name);
cy.get(descriptionfield).type(description);
cy.get(nextButton).click();
});
it('should allow a user to select their country', () => {
//set dialcode
cy.get(countryDropdown).click();
cy.get(ukOption).click();
......
context('Plus Product Page', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
if (!sessionCookie) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
});
const upgradeButton = 'm-plus--subscription .mf-button';
const wirePaymentsComponent = 'm-wire__paymentscreator .m-wire--creator';
it('should open the Wire Payment modal', () => {
cy.visit('/plus');
cy.get(upgradeButton)
.should('be.visible')
.should('contain', 'Upgrade to Plus')
.click();
cy.get(wirePaymentsComponent).should('be.visible');
});
it('should automatically open the Wire Payment modal', () => {
cy.visit('/plus?i=yearly&c=tokens');
cy.get(wirePaymentsComponent).should('be.visible');
});
});
context('Pro Product Page', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
if (!sessionCookie) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
});
const upgradeButton = 'm-pro--subscription .mf-button';
const wirePaymentsComponent = 'm-wire__paymentscreator .m-wire--creator';
it('should show a coming soon button', () => {
cy.visit('/pro');
cy.get(upgradeButton)
.should('be.visible')
.should('contain', 'Coming soon')
.click();
});
// it('should open the Wire Payment modal', () => {
//
// cy.visit('/pro');
//
// cy.get(upgradeButton)
// .should('be.visible')
// .should('contain', 'Upgrade to Pro')
// .click();
//
// cy.get(wirePaymentsComponent).should('be.visible');
// });
//
// it('should automatically open the Wire Payment modal', () => {
// cy.visit('/pro?i=yearly&c=tokens');
//
// cy.get(wirePaymentsComponent).should('be.visible');
// });
});
/**
* @author Ben Hayward
* @desc E2E testing for Minds Pro's pages.
*/
context('Pro Page', () => {
if (Cypress.env().pro_password) { // required to run tests against pro user only.
const topBar = '.m-proChannel__topbar';
let categories = [
{ label: 'Technology', tag: '#technology' },
{ label: 'Food', tag: '#food' },
{ label: 'News', tag: '#news' }
];
let footerLinks = [
{ label: 'Minds', link: 'https://www.minds.com/' },
{ label: 'Careers', link: 'https://www.minds.com/careers' },
];
const proButton = 'data-minds-sidebar-admin-pro-button';
function resetSettings() {
cy.visit(`/pro/settings`);
cy.route("POST", "**/api/v2/pro/settings").as("settings");
cy.get('#title').focus().clear().type('Title');
cy.get('#headline').focus().clear().type('This is a headline');
cy.contains('Hashtags')
.click();
// remove all hashtags
removeInputs();
for (let i = 0; i < categories.length; i++) {
let cat = categories[i];
addTag(cat.label, cat.tag, i);
}
cy.contains('Footer')
.click();
cy.get('#footer_text')
.clear()
.type('This is the footer text');
// remove all footer links
removeInputs();
for (let i = 0; i < footerLinks.length; i++) {
let link = footerLinks[i];
addFooterLink(link.label, link.link, i);
}
cy.contains('Save')
.click()
.wait('@settings').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body).to.deep.equal({ status: 'success' });
}
);
}
function removeInputs() {
cy.get('.m-draggableList__list .m-proSettings__field .m-proSettings__flexInputs').should('be.visible').within($el => {
for (let i = $el.length - 1; i >= 0; i--) { // flexInput. Start from the last one
let c = $el[i];
for (let j = 0; j < c.children.length; j++) { // inputs and the X button
let cc = c.children[j];
if (cc.nodeName === 'I') { // if it's the X button, click on it
cy.wrap(cc).click();
}
}
}
});
}
function addTag(label, tag, index) {
cy.contains('+ Add Tag')
.click();
cy.get(`#tag-label-${index}`)
.clear()
.type(label);
cy.get(`#tag-tag-${index}`)
.clear()
.type(tag);
}
function addFooterLink(label, link, index) {
cy.contains('Add Link')
.click();
cy.get(`#footer_link-title-${index}`)
.clear()
.type(label);
cy.get(`#footer_link-href-${index}`)
.clear()
.type(link);
}
before(() => {
cy.login(true, Cypress.env().pro_username, Cypress.env().pro_password);
// after logging in, we need to get to settings and set everything up
resetSettings();
// go to pro page
cy.visit(`/pro/${Cypress.env().pro_username}`);
cy.get(topBar);
});
beforeEach(() => {
cy.server();
cy.preserveCookies();
});
it('should load the feed tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/activities/top**").as("activities");
cy.contains('Feed')
.click()
.wait('@activities').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
it('should load the videos tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/videos/top**").as("videos");
cy.contains('Videos')
.click()
.wait('@videos').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
it('should load the images tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/images/top**").as("images");
cy.contains('Images')
.click()
.wait('@images').then((xhr) => {
expect(xhr.status).to.equal(200);
});
// should have sub-categories
cy.get('m-pro--channel--categories > .m-proChannel__category').each(($el, $index) => {
let c = categories.slice(0);
c.unshift({ label: 'All', tag: '#all' });
expect($el.text()).to.contain(c[$index].label);
});
cy.get('m-pro--channel .m-overlay-modal').should('not.be.visible');
// click on tile
cy.get('.m-proChannelListContent__list li:first-child m-pro--channel-tile').click();
cy.wait(200);
// media modal should appear
cy.get('m-pro--channel .m-overlay-modal').should('be.visible');
// close media modal
cy.get('m-pro--channel .m-overlay-modal--close').click();
})
it('should load the articles tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/blogs/top**").as("blogs");
cy.contains('Articles')
.click()
.wait('@blogs').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
it('should load the groups tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/groups/top**").as("groups");
cy.contains('Groups')
.click()
.wait('@groups').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
it('should have a footer', () => {
// should have a footer text
cy.get('.m-proChannelFooter__text').contains('This is the footer text');
// should have footer links
cy.get('.m-proChannel__footer .m-proChannelFooter .m-proChannelFooter__link').should('be.visible').each(($el, $index) => {
expect($el.text()).to.contain(footerLinks[$index].label);
expect($el.attr('href')).to.contain(footerLinks[$index].link);
});
})
}
})
/**
* @author Ben Hayward
* @desc E2E testing for Minds Pro's settings.
*/
context('Pro Settings', () => {
if (Cypress.env().pro_password) { // required to run tests against pro user only.
const title = '#title';
const headline = '#headline';
const previewButton = '.m-proSettings__previewBtn';
const activityContainer = 'minds-activity';
const footerText = '#footer_text';
const theme = {
primaryColor: '#primary_color',
plainBackgroundColor: '#plain_background_color',
schemeLight: '#scheme_light',
schemeDark: '#scheme_dark',
aspectRatio: {
169: '#tile_ratio_16\:9', // 16:9
1610: '#tile_ratio_16\:10', // 16:10
43: '#tile_ratio_4\:3', // 4:3
11: '#tile_ratio_1\:1' , // 1:1
},
}
const hashtags = {
labelInput0: '#tag-label-0',
hashtagInput0: '#tag-tag-0',
labelInput1: '#tag-label-1',
hashtagInput1: '#tag-tag-1',
label1: 'label1',
label2: 'label2',
label3: 'label3',
hashtag1: '#hashtag1',
hashtag2: '#hashtag2',
hashtag3: '#hashtag3',
}
const footer = {
hrefInput: `#footer_link-href-0`,
titleInput: `#footer_link-title-0`,
}
const strings = {
title: "Minds Pro E2E",
headline: "This headline is a test",
footer: "This is a footer",
footerTitle: "Minds",
footerHref: 'https://www.minds.com/',
}
before(() => {
cy.login(true, Cypress.env().pro_username, Cypress.env().pro_password);
});
after(() => {
cy.visit("/pro/settings")
.location('pathname')
.should('eq', '/pro/settings');
clearHashtags();
});
beforeEach(()=> {
cy.preserveCookies();
cy.server();
cy.route("POST", "**/api/v2/pro/settings").as("settings");
cy.visit("/pro/settings")
.location('pathname')
.should('eq', '/pro/settings');
});
it('should update the title and headline', () => {
//enter data
cy.get(title)
.focus()
.clear()
.type(strings.title);
cy.get(headline)
.focus()
.clear()
.type(strings.headline);
saveAndPreview();
//check tab title.
cy.title()
.should('eq', strings.title+' - '+strings.headline+" | Minds");
});
// Need to find a way around the color input in Cypress.
it('should allow the user to set a dark theme for posts', () => {
cy.contains('Theme')
.click();
cy.get(theme.schemeDark)
.click();
saveAndPreview();
cy.contains('Feed')
.click();
cy.get(activityContainer)
.should('have.css', 'background-color')
.and('eq', 'rgb(35, 35, 35)');
});
it('should allow the user to set a light theme for posts', () => {
cy.contains('Theme')
.click();
cy.get(theme.schemeLight)
.click();
saveAndPreview();
cy.contains('Feed')
.click();
cy.get(activityContainer)
.should('have.css', 'background-color')
.and('eq', 'rgb(255, 255, 255)');
});
it('should allow the user to set category hashtags', () => {
cy.contains('Hashtags')
.click();
cy.contains('+ Add Tag')
.click();
cy.get(hashtags.labelInput0)
.clear()
.type(hashtags.label1);
cy.get(hashtags.hashtagInput0)
.clear()
.type(hashtags.hashtag1);
cy.contains('+ Add Tag')
.click();
cy.get(hashtags.labelInput1)
.first()
.clear()
.type(hashtags.label2);
cy.get(hashtags.hashtagInput1)
.first()
.clear()
.type(hashtags.hashtag2);
saveAndPreview();
//check the labels are present and clickable.
cy.contains('label1');
cy.contains('label2');
});
it('should allow the user to set footer', () => {
cy.contains('Footer')
.click();
cy.get(footerText)
.clear()
.type(strings.footer);
cy.contains('Add Link')
.click();
cy.get(footer.hrefInput)
.clear()
.type(strings.footerHref);
cy.get(footer.titleInput)
.clear()
.type(strings.footerTitle);
saveAndPreview();
cy.contains(strings.footerTitle)
.should('have.attr', 'href')
.should('contain', strings.footerHref);
});
//save, await response, preview.
function saveAndPreview() {
//save and await response
cy.contains('Save')
.click()
.wait('@settings').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body).to.deep.equal({ status: 'success' });
});
//go to pro page
cy.get(previewButton)
.click();
}
function clearHashtags() {
cy.contains('Hashtags')
.click();
cy.contains('+ Add Tag')
.click();
cy.contains('clear')
.click({multiple: true});
saveAndPreview();
}
//
// it.only('should update the theme', () => {
// // nav to theme tab
// cy.contains('Theme')
// .click();
// cy.get(theme.plainBackgroundColor).then(elem => {
// elem.val('#00dd00');
// //save and await response
// cy.contains('Save')
// .click()
// .wait('@settings').then((xhr) => {
// expect(xhr.status).to.equal(200);
// expect(xhr.response.body).to.deep.equal({ status: 'success' });
// });
// //go to pro page
// cy.get(previewButton)
// .click();
// });
// })
}
})
......@@ -19,6 +19,7 @@ context('Registration', () => {
const submitButton = 'minds-form-register .mdl-card__actions button';
beforeEach(() => {
cy.clearCookies();
cy.visit('/login');
cy.location('pathname').should('eq', '/login');
cy.server();
......@@ -30,6 +31,7 @@ context('Registration', () => {
cy.location('pathname').should('eq', '/login');
cy.login(false, username, password);
cy.deleteUser(username, password);
cy.clearCookies();
})
it('should allow a user to register', () => {
......
......@@ -14,11 +14,11 @@ context('Remind', () => {
beforeEach(() => {
cy.preserveCookies();
cy.server();
cy.route("POST", "**/api/v2/newsfeed/remind/*").as("postRemind");
});
it('should allow a user to remind their post', () => {
cy.server();
cy.route("POST", "**/api/v2/newsfeed/remind/*").as("postRemind");
//post
cy.post("test!!");
......@@ -29,13 +29,14 @@ context('Remind', () => {
//fill out text box in modal
cy.get('.m-modal-remind-composer textarea')
.focus()
.clear()
.type(remindText);
//post remind.
cy.get('.m-modal-remind-composer-send i')
.click();
cy.wait('@postRemind').then((xhr) => {
.click()
.wait('@postRemind').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
......
......@@ -5,14 +5,25 @@ context('Subscription', () => {
const messageButton = 'm-messenger--channel-button > button';
const userDropdown = 'minds-button-user-dropdown > button';
beforeEach(()=> {
cy.login(true);
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
cy.server();
cy.route("POST", "**/api/v1/subscribe/*").as("subscribe");
cy.route("DELETE", "**/api/v1/subscribe/*").as("unsubscribe");
cy.location('pathname', { timeout: 30000 })
.should('eq', `/newsfeed/subscriptions`);
cy.visit(`/${user}`);
})
cy.visit(`/${user}/`);
cy.location('pathname')
.should('eq', `/${user}/`);
});
it('should allow a user to subscribe to another', () => {
subscribe();
......@@ -20,16 +31,26 @@ context('Subscription', () => {
it('should allow a user to unsubscribe',() => {
unsubscribe();
})
});
function subscribe() {
cy.get(subscribeButton).click();
cy.get(messageButton).should('be.visible');
cy.get(subscribeButton)
.click()
.wait('@subscribe').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
cy.get(messageButton).should('be.visible')
}
function unsubscribe() {
cy.get(userDropdown).click();
cy.contains('Unsubscribe').click();
cy.contains('Unsubscribe')
.click()
.wait('@unsubscribe').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
cy.get(subscribeButton).should('be.visible');
}
......
......@@ -20,7 +20,7 @@ context('Topbar', () => {
.contains('View Channel')
.click();
cy.location('pathname').should('eq', `/${Cypress.env().username}`);
cy.location('pathname').should('eq', `/${Cypress.env().username}/`);
});
it('clicking on the dropdown on the right should allow to go to settings', () => {
......
context('Upgrades page', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
if (!sessionCookie) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
cy.visit('/upgrades');
});
it('should scroll to upgrades table', () => {
cy.viewport(1200, 600); // Only on desktop
const scrollButton = '[data-cy="m-upgrades__upgrade-now-button"]';
const heading = '.m-upgradesUpgradeOptions__header h2';
cy.get(scrollButton)
.should('contain', 'Upgrade now')
.click();
cy.wait(1500);
cy.isInViewport(heading);
});
// TODO: Toggles tests (make them testable)
it('should have the ability to trigger Buy Tokens modal', () => {
const tokensInput = 'm-blockchain--purchase input[name=amount]';
const buyTokensButton =
'm-blockchain--purchase .m-blockchainTokenPurchase__action .mf-button';
const anyBuyTokensModal =
'm-blockchain--purchase m-modal .m-modal-container';
cy.get(tokensInput)
.focus()
.clear()
.type('0');
cy.get(buyTokensButton).should('be.disabled');
cy.get(tokensInput)
.focus()
.clear()
.type('1');
cy.get(buyTokensButton)
.should('not.be.disabled')
.click();
cy.get('.m-get-metamask--cancel-btn.m-btn').click();
cy.get(anyBuyTokensModal).should('be.visible');
});
it('should have the ability to trigger Buy Eth modal', () => {
const buyEthLink =
'm-blockchain--purchase .m-blockchainTokenPurchase__ethRate a';
const buyEthModal = 'm-blockchain__eth-modal .m-modal-container';
cy.get(buyEthLink).click();
cy.get(buyEthModal).should('be.visible');
});
it('should navigate to Plus and trigger a Wire', () => {
const upgradeButton = cy.get(
'[data-cy="m-upgradeOptions__upgrade-to-plus-button"]'
);
upgradeButton.click();
cy.location('pathname').should('contain', '/plus');
});
it('should navigate to Pro and trigger a Wire', () => {
const upgradeButton = cy.get(
'[data-cy="m-upgradeOptions__upgrade-to-pro-button"]'
);
upgradeButton.click();
cy.location('pathname').should('contain', '/pro');
});
it('should navigate to Nodes', () => {
const upgradeButton = cy.get(
'[data-cy="m-upgradeOptions__contact-us-nodes-button"]'
);
upgradeButton.click();
cy.location('pathname').should('contain', '/nodes');
});
});
......@@ -5,28 +5,34 @@
* @desc Spec tests for Wire transactions.
*/
context('Wire', () => {
import generateRandomId from "../../support/utilities";
// Issue to re-enable https://gitlab.com/minds/front/issues/1846
context.skip('Wire Creator', () => {
const receiver = {
username: generateRandomId(),
password: generateRandomId()+'F!',
}
const sendAmount = 5000;
const wireButton = 'm-wire-channel > div > button';
const sendButton = '.m-wire--creator-section--last > div > button';
const modal = 'm-overlay-modal > div.m-overlay-modal';
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.newUser(receiver.username, receiver.password);
cy.logout();
});
beforeEach(()=> {
cy.preserveCookies();
cy.login(true);
});
//TODO: Remove me when we get user to user wires working on the review environment
it.skip('should allow a user to send a wire to another user', () => {
// Visit users page.
cy.visit('/minds');
afterEach(() => {
// cy.login(true, receiver.username, receiver.password);
cy.visit(`/${Cypress.env().username}`);
// Click profile wire button
cy.get(wireButton).click();
......@@ -40,4 +46,19 @@ context('Wire', () => {
cy.get(modal).should('be.hidden');
});
it('should allow a user to send a wire to another user', () => {
// Visit users page.
cy.visit(`/${receiver.username}`);
// Click profile wire button
cy.get(wireButton).click();
cy.wait(2000);
// Click send button
cy.get(sendButton).click();
cy.wait(5000);
//Make sure modal is hidden after 5 seconds.
cy.get(modal).should('be.hidden');
});
})
context('Pay Product Page', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
if (!sessionCookie) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
});
const monetizeChannelButton = '.m-marketing__mainWrapper .mf-button';
it('should have a monetize channel button', () => {
cy.visit('/pay');
cy.get(monetizeChannelButton)
.should('be.visible')
.should('contain', 'Monetize your channel')
.click();
cy.location('pathname').should(
'contains',
'/wallet/tokens/contributions'
);
});
});
......@@ -72,21 +72,23 @@ const poster = {
* @returns void
*/
Cypress.Commands.add('login', (canary = false, username, password) => {
cy.clearCookies();
cy.setCookie('staging', "1"); // Run in staging mode. Note: does not impact review sites
username = username ? username : Cypress.env().username;
password = password ? password : Cypress.env().password;
cy.setCookie('staging', "1"); // Run in stagin mode. Note: does not impact review sites
cy.visit('/login');
cy.server();
cy.route("POST", "/api/v1/authenticate").as("postLogin");
cy.get(loginForm.username).type(username);
cy.get(loginForm.password).type(password);
cy.get(loginForm.username).focus().type(username);
cy.get(loginForm.password).focus().type(password);
cy.get(loginForm.submit).click();
cy.wait('@postLogin').then((xhr) => {
cy.get(loginForm.submit)
.focus()
.click({force: true})
.wait('@postLogin').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
});
......@@ -97,8 +99,7 @@ Cypress.Commands.add('login', (canary = false, username, password) => {
* @returns void
*/
Cypress.Commands.add('logout', () => {
cy.get(nav.hamburgerMenu).click();
cy.get(nav.logoutButton).click();
cy.visit('/logout')
});
/**
......@@ -111,9 +112,8 @@ Cypress.Commands.add('logout', () => {
* @returns void
*/
Cypress.Commands.add('newUser', (username = '', password = '') => {
cy.visit('/login');
cy.location('pathname', { timeout: 30000 })
cy.visit('/login')
.location('pathname')
.should('eq', `/login`);
cy.server();
......@@ -146,7 +146,7 @@ Cypress.Commands.add('newUser', (username = '', password = '') => {
});
Cypress.Commands.add('preserveCookies', () => {
Cypress.Cookies.preserveOnce('staging', 'minds_sess', 'mwa', 'XSRF-TOKEN');
Cypress.Cookies.preserveOnce('staging', 'minds_sess', 'mwa', 'XSRF-TOKEN', 'staging-features');
});
/**
......@@ -216,6 +216,17 @@ Cypress.Commands.add('post', (message) => {
});
});
/**
* Sets the feature flag cookie.
* @param { Object } flags - JSON object containing flags to turn on
* e.g. { dark mode:false, es-feeds: true }
* @returns void
*/
// Cypress.Commands.add('overrideFeatureFlag', (flags) => {
// const base64 = Buffer.from(JSON.stringify(flags)).toString("base64");
// cy.setCookie('staging-features', base64);
// });
/**
* Converts base64 to blob format
* @param { string } b64Data - The base64 data.
......@@ -244,3 +255,36 @@ function b64toBlob(b64Data, contentType, sliceSize = 512) {
blob.lastModifiedDate = new Date();
return blob;
}
/**
* Check if certain element is on viewport
* @param {*} element
*/
Cypress.Commands.add('isInViewport', element => {
cy.get(element).then($el => {
const bottom = Cypress.$(cy.state('window')).height();
const rect = $el[0].getBoundingClientRect();
expect(rect.top).not.to.be.greaterThan(bottom);
expect(rect.bottom).not.to.be.greaterThan(bottom);
expect(rect.top).not.to.be.greaterThan(bottom);
expect(rect.bottom).not.to.be.greaterThan(bottom);
})
});
/**
* Check if certain element is on viewport
* @param {*} element
*/
Cypress.Commands.add('isNotInViewport', element => {
cy.get(element).then($el => {
const bottom = Cypress.$(cy.state('window')).height();
const rect = $el[0].getBoundingClientRect();
expect(rect.top).to.be.greaterThan(bottom);
expect(rect.bottom).to.be.greaterThan(bottom);
expect(rect.top).to.be.greaterThan(bottom);
expect(rect.bottom).to.be.greaterThan(bottom);
})
});
This diff is collapsed.
......@@ -10,6 +10,7 @@
"build": "ng build --prod",
"prebuild-dev": "gulp build.sass --deploy-url=http://localhost/en",
"build-dev": "ng build --output-path dist/en --deploy-url=/en/ --watch=true --poll=800",
"serve-dev": "ng serve --host=0.0.0.0 --deploy-url=/en/ --configuration=hmr --hmr --poll=800 --progress",
"test": "ng test",
"lint": "ng lint",
"e2e": "cypress run --debug",
......@@ -42,6 +43,7 @@
"material-design-icons": "~3.0.1",
"material-design-lite": "~1.3.0",
"medium-editor": "^5.23.2",
"ngx-drag-drop": "^2.0.0",
"plotly.js": "^1.47.4",
"qrcodejs2": "0.0.2",
"rxjs": "~6.5.2",
......@@ -56,6 +58,7 @@
"@angular/cli": "^7.2.1",
"@angular/compiler-cli": "~8.0.3",
"@angular/language-service": "~8.0.3",
"@angularclass/hmr": "^2.1.3",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.4",
"@types/node": "~10.12.18",
......@@ -87,7 +90,7 @@
},
"husky": {
"hooks": {
"pre-commit": ".githooks/pre-commit && pretty-quick --staged --bail --pattern '**/*.*(ts|html|scss)'"
"pre-commit": "sh .githooks/pre-commit && pretty-quick --staged --bail --pattern \"**/*.*(ts|html|scss)\""
}
}
}
<m-v2-topbar *mIfFeature="'top-feeds'; else legacyTopbar">
<ng-container search>
<m-search--bar [defaultSizes]="false"></m-search--bar>
</ng-container>
<ng-container icons>
<m-notifications--topbar-toggle
*ngIf="session.isLoggedIn()"
></m-notifications--topbar-toggle>
</ng-container>
</m-v2-topbar>
<ng-template #legacyTopbar>
<m-topbar class="m-noshadow">
<ng-container *ngIf="!isProDomain">
<m-v2-topbar *mIfFeature="'top-feeds'; else legacyTopbar">
<ng-container search>
<m-search--bar></m-search--bar>
<m-search--bar [defaultSizes]="false"></m-search--bar>
</ng-container>
<ng-container icons>
<m-notifications--topbar-toggle></m-notifications--topbar-toggle>
<m-wallet--topbar-toggle></m-wallet--topbar-toggle>
<m-notifications--topbar-toggle
*ngIf="session.isLoggedIn()"
></m-notifications--topbar-toggle>
</ng-container>
</m-topbar>
</ng-template>
<m-sidebar--markers
</m-v2-topbar>
<ng-template #legacyTopbar>
<m-topbar class="m-noshadow">
<ng-container search>
<m-search--bar></m-search--bar>
</ng-container>
<ng-container icons>
<m-notifications--topbar-toggle></m-notifications--topbar-toggle>
<m-wallet--topbar-toggle></m-wallet--topbar-toggle>
</ng-container>
</m-topbar>
</ng-template>
<m-sidebar--markers
[class.has-v2-navbar]="featuresService.has('top-feeds')"
></m-sidebar--markers>
</ng-container>
<m-body
[class.has-v2-navbar]="featuresService.has('top-feeds')"
></m-sidebar--markers>
<m-body [class.has-v2-navbar]="featuresService.has('top-feeds')">
[class.is-pro-domain]="isProDomain"
>
<m-announcement [id]="'blockchain:sale'" *ngIf="false">
<span
class="m-blockchain--wallet-address-notice--action"
......@@ -40,7 +45,7 @@
<router-outlet></router-outlet>
</m-body>
<m-messenger *ngIf="minds.LoggedIn"></m-messenger>
<m-messenger *ngIf="minds.LoggedIn && !isProDomain"></m-messenger>
<m-hovercard-popup></m-hovercard-popup>
......@@ -48,10 +53,10 @@
<m--blockchain--transaction-overlay></m--blockchain--transaction-overlay>
<m-modal--tos-updated *ngIf="session.isLoggedIn()"></m-modal--tos-updated>
<m-juryDutySession__summons
*ngIf="session.isLoggedIn()"
*ngIf="session.isLoggedIn() && !isProDomain"
></m-juryDutySession__summons>
<m-modal-signup-on-scroll></m-modal-signup-on-scroll>
<m-modal-signup-on-scroll *ngIf="!isProDomain"></m-modal-signup-on-scroll>
<m-channel--onboarding *ngIf="showOnboarding"></m-channel--onboarding>
......
import { ChangeDetectorRef, Component, NgZone } from '@angular/core';
import { Component, HostBinding } from '@angular/core';
import { NotificationService } from './modules/notifications/notification.service';
import { AnalyticsService } from './services/analytics';
......@@ -7,17 +7,20 @@ import { Session } from './services/session';
import { LoginReferrerService } from './services/login-referrer.service';
import { ScrollToTopService } from './services/scroll-to-top.service';
import { ContextService } from './services/context.service';
import { BlockchainService } from './modules/blockchain/blockchain.service';
import { Web3WalletService } from './modules/blockchain/web3-wallet.service';
import { Client } from './services/api/client';
import { WebtorrentService } from './modules/webtorrent/webtorrent.service';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { ChannelOnboardingService } from './modules/onboarding/channel/onboarding.service';
import { BlockListService } from './common/services/block-list.service';
import { FeaturesService } from './services/features.service';
import { ThemeService } from './common/services/theme.service';
import { BannedService } from './modules/report/banned/banned.service';
import { DiagnosticsService } from './services/diagnostics.service';
import { SiteService } from './common/services/site.service';
import { Subscription } from 'rxjs';
import { RouterHistoryService } from './common/services/router-history.service';
import { PRO_DOMAIN_ROUTES } from './modules/pro/pro.module';
@Component({
moduleId: module.id,
......@@ -32,7 +35,7 @@ export class Minds {
showTOSModal: boolean = false;
paramsSubscription;
protected router$: Subscription;
constructor(
public session: Session,
......@@ -52,20 +55,31 @@ export class Minds {
public featuresService: FeaturesService,
public themeService: ThemeService,
private bannedService: BannedService,
private diagnostics: DiagnosticsService
private diagnostics: DiagnosticsService,
private routerHistoryService: RouterHistoryService,
private site: SiteService
) {
this.name = 'Minds';
if (this.site.isProDomain) {
this.router.resetConfig(PRO_DOMAIN_ROUTES);
}
}
async ngOnInit() {
this.diagnostics.setUser(this.minds.user);
this.diagnostics.listen(); // Listen for user changes
this.notificationService.getNotifications();
if (!this.site.isProDomain) {
this.notificationService.getNotifications();
}
this.session.isLoggedIn(async is => {
if (is) {
this.showOnboarding = await this.onboardingService.showModal();
if (is && !this.site.isProDomain) {
if (!this.site.isProDomain) {
this.showOnboarding = await this.onboardingService.showModal();
}
if (this.minds.user.language !== this.minds.language) {
console.log(
'[app]:: language change',
......@@ -109,6 +123,17 @@ export class Minds {
ngOnDestroy() {
this.loginReferrer.unlisten();
this.scrollToTop.unlisten();
this.paramsSubscription.unsubscribe();
}
@HostBinding('class') get cssColorSchemeOverride() {
if (!this.site.isProDomain || !this.site.pro.scheme) {
return '';
}
return `m-theme--wrapper m-theme--wrapper__${this.site.pro.scheme}`;
}
get isProDomain() {
return this.site.isProDomain;
}
}
......@@ -68,6 +68,9 @@ import { IssuesModule } from './modules/issues/issues.module';
import { CanaryModule } from './modules/canary/canary.module';
import { HttpClientModule } from '@angular/common/http';
import { AnalyticsModule } from './modules/analytics/analytics.module';
import { ProModule } from './modules/pro/pro.module';
import { ChannelContainerModule } from './modules/channel-container/channel-container.module';
import { UpgradesModule } from './modules/upgrades/upgrades.module';
import * as Sentry from '@sentry/browser';
......@@ -104,6 +107,7 @@ export class SentryErrorHandler implements ErrorHandler {
RouterModule.forRoot(MindsAppRoutes, { onSameUrlNavigation: 'reload' }),
CaptchaModule,
CommonModule,
ProModule, // NOTE: Pro Module should be declared _BEFORE_ anything else
AnalyticsModule,
WalletModule,
//CheckoutModule,
......@@ -143,9 +147,11 @@ export class SentryErrorHandler implements ErrorHandler {
MobileModule,
IssuesModule,
CanaryModule,
ChannelsModule,
UpgradesModule,
//last due to :username route
ChannelsModule,
ChannelContainerModule,
],
providers: [
{ provide: ErrorHandler, useClass: SentryErrorHandler },
......
import { Cookie } from '../../services/cookie';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from '../../../environments/environment';
import { Location } from '@angular/common';
import { SiteService } from '../services/site.service';
/**
* API Class
*/
export class MindsHttpClient {
base: string = '/';
origin: string = '';
cookie: Cookie = new Cookie();
static _(http: HttpClient) {
return new MindsHttpClient(http);
static _(http: HttpClient, site: SiteService) {
return new MindsHttpClient(http, site);
}
constructor(public http: HttpClient) {}
constructor(public http: HttpClient, protected site: SiteService) {
if (this.site.isProDomain) {
this.base = window.Minds.site_url;
this.origin = document.location.host;
}
}
/**
* Return a GET request
......@@ -61,21 +69,35 @@ export class MindsHttpClient {
.join('&');
}
x;
/**
* Build the options
*/
private buildOptions(options: Object) {
const XSRF_TOKEN = this.cookie.get('XSRF-TOKEN') || '';
const headers = new HttpHeaders({
const headers = {
'X-XSRF-TOKEN': XSRF_TOKEN,
'X-VERSION': environment.version,
});
};
if (this.origin) {
const PRO_XSRF_JWT = this.cookie.get('PRO-XSRF-JWT') || '';
headers['X-MINDS-ORIGIN'] = this.origin;
headers['X-PRO-XSRF-JWT'] = PRO_XSRF_JWT;
}
return Object.assign(options, {
headers: headers,
const builtOptions = {
headers: new HttpHeaders(headers),
cache: true,
});
};
if (this.origin) {
builtOptions['withCredentials'] = true;
}
return Object.assign(options, builtOptions);
}
}
......
import { NgModule } from '@angular/core';
import { CommonModule as NgCommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { RouterModule, Router } from '@angular/router';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MINDS_PIPES } from './pipes/pipes';
......@@ -102,11 +102,29 @@ import { SettingsService } from '../modules/settings/settings.service';
import { ThemeService } from './services/theme.service';
import { HorizontalInfiniteScroll } from './components/infinite-scroll/horizontal-infinite-scroll.component';
import { ReferralsLinksComponent } from '../modules/wallet/tokens/referrals/links/links.component';
import { PosterDateSelectorComponent } from './components/poster-date-selector/selector.component';
import { ChannelModeSelectorComponent } from './components/channel-mode-selector/channel-mode-selector.component';
import { ShareModalComponent } from '../modules/modals/share/share';
import { RouterHistoryService } from './services/router-history.service';
import { DraggableListComponent } from './components/draggable-list/list.component';
import { DndModule } from 'ngx-drag-drop';
import { SiteService } from './services/site.service';
import { MarketingComponent } from './components/marketing/marketing.component';
import { MarketingFooterComponent } from './components/marketing/footer.component';
import { ToggleComponent } from './components/toggle/toggle.component';
import { MarketingAsFeaturedInComponent } from './components/marketing/as-featured-in.component';
import { SidebarMenuComponent } from './components/sidebar-menu/sidebar-menu.component';
import { ChartV2Component } from './components/chart-v2/chart-v2.component';
import { MiniChartComponent } from './components/mini-chart/mini-chart.component';
@NgModule({
imports: [NgCommonModule, RouterModule, FormsModule, ReactiveFormsModule],
imports: [
NgCommonModule,
DndModule,
RouterModule,
FormsModule,
ReactiveFormsModule,
],
declarations: [
MINDS_PIPES,
......@@ -194,6 +212,15 @@ import { ShareModalComponent } from '../modules/modals/share/share';
SwitchComponent,
FeaturedContentComponent,
PosterDateSelectorComponent,
DraggableListComponent,
ToggleComponent,
MarketingComponent,
MarketingFooterComponent,
MarketingAsFeaturedInComponent,
SidebarMenuComponent,
ChartV2Component,
MiniChartComponent,
],
exports: [
MINDS_PIPES,
......@@ -278,9 +305,16 @@ import { ShareModalComponent } from '../modules/modals/share/share';
SwitchComponent,
NSFWSelectorComponent,
FeaturedContentComponent,
PosterDateSelectorComponent,
ChannelModeSelectorComponent,
DraggableListComponent,
ToggleComponent,
MarketingComponent,
MarketingAsFeaturedInComponent,
SidebarMenuComponent,
],
providers: [
SiteService,
{
provide: AttachmentService,
useFactory: AttachmentService._,
......@@ -296,7 +330,7 @@ import { ShareModalComponent } from '../modules/modals/share/share';
{
provide: MindsHttpClient,
useFactory: MindsHttpClient._,
deps: [HttpClient],
deps: [HttpClient, SiteService],
},
{
provide: NSFWSelectorCreatorService,
......@@ -338,6 +372,11 @@ import { ShareModalComponent } from '../modules/modals/share/share';
new FeaturedContentService(boostedContentService),
deps: [FeedsService],
},
{
provide: RouterHistoryService,
useFactory: router => new RouterHistoryService(router),
deps: [Router],
},
],
entryComponents: [
NotificationsToasterComponent,
......
......@@ -43,6 +43,15 @@ minds-button {
}
}
a.m-link-btn {
display: inline-block;
padding: 8px !important;
line-height: 1.2;
height: auto;
text-decoration: none;
font-weight: inherit;
}
.m-btn--slim {
height: 32px;
}
......
......@@ -8,6 +8,8 @@ import {
ChangeDetectorRef,
ComponentRef,
ElementRef,
Injector,
SkipSelf,
} from '@angular/core';
import { DynamicHostDirective } from '../../directives/dynamic-host.directive';
......@@ -20,12 +22,14 @@ import { VideoCard } from '../../../modules/legacy/components/cards/object/video
import { AlbumCard } from '../../../modules/legacy/components/cards/object/album/album';
import { BlogCard } from '../../../modules/blogs/card/card';
import { CommentComponentV2 } from '../../../modules/comments/comment/comment.component';
import { ActivityService } from '../../services/activity.service';
@Component({
selector: 'minds-card',
template: `
<ng-template dynamic-host></ng-template>
`,
providers: [ActivityService],
})
export class MindsCard implements AfterViewInit {
@ViewChild(DynamicHostDirective, { static: true })
......@@ -43,7 +47,10 @@ export class MindsCard implements AfterViewInit {
private initialized: boolean = false;
constructor(private _componentFactoryResolver: ComponentFactoryResolver) {}
constructor(
private _componentFactoryResolver: ComponentFactoryResolver,
private _injector: Injector
) {}
@Input('object') set _object(value: any) {
const oldType = this.type;
......@@ -121,7 +128,11 @@ export class MindsCard implements AfterViewInit {
viewContainerRef.clear();
this.componentRef = viewContainerRef.createComponent(componentFactory);
this.componentRef = viewContainerRef.createComponent(
componentFactory,
undefined,
this._injector
);
this.componentInstance = this.componentRef.instance;
this.anchorRef = viewContainerRef.element;
......
<!-- <div
#chartContainer
class="m-chartV2__chartContainer"
[ngClass]="{ isTouchDevice: isTouchDevice }"
>
<plotly-plot
*ngIf="init"
#graphDiv
id="graphDiv"
[data]="data"
[layout]="layout"
[config]="config"
[useResizeHandler]="true"
[style]="{ position: 'relative' }"
(hover)="onHover($event)"
(unhover)="onUnhover($event)"
(plotly_click)="onClick($event)"
>
</plotly-plot>
</div>
<div #hoverInfoDiv id="hoverInfoDiv" class="m-chartV2__hoverInfoDiv">
<i *ngIf="isTouchDevice" class="material-icons" (click)="onUnhover($event)"
>close</i
>
<div class="m-chartV2__hoverInfo__row">
{{ hoverInfo.date | utcDate | date: datePipe }}
</div>
<div
[ngSwitch]="selectedMetric?.unit"
class="m-chartV2__hoverInfo__row--primary"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.value | number }} {{ selectedMetric.label | lowercase }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.value | currency }} USD
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.value | number: '1.1-3' }} {{ selectedMetric?.unit }}
</ng-template>
</div>
<div class="m-chartV2__hoverInfo__row" *ngIf="isComparison">
vs
<ng-container
[ngSwitch]="selectedMetric?.unit"
class="m-chartV2__hoverInfo__row"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.comparisonValue | number }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.comparisonValue | currency }}
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.comparisonValue | number: '1.1-3' }}
</ng-template>
</ng-container>
on {{ hoverInfo.comparisonDate | utcDate | date: datePipe }}
</div>
</div> -->
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChartV2Component } from './chart-v2.component';
describe('ChartV2Component', () => {
let component: ChartV2Component;
let fixture: ComponentFixture<ChartV2Component>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ChartV2Component],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ChartV2Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
This diff is collapsed.
@import 'themes';
m-draggable-list {
ul.m-draggableList__list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
transition: all ease 300ms;
&.dndDragover {
padding-top: 16px;
padding-bottom: 16px;
}
li.m-draggableList__listItem {
padding: 8px;
border: 1px solid #ddd;
display: flex;
align-items: center;
.handle {
@include m-theme() {
color: themed($grey-600);
}
}
}
}
}
import { Component, ContentChild, Input, TemplateRef } from '@angular/core';
import { DndDropEvent, EffectAllowed } from 'ngx-drag-drop';
@Component({
selector: 'm-draggable-list',
template: `
<ul
dndDropzone
[dndHorizontal]="false"
[dndEffectAllowed]="dndEffectAllowed"
(dndDrop)="onDrop($event)"
class="m-draggableList__list"
>
<div
class="dndPlaceholder"
dndPlaceholderRef
style="min-height:100px;border:1px dashed green;background-color:rgba(0, 0, 0, 0.1)"
></div>
<li
*ngFor="let item of data; let i = index; trackBy: trackByFunction"
[dndDraggable]="item"
[dndEffectAllowed]="'move'"
class="m-draggableList__listItem"
>
<i class="handle material-icons" dndHandle>reorder</i>
<ng-container
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{ item: item, i: i }"
></ng-container>
</li>
</ul>
`,
})
export class DraggableListComponent {
@Input() data: Array<any>;
@Input() dndEffectAllowed: EffectAllowed = 'copyMove';
@Input() id: string;
@ContentChild(TemplateRef, { static: false }) template: TemplateRef<any>;
trackByFunction(index, item) {
return this.id ? item[this.id] + index : index;
}
onDrop(event: DndDropEvent) {
if (
this.data &&
(event.dropEffect === 'copy' || event.dropEffect === 'move')
) {
let dragIndex = this.data.findIndex(
item => event.data[this.id] === item[this.id]
);
let dropIndex = event.index || this.data.length;
// remove element
this.data.splice(dragIndex, 1);
// add it back to new index
if (dragIndex < dropIndex) {
dropIndex--;
}
this.data.splice(dropIndex, 0, event.data);
}
}
}
......@@ -206,6 +206,10 @@ export class ButtonsPlugin {
let $buttons = this.$element.querySelector('.medium-insert-buttons');
let $p = this.$element.querySelector('.medium-insert-active');
if (!$buttons) {
return;
}
if ($p !== null) {
let $lastCaption = $p.classList.contains('medium-insert-images-grid')
? []
......@@ -439,7 +443,9 @@ export class ButtonsPlugin {
const $input = this.$element.querySelector('.medium-media-buttons');
if ($input) {
$input.parentNode.removeChild($input);
try {
$input.parentNode.removeChild($input);
} catch (e) {}
}
}
......
......@@ -247,7 +247,9 @@ export class EmbedImage {
if ($image.tagName === 'SPAN') {
$image = $image.parentNode.querySelector('img');
} else if ($image.tagName !== 'IMG') {
}
if (!$image || $image.tagName !== 'IMG') {
return;
}
......
......@@ -34,7 +34,7 @@ export class FeaturedContentComponent implements OnInit {
protected componentFactoryResolver: ComponentFactoryResolver,
protected cd: ChangeDetectorRef,
protected clientMetaService: ClientMetaService,
@SkipSelf() injector: Injector
@SkipSelf() protected injector: Injector
) {
this.clientMetaService.inherit(injector).setMedium('featured-content');
}
......@@ -81,7 +81,11 @@ export class FeaturedContentComponent implements OnInit {
const componentRef: ComponentRef<
any
> = this.dynamicHost.viewContainerRef.createComponent(componentFactory);
> = this.dynamicHost.viewContainerRef.createComponent(
componentFactory,
void 0,
this.injector
);
injector.call(this, componentRef, this.entity);
}
}
......
<div class="m-grid m-marketing__asFeaturedIn">
<div class="m-grid__column-2 m-marketingAsFeaturedIn__title" i18n>
As featured in
</div>
<ul class="m-grid__column-10">
<li>
<a
href="https://www.wsj.com/articles/facebook-on-notice-as-vietnam-tightens-grip-on-social-media-11547036594"
target="_blank"
>
<img
[src]="cdnAssetsUrl + 'assets/marketing/wsj.png'"
alt="The Wall Street Journal"
/>
</a>
</li>
<li>
<a
href="https://www.wired.com/story/minds-anti-facebook/"
target="_blank"
>
<img [src]="cdnAssetsUrl + 'assets/marketing/wired.png'" alt="Wired" />
</a>
</li>
<li>
<a
href="https://techcrunch.com/2018/04/16/minds-aims-to-decentralize-the-social-network/"
target="_blank"
>
<img
[src]="cdnAssetsUrl + 'assets/marketing/techcrunch.png'"
alt="TechCrunch"
/>
</a>
</li>
<li>
<a
href="http://podcasts.joerogan.net/podcasts/bill-ottman"
target="_blank"
>
<img
[src]="cdnAssetsUrl + 'assets/marketing/tjre.png'"
alt="The Joe Rogan Experience"
/>
</a>
</li>
<li>
<a
href="https://www.foxnews.com/tech/alternate-social-media-squash-extremist-content-without-violating-first-amendment"
target="_blank"
>
<img
[src]="cdnAssetsUrl + 'assets/marketing/foxnews.png'"
alt="Fox News"
/>
</a>
</li>
<li>
<a
href="https://www.independent.co.uk/news/business/indyventure/minds-facebook-alternative-deletefacebook-social-network-data-a8475841.html"
target="_blank"
>
<img
[src]="cdnAssetsUrl + 'assets/marketing/press-logos/independent.png'"
alt="Independent"
/>
</a>
</li>
<li>
<a
href="https://www.reuters.com/article/us-vietnam-cyber-usa/u-s-lawmakers-urge-google-facebook-to-resist-vietnam-cybersecurity-law-idUSKBN1K7147"
target="_blank"
>
<img
[src]="cdnAssetsUrl + 'assets/marketing/reuters.png'"
alt="Reuters"
/>
</a>
</li>
<li>
<a
href="https://www.npr.org/2019/08/06/748810962/debate-over-policing-free-speech-intensifies-as-8chan-struggles-to-stay-online"
target="_blank"
>
<img [src]="cdnAssetsUrl + 'assets/marketing/npr.png'" alt="npr" />
</a>
</li>
</ul>
</div>
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'm-marketing__asFeaturedIn',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'as-featured-in.component.html',
})
export class MarketingAsFeaturedInComponent {
readonly cdnAssetsUrl: string = window.Minds.cdn_assets_url;
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.