...
 
Commits (18)
......@@ -31,6 +31,8 @@ tmtags
Thumbs.db
Desktop.ini
node_modules
cypress/screenshots
cypress/videos
# don't ignore travis config
!/.travis.yml
!/.drone.yml
......
......@@ -16,8 +16,8 @@ stages:
variables:
CYPRESS_INSTALL_BINARY: 0 # Speeds up the install process
npm_config_cache: "$CI_PROJECT_DIR/.npm"
CYPRESS_CACHE_FOLDER: "$CI_PROJECT_DIR/cache/Cypress"
npm_config_cache: '$CI_PROJECT_DIR/.npm'
CYPRESS_CACHE_FOLDER: '$CI_PROJECT_DIR/cache/Cypress'
test:
image: circleci/node:8-browsers
......@@ -68,14 +68,14 @@ e2e:chrome:
build:review:
stage: build
before_script:
- sed -ri "s|\"VERSION\"|\"$CI_PIPELINE_ID\"|" src/environments/environment.prod.ts
- sed -ri "s|'VERSION'|'$CI_PIPELINE_ID'|" src/environments/environment.prod.ts
script:
- npm ci && npm install -g gulp-cli
- npm run postinstall
- gulp build.sass && gulp build.sass ##weird build needs to be run twice for now
- sh build/base-locale.sh dist
artifacts:
name: "$CI_COMMIT_REF_SLUG"
name: '$CI_COMMIT_REF_SLUG'
paths:
- dist
except:
......@@ -86,14 +86,14 @@ build:review:
build:production:en:
stage: build
before_script:
- sed -ri "s|\"VERSION\"|\"$CI_PIPELINE_ID\"|" src/environments/environment.prod.ts
- sed -ri "s|'VERSION'|'$CI_PIPELINE_ID'|" src/environments/environment.prod.ts
script:
- npm ci && npm install -g gulp-cli
- npm run postinstall
- gulp build.sass --deploy-url=https://cdn-assets.minds.com/front/dist/en && gulp build.sass --deploy-url=https://cdn-assets.minds.com/front/dist/en ##weird build needs to be run twice for now
- sh build/base-locale.sh dist https://cdn-assets.minds.com/front/dist
artifacts:
name: "$CI_COMMIT_REF_SLUG"
name: '$CI_COMMIT_REF_SLUG'
paths:
- dist/en
only:
......@@ -104,14 +104,14 @@ build:production:en:
build:production:i18n:
stage: build
before_script:
- sed -ri "s|\"VERSION\"|\"$CI_PIPELINE_ID\"|" src/environments/environment.prod.ts
- sed -ri "s|'VERSION'|'$CI_PIPELINE_ID'|" src/environments/environment.prod.ts
script:
- npm ci && npm install -g gulp-cli
- npm run postinstall
- gulp build.sass --deploy-url=https://cdn-assets.minds.com/front/dist/en && gulp build.sass --deploy-url=https://cdn-assets.minds.com/front/dist/en ##weird build needs to be run twice for now
- sh build/i18n-locales-all.sh dist https://cdn-assets.minds.com/front/dist
artifacts:
name: "$CI_COMMIT_REF_SLUG"
name: '$CI_COMMIT_REF_SLUG'
paths:
- dist/vi
only:
......@@ -138,7 +138,7 @@ prepare:review:
stage: prepare
image: minds/ci:latest
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- 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/.
- docker push $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID
dependencies:
......@@ -151,7 +151,7 @@ prepare:review:
prepare:review:sentry:
<<: *sentry_prepare
variables:
SOURCEMAP_PREFIX: "~/en"
SOURCEMAP_PREFIX: '~/en'
except:
refs:
- master
......@@ -163,7 +163,7 @@ prepare:production:
stage: prepare
image: minds/ci:latest
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- 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/.
- docker push $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID
only:
......@@ -177,7 +177,7 @@ prepare:production:
prepare:production:sentry:
<<: *sentry_prepare
variables:
SOURCEMAP_PREFIX: "~/front/dist/en"
SOURCEMAP_PREFIX: '~/front/dist/en'
only:
refs:
- master
......@@ -185,7 +185,7 @@ prepare:production:sentry:
dependencies:
- build:production:en
- build:production:i18n
################
# Review Stage #
################
......@@ -201,7 +201,7 @@ prepare:production:sentry:
action: stop
variables:
GIT_STRATEGY: none
except:
except:
refs:
- master
- test/gitlab-ci
......@@ -213,22 +213,22 @@ review:start:
- aws eks update-kubeconfig --name=sandbox
- git clone --branch=sandbox-wip https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/minds/helm-charts.git
- "helm upgrade \
--install \
--reuse-values \
--set frontInit.image.repository=$CI_REGISTRY_IMAGE/front-init \
--set-string frontInit.image.tag=$CI_PIPELINE_ID \
--set domain=$CI_BUILD_REF_SLUG.$KUBE_INGRESS_BASE_DOMAIN \
--set elasticsearch.clusterName=$CI_BUILD_REF_SLUG--elasticsearch \
--wait \
$CI_BUILD_REF_SLUG \
./helm-charts/minds"
--install \
--reuse-values \
--set frontInit.image.repository=$CI_REGISTRY_IMAGE/front-init \
--set-string frontInit.image.tag=$CI_PIPELINE_ID \
--set domain=$CI_BUILD_REF_SLUG.$KUBE_INGRESS_BASE_DOMAIN \
--set elasticsearch.clusterName=$CI_BUILD_REF_SLUG--elasticsearch \
--wait \
$CI_BUILD_REF_SLUG \
./helm-charts/minds"
# Update sentry
- sentry-cli releases deploys $CI_PIPELINE_ID new -e review-$CI_COMMIT_REF_SLUG
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://$CI_BUILD_REF_SLUG.$KUBE_INGRESS_BASE_DOMAIN
on_stop: review:stop
except:
except:
refs:
- master
- test/gitlab-ci
......@@ -249,7 +249,7 @@ review:stop:
- aws s3 sync dist $S3_REPOSITORY_URL
- $(aws ecr get-login --no-include-email --region us-east-1)
## Update docker front-init container
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker pull $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID
- docker tag $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID $ECR_REPOSITORY_URL:$IMAGE_LABEL
- docker push $ECR_REPOSITORY_URL:$IMAGE_LABEL
......@@ -269,7 +269,7 @@ staging:fpm:
<<: *deploy
stage: deploy:staging
variables:
IMAGE_LABEL: "staging"
IMAGE_LABEL: 'staging'
ECS_SERVICE: $ECS_APP_STAGING_SERVICE
environment:
name: staging
......@@ -279,7 +279,7 @@ deploy:canary:
<<: *deploy
stage: deploy:canary
variables:
IMAGE_LABEL: "canary"
IMAGE_LABEL: 'canary'
ECS_SERVICE: $ECS_APP_CANARY_SERVICE
environment:
name: canary
......@@ -291,7 +291,7 @@ deploy:production:
<<: *deploy
stage: deploy:production
variables:
IMAGE_LABEL: "production"
IMAGE_LABEL: 'production'
ECS_SERVICE: $ECS_APP_PRODUCTION_SERVICE
environment:
name: production
......@@ -306,7 +306,7 @@ deploy:production:
cleanup:review: # We stop the review site after the e2e tests have run
<<: *cleanup_review
stage: cleanup
except:
except:
refs:
- master
- test/gitlab-ci
{
"projectId": "qrjqcv",
"requestTimeout": 3600000,
"responseTimeout": 3600000,
"pageLoadTimeout": 3600000
......
// import 'cypress-file-upload';
context('Blogs', () => {
beforeEach(() => {
cy.login(true);
before(() => {
cy.clearCookies();
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
});
cy.location('pathname', { timeout: 30000 })
.should('eq', `/newsfeed/subscriptions`);
})
beforeEach(()=> {
cy.preserveCookies();
});
it('should not be able to create a new blog if no title or banner are specified', () => {
it('should not be able to create a new blog if no title or banner are specified', () => {
cy.visit('/blog/edit/new');
cy.get('.m-button--submit').click();
cy.wait(100);
cy.get('.m-blog--edit--error').contains('Error: You must provide a title');
......@@ -26,26 +31,6 @@ context('Blogs', () => {
cy.get('.m-blog--edit--error').contains('Error: You must upload a banner');
})
// TODO: remove the x when we run tests in new users each time
xit("should not be able to create a new blog if the channel doesn't have an avatar", () => {
cy.visit('/blog/edit/new');
cy.uploadFile('minds-banner #file', '../fixtures/international-space-station-1776401_1920.jpg', 'image/jpg');
cy.get('minds-textarea .m-editor').type('Title');
cy.get('m-inline-editor .medium-editor-element').type('Content\n');
cy.wait(1000);
cy.server();
cy.route("POST", "**!/api/v1/blog/new").as("newBlog");
cy.get('.m-button--submit').click({ force: true }); // TODO: Investigate why disabled flag is being detected
cy.get('h1.m-blog--edit--error').contains('Error: Please ensure your channel has an avatar before creating a blog');
});
it('should be able to create a new blog', () => {
// upload avatar first
......@@ -53,8 +38,6 @@ context('Blogs', () => {
cy.get('.m-channel--name .minds-button-edit button:first-child').click();
cy.wait(100);
cy.uploadFile('.minds-avatar input[type=file]', '../fixtures/avatar.jpeg', 'image/jpg');
cy.get('.m-channel--name .minds-button-edit button:last-child').click();
......@@ -106,14 +89,24 @@ context('Blogs', () => {
cy.get('.m-mature-info a').click();
cy.get('.m-mature-info a span').contains('Mature content');
cy.server();
cy.route("POST", "**/api/v1/blog/new").as("postBlog");
cy.route("GET", "**/api/v1/blog/**").as("getBlog");
cy.get('.m-button--submit').click({ force: true }); // TODO: Investigate why disabled flag is being detected
cy.clock();
cy.clock().then((clock) => { clock.tick(1000); });
cy.wait('@postBlog').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
cy.wait(1000);
cy.wait('@getBlog').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
expect(xhr.response.body).to.have.property("blog");
});
cy.location('pathname', { timeout: 30000 })
cy.location('pathname')
.should('contains', `/${Cypress.env().username}/blog`);
cy.get('.m-blog--title').contains('Title');
......
context('Boost Console', () => {
const postContent = "Test boost, please reject..." + Math.random().toString(36);
beforeEach(() => {
cy.login(true);
cy.wait(5000);
cy.visit('/newsfeed/subscriptions');
cy.wait(3000);
cy.location('pathname', { timeout: 30000 })
.should('eq', `/newsfeed/subscriptions`);
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
cy.visit('/newsfeed/subscribed');
newBoost(postContent, 100);
});
it('should show a new boost in the console', () => {
cy.visit('/boost/console/newsfeed/history');
cy.wait(3000);
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.wait(1000);
cy.get('m-boost-console-card:nth-child(1) .m-mature-message span')
.contains(postContent);
});
......@@ -36,7 +34,6 @@ context('Boost Console', () => {
cy.get('m-boost-console-card:nth-child(1) .m-boost-card--manager-item--buttons > button')
.click();
cy.wait(1000);
cy.get('m-boost-console-card:nth-child(1) div.m-boost-card--manager-item.m-boost-card--state')
.contains('revoked');
......@@ -44,29 +41,31 @@ context('Boost Console', () => {
function navToConsole() {
cy.visit('/boost/console/newsfeed/history');
cy.wait(3000);
cy.location('pathname', { timeout: 30000 })
cy.location('pathname')
.should('eq', `/boost/console/newsfeed/history`);
}
function newBoost(text, views) {
cy.server();
cy.route("POST", '**/api/v2/boost/**').as('boostPost');
cy.post(text);
cy.wait(2000);
cy.get('#boost-actions')
.first()
.click();
cy.wait(5000);
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(5000);
cy.wait('@boostPost').then((xhr) => {
cy.log(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', () => {
beforeEach(() => {
cy.login(true);
before(() => {
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.location('pathname', { timeout: 30000 })
.should('eq', '/newsfeed/subscriptions');
beforeEach(()=> {
cy.preserveCookies();
});
it('should register views on scroll', () => {
......@@ -13,48 +22,36 @@ context('Boost Impressions', () => {
cy.route("POST", "**/api/v2/analytics/views/activity/*").as("analytics");
//load, scroll, wait to trigger analytics
cy.wait(3000);
cy.scrollTo(0, 500);
cy.wait(3000);
//assert
cy.wait('@analytics', { requestTimeout: 5000 }).then((xhr) => {
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 forward', () => {
it('should register views on boost rotate', () => {
//stub endpoint
cy.server();
cy.route("POST", "**/api/v2/analytics/views/boost/*").as("analytics");
cy.wait(3000);
//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 > div > ul > li:nth-child(3) > i')
.click();
cy.wait(3000);
//assert
cy.wait('@analytics', { requestTimeout: 5000 }).then((xhr) => {
cy.wait('@analytics').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
});
it('should register views on boost rotate backward', () => {
//stub endpoint
cy.server();
cy.route("POST", "**/api/v2/analytics/views/boost/*").as("analytics");
cy.wait(3000);
//rotate forward and wait to trigger analytics
cy.get('m-newsfeed--boost-rotator > div > ul > li:nth-child(1) > i')
cy.get('m-newsfeed--boost-rotator > div > ul > li:nth-child(2) > i')
.click();
cy.wait(3000);
//assert
cy.wait('@analytics', { requestTimeout: 5000 }).then((xhr) => {
cy.wait('@analytics').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
......
context('Channel', () => {
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.visit(`/${Cypress.env().username}`);
});
beforeEach(()=> {
cy.preserveCookies();
});
after(()=> {
cy.get('.m-channel-mode-selector--dropdown')
.click()
.find(".m-dropdown--list--item:contains('Public')")
.should('be.visible')
.click();
});
it('should change channel mode to public', () => {
cy.get('.m-channel-mode-selector--dropdown')
.click()
.find(".m-dropdown--list--item:contains('Public')")
.should('be.visible')
.click();
cy.get('.m-channel-mode-selector--dropdown')
.find('label').contains('Public');
});
it('should change channel mode to moderated', () => {
cy.get('.m-channel-mode-selector--dropdown')
.click()
.find(".m-dropdown--list--item:contains('Moderated')")
.should('be.visible')
.click();
cy.get('.m-channel-mode-selector--dropdown')
.find('label').contains('Moderated');
});
it('should change channel mode to closed', () => {
cy.get('.m-channel-mode-selector--dropdown')
.click()
.find(".m-dropdown--list--item:contains('Closed')")
.should('be.visible')
.click();
cy.get('.m-channel-mode-selector--dropdown')
.find('label').contains('Closed');
});
});
/**
* @author Ben Hayward
* @author Ben Hayward
* @create date 2019-08-09 14:42:51
* @modify date 2019-08-09 14:42:51
* @desc Spec tests for comment threads.
......@@ -12,95 +12,90 @@ context('Comment Threads', () => {
3: 'test tier 3',
};
const hamburgerMenu = '.m-v2-topbar__UserMenu > m-user-menu > div.m-user-menu.m-dropdown > a';
const logoutButton = '.m-user-menu.m-dropdown > ul > li:nth-child(11) > a';
const postMenu = 'minds-activity:nth-child(2) > div > m-post-menu > button > i';
const deletePostOption = 'minds-activity:nth-child(2) m-post-menu > ul > li:nth-child(4)';
const deletePostButton = 'm-modal-confirm div:nth-child(1) > div > button.mdl-button--colored';
const postMenu = 'minds-activity:first > div > m-post-menu > button > i';
const deletePostOption = "m-post-menu > ul > li:visible:contains('Delete')";
const deletePostButton = ".m-modal-confirm-buttons > button:contains('Delete')";
const channelButton = '.m-v2-topbar__Top > div > a > minds-avatar > div';
const postCommentButton = 'm-comment__poster > div > div.minds-body > div > div > a.m-post-button';
const thumbsUpButtons = '.m-comment__toolbar minds-button-thumbs-up';
const thumbsDownButtons = '.m-comment__toolbar minds-button-thumbs-down';
const thumbsUpCounters = '.m-comment__toolbar > div > minds-button-thumbs-up > a > span';
const thumbsDownCounters = '.m-comment__toolbar > div > minds-button-thumbs-down > a > span';
// pass in tier / tree depth.
const replyButton = (index) => `minds-activity:nth-child(${index}) .m-comment__toolbar > div > span`;
const commentButton = (index) => `minds-activity:nth-child(${index}) minds-button-comment`;
const commentInput = (index) => `minds-activity:nth-child(${index}) m-text-input--autocomplete-container > minds-textarea > div`;
const commentContent = (index) => `minds-activity:nth-child(${index}) m-comments__tree .m-comment__bubble > p`;
const replyButton = `minds-activity:first .m-comment__toolbar > div > span`;
const commentButton = `minds-activity:first minds-button-comment`;
const commentInput = `minds-activity:first m-text-input--autocomplete-container > minds-textarea > div`;
const commentContent = `minds-activity:first m-comments__tree .m-comment__bubble > p`;
before(() => {
//make a post new.
login();
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.post('test post');
//manually sign-out.
cy.get(hamburgerMenu).click();
cy.get(logoutButton).click();
});
beforeEach(()=> {
cy.preserveCookies();
});
after(() => {
//delete the post
cy.wait(1000);
cy.get(postMenu).click();
cy.get(deletePostOption).click();
cy.get(deletePostButton).click();
});
beforeEach(() => {
login();
cy.wait(2000);
});
it('should post three tiers of comments', () => {
//Reveal the conversation
cy.get(commentButton).click();
it('should allow a user to post a tier 1 comment', () => {
cy.get(commentButton(2)).click();
cy.get(commentInput(2)).type(testMessage[1]);
//Add the first level of comments
cy.get(commentInput).type(testMessage[1]);
cy.get(postCommentButton).click();
cy.get(commentContent(2)).contains(testMessage[1]);
});
it('should allow a user to post a tier 2 comment', () => {
//expand top comment, then top reply button.
cy.get(commentButton(2)).click();
cy.get(replyButton(2)).click();
cy.get(commentInput(2)).first().type(testMessage[2]);
cy.get(postCommentButton).first().click();
cy.get(commentContent(2)).contains(testMessage[2]);
});
it('should allow a user to post a tier 3 comment', () => {
//expand top comment, then top reply button.
cy.get(commentButton(2)).click();
cy.get(replyButton(2)).click();
cy.wait(1000);
cy.get(commentContent).contains(testMessage[1]);
//there are two reply buttons now, use the last one.
cy.get(replyButton(2)).last().click();
cy.wait(1000);
//Add the second level of comments
cy.get(replyButton).click();
cy.get(commentInput)
.first()
.type(testMessage[2]);
cy.get(postCommentButton)
.first()
.click();
cy.get(commentContent).contains(testMessage[2]);
//check the comments.
cy.get(commentInput(2)).first().type(testMessage[3]);
cy.get(postCommentButton).first().click();
cy.get(commentContent(2)).contains(testMessage[3]);
});
it('should allow the user to vote up and down comments', () => {
//expand top comment, then top reply button.
cy.get(commentButton(2)).click();
cy.get(replyButton(2)).click();
cy.wait(1000);
//Add the third level of comments
cy.get('minds-activity:first')
.find('m-comments__tree m-comments__thread m-comment')
.find('m-comments__thread m-comment:nth-child(2) .m-comment__toolbar > div > span')
.last()
.click();
cy.get(commentInput)
.first()
.type(testMessage[3]);
cy.get(postCommentButton)
.first()
.click();
cy.get(commentContent).contains(testMessage[3]);
//there are two reply buttons now, use the last one.
cy.get(replyButton(2)).last().click();
cy.wait(1000);
//click thumbs up and down
cy.get(thumbsDownButtons).click({multiple: true});
cy.get(thumbsUpButtons).click({multiple: true});
cy.get('.m-comment__toolbar')
.find('minds-button-thumbs-up')
.click({multiple: true});
cy.get('.m-comment__toolbar')
.find('minds-button-thumbs-down')
.click({multiple: true});
// check the values
cy.get(thumbsUpCounters)
......@@ -109,12 +104,4 @@ context('Comment Threads', () => {
.each((counter) => expect(counter.context.innerHTML).to.eql('1'));
});
function login() {
cy.login(true);
cy.location('pathname', { timeout: 30000 })
.should('eq', `/newsfeed/subscriptions`);
cy.get(channelButton).click();
}
})
context('Discovery', () => {
beforeEach(() => {
cy.login(true);
cy.location('pathname', { timeout: 30000 })
.should('eq', `/newsfeed/subscriptions`);
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.visit('/newsfeed/global/top');
});
beforeEach(()=> {
cy.preserveCookies();
});
it('should allow a user to post on the discovery page', () => {
cy.visit('/newsfeed/global/top');
cy.post("test!!");
});
it('should be able to filter by hot', () => {
cy.visit('/newsfeed/global/top');
cy.get('.m-sort-selector--algorithm-dropdown ul > li:nth-child(1)')
cy.get(".m-sort-selector--algorithm-dropdown ul > li:contains('Hot')")
.click()
.should('have.css', 'color', 'rgb(70, 144, 223)'); // selected color
.should('have.class', 'm-dropdown--list--item--selected'); // selected class
cy.url().should('include', '/hot');
});
it('should be able to filter by top', () => {
cy.visit('/newsfeed/global/hot');
cy.get('.m-sort-selector--algorithm-dropdown ul > li:nth-child(2)')
cy.get(".m-sort-selector--algorithm-dropdown ul > li:contains('Top')")
.click()
.should('have.css', 'color', 'rgb(70, 144, 223)'); // selected color
.should('have.class', 'm-dropdown--list--item--selected'); // selected class
cy.url().should('include', '/top');
});
it('should be able to filter by time in the top feed', () => {
cy.visit('/newsfeed/global/top');
cy.get('.m-sort-selector--period-dropdown').click();
cy.get('.m-sort-selector--period-dropdown ul > li:nth-child(5)').click();
cy.url().should('include', '=1y');
cy.get('.m-sort-selector--period-dropdown').click();
cy.get('.m-sort-selector--period-dropdown ul > li:nth-child(4)').click();
cy.get(".m-sort-selector--period-dropdown ul > li:contains('30d')").click();
cy.url().should('include', '=30d');
cy.get('.m-sort-selector--period-dropdown').click();
cy.get('.m-sort-selector--period-dropdown ul > li:nth-child(3)').click();
cy.get(".m-sort-selector--period-dropdown ul > li:contains('7d')").click();
cy.url().should('include', '=7d');
cy.get('.m-sort-selector--period-dropdown').click();
cy.get('.m-sort-selector--period-dropdown ul > li:nth-child(2)').click();
cy.get(".m-sort-selector--period-dropdown ul > li:contains('24h')").click();
cy.url().should('include', '=24h');
cy.get('.m-sort-selector--period-dropdown').click();
cy.get('.m-sort-selector--period-dropdown ul > li:nth-child(1)').click();
cy.get(".m-sort-selector--period-dropdown ul > li:contains('12h')").click();
cy.url().should('include', '=12h');
});
it('should filter by latest', () => {
cy.visit('/newsfeed/global/hot');
cy.get('.m-sort-selector--algorithm-dropdown ul > li:nth-child(3)')
cy.get(".m-sort-selector--algorithm-dropdown ul > li:contains('Latest')")
.click()
.should('have.css', 'color', 'rgb(70, 144, 223)'); // selected color
.should('have.class', 'm-dropdown--list--item--selected'); // selected class
cy.url().should('include', '/latest');
});
it('should filter by image', () => {
cy.visit('/newsfeed/global/hot');
cy.get('.m-sort-selector--custom-type-dropdown').click();
cy.get('.m-sort-selector--custom-type-dropdown ul > li:nth-child(2)').click();
cy.get(".m-sort-selector--custom-type-dropdown ul > li:contains('photo')")
.click();
cy.url().should('include', '=images');
});
it('should filter by video', () => {
cy.visit('/newsfeed/global/hot');
cy.get('.m-sort-selector--custom-type-dropdown').click();
cy.get('.m-sort-selector--custom-type-dropdown ul > li:nth-child(3)').click();
cy.get(".m-sort-selector--custom-type-dropdown ul > li:contains('videocam')")
.click();
cy.url().should('include', '=videos');
});
it('should filter by blog', () => {
cy.visit('/newsfeed/global/hot');
cy.get('.m-sort-selector--custom-type-dropdown').click();
cy.get('.m-sort-selector--custom-type-dropdown ul > li:nth-child(4)').click();
cy.get(".m-sort-selector--custom-type-dropdown ul > li:contains('subject')")
.click();
cy.url().should('include', '=blog');
});
it('should filter by channels', () => {
cy.visit('/newsfeed/global/hot');
cy.get('.m-sort-selector--custom-type-dropdown').click();
cy.get('.m-sort-selector--custom-type-dropdown ul > li:nth-child(5)').click();
cy.get(".m-sort-selector--custom-type-dropdown ul > li:contains('people')")
.click();
cy.url().should('include', '=channels');
});
it('should filter by groups', () => {
cy.visit('/newsfeed/global/hot');
cy.get('.m-sort-selector--custom-type-dropdown').click();
cy.get('.m-sort-selector--custom-type-dropdown ul > li:nth-child(6)').click();
cy.get(".m-sort-selector--custom-type-dropdown ul > li:contains('group_work')")
.click();
cy.url().should('include', '=groups');
});
it('should filter by all', () => {
cy.visit('/newsfeed/global/top?type=images');
cy.get('.m-sort-selector--custom-type-dropdown').click();
cy.get('.m-sort-selector--custom-type-dropdown ul > li:nth-child(1)').click();
cy.get(".m-sort-selector--custom-type-dropdown ul > li:contains('all_inclusive')")
.click();
cy.url().should('not.include', '=images');
});
......@@ -108,21 +106,26 @@ context('Discovery', () => {
cy.visit('/newsfeed/global/top?type=images');
cy.get('m-topbar--navigation--options').click();
cy.get('m-topbar--navigation--options label > span').click();
cy.get('m-topbar--navigation--options ul > m-nsfw-selector ul > li:nth-child(1)').click();
cy.get('m-topbar--navigation--options ul > m-nsfw-selector ul > li:nth-child(2)').click();
cy.get('m-topbar--navigation--options ul > m-nsfw-selector ul > li:nth-child(3)').click();
cy.get('m-topbar--navigation--options ul > m-nsfw-selector ul > li:nth-child(4)').click();
cy.get('m-topbar--navigation--options ul > m-nsfw-selector ul > li:nth-child(5)').click();
cy.get('m-topbar--navigation--options ul > m-nsfw-selector ul > li:nth-child(6)').click();
cy.get("m-topbar--navigation--options ul > m-nsfw-selector ul > li:contains('Nudity')").click();
cy.get("m-topbar--navigation--options ul > m-nsfw-selector ul > li:contains('Pornography')").click();
cy.get("m-topbar--navigation--options ul > m-nsfw-selector ul > li:contains('Profanity')").click();
cy.get("m-topbar--navigation--options ul > m-nsfw-selector ul > li:contains('Violence and Gore')").click();
cy.get("m-topbar--navigation--options ul > m-nsfw-selector ul > li:contains('Race and Religion')").click();
cy.get("m-topbar--navigation--options ul > m-nsfw-selector ul > li:contains('Other')").click();
});
it('should allow the user to filter by a single hashtag', () => {
cy.get('.m-hashtagsSidebarSelector__list > ul > li:nth-child(1) .m-hashtagsSidebarSelectorList__visibility > i')
.click(); // Will fail on non-configured users
cy.visit('/newsfeed/global/top');
cy.get('m-hashtagssidebarselector__item')
.first()
.click();
});
it('should allow the user to turn off single hashtag filter and view all posts', () => {
cy.get('.m-hashtagsSidebarSelector__list > ul > li:nth-child(1) .m-hashtagsSidebarSelectorList__visibility > i')
cy.visit('/newsfeed/global/top');
cy.get('m-hashtagssidebarselector__item')
.first()
.find('.m-hashtagsSidebarSelectorList__visibility > i')
.click();
})
})
\ No newline at end of file
})
context('Groups', () => {
beforeEach(() => {
cy.login(true);
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
});
cy.location('pathname', { timeout: 30000 })
.should('eq', `/newsfeed/subscriptions`);
})
beforeEach(()=> {
cy.preserveCookies();
});
it('should create and edit a group', () => {
cy.server();
cy.route("POST", "**/api/v1/groups/group*").as("postGroup");
cy.get('m-group--sidebar-markers li:first-child').contains('New group').click();
cy.location('pathname').should('eq', '/groups/create');
......@@ -31,7 +40,10 @@ context('Groups', () => {
cy.get('.m-groups-save > button').contains('Create').click();
cy.wait(1000);
cy.wait('@postGroup').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
});
cy.get('.m-groupInfo__name').contains('test');
cy.get('.m-groupInfo__description').contains('This is a test');
......@@ -58,7 +70,9 @@ context('Groups', () => {
it('should be able to toggle conversation and comment on it', () => {
cy.get('m-group--sidebar-markers li:nth-child(3)').contains('test group').click();
cy.get("m-group--sidebar-markers li:contains('test group')")
.first()
.click();
// toggle the conversation
......@@ -71,8 +85,6 @@ context('Groups', () => {
cy.get('minds-groups-profile-conversation m-comments__tree minds-textarea .m-editor').type('lvl 1 comment');
cy.get('minds-groups-profile-conversation m-comments__tree a.m-post-button').click();
cy.wait(500);
// comment should appear on the list
cy.get('minds-groups-profile-conversation m-comments__tree > m-comments__thread .m-commentBubble__message').contains('lvl 1 comment');
......@@ -82,9 +94,9 @@ context('Groups', () => {
})
it('should post an activity inside the group and record the view when scrolling', () => {
cy.get('m-group--sidebar-markers li:nth-child(3)').contains('test group').click();
cy.wait(1000);
cy.get("m-group--sidebar-markers li:contains('test group')")
.first()
.click();
cy.server();
cy.route("POST", "**/api/v2/analytics/views/activity/*").as("view");
......@@ -93,8 +105,6 @@ context('Groups', () => {
cy.get('.m-posterActionBar__PostButton').click();
cy.wait(500);
// the activity should show that it was posted in this group
cy.get('.minds-list minds-activity .body a:nth-child(2)').contains('(test group)');
......@@ -105,11 +115,9 @@ context('Groups', () => {
cy.get('.m-posterActionBar__PostButton').click();
cy.wait(200);
cy.scrollTo(0, '20px');
cy.wait('@view', { requestTimeout: 2000 }).then((xhr) => {
cy.wait('@view').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body).to.deep.equal({ status: 'success' });
});
......@@ -118,8 +126,6 @@ context('Groups', () => {
it('should delete a group', () => {
cy.get('m-group--sidebar-markers li:nth-child(3)').contains('test group').click();
cy.wait(1000);
// cleanup
cy.get('minds-groups-settings-button > button').click();
cy.get('minds-groups-settings-button ul.minds-dropdown-menu > li:nth-child(8)').contains('Delete Group').click();
......
context('Login', () => {
beforeEach(() => {
cy.clearCookies();
cy.visit('/')
})
......@@ -17,7 +18,7 @@ context('Login', () => {
cy.get('minds-form-login .m-btn--login').click();
cy.location('pathname', { timeout: 10000 })
cy.location('pathname')
.should('eq', '/newsfeed/subscriptions');
})
......
context('Newsfeed', () => {
beforeEach(() => {
cy.login(true);
cy.location('pathname', { timeout: 30000 })
.should('eq', '/newsfeed/subscriptions');
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
})
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 picking hashtags from the dropdown', () => {
cy.get('minds-newsfeed-poster').should('be.visible');
......@@ -20,14 +29,18 @@ context('Newsfeed', () => {
// type in another hashtag manually
cy.get('minds-newsfeed-poster m-hashtags-selector m-form-tags-input input').type('hashtag{enter}').click();
// click away
cy.get('minds-newsfeed-poster m-hashtags-selector .minds-bg-overlay').click();
// click away on arbitrary area.
cy.get('minds-newsfeed-poster m-hashtags-selector .minds-bg-overlay').click({force: true});
// define request
cy.get('.m-posterActionBar__PostButton').click();
//await response
cy.wait('@newsfeedPOST').then((xhr) => {
expect(xhr.status).to.equal(200);
});
cy.wait(100);
cy.get('.minds-list > minds-activity:first-child .message').contains('This is a post #art #hashtag');
cy.get('.mdl-card__supporting-text.message.m-mature-message > span').first().contains('This is a post #art #hashtag');
cy.get('.minds-list > minds-activity:first-child .message a:first-child').contains('#art').should('have.attr', 'href', '/newsfeed/global/top;hashtag=art;period=24h');
cy.get('.minds-list > minds-activity:first-child .message a:last-child').contains('#hashtag').should('have.attr', 'href', '/newsfeed/global/top;hashtag=hashtag;period=24h');
......@@ -44,13 +57,18 @@ context('Newsfeed', () => {
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');
cy.wait(1000);
cy.wait('@mediaPOST').then((xhr) => {
expect(xhr.status).to.equal(200);
});
cy.get('.m-posterActionBar__PostButton').click();
cy.wait(300);
//await response
cy.wait('@newsfeedPOST').then((xhr) => {
expect(xhr.status).to.equal(200);
});
cy.get('.minds-list > minds-activity:first-child .message').contains('This is a post with an image');
// assert image
......@@ -77,7 +95,10 @@ context('Newsfeed', () => {
cy.get('.m-posterActionBar__PostButton').click();
cy.wait(100);
//await response
cy.wait('@newsfeedPOST').then((xhr) => {
expect(xhr.status).to.equal(200);
});
// should have the mature text toggle
cy.get('.minds-list > minds-activity:first-child .message .m-mature-text-toggle').should('not.have.class', 'mdl-color-text--red-500');
......@@ -139,6 +160,7 @@ context('Newsfeed', () => {
})
it('should have a "Buy Tokens" button and it should redirect to /token', () => {
cy.visit('/');
cy.get('.m-page--sidebar--navigation a.m-page--sidebar--navigation--item:last-child span')
.contains('Buy Tokens');
......@@ -149,6 +171,8 @@ context('Newsfeed', () => {
})
it('"create blog" button in poster should redirect to /blog/edit/new', () => {
cy.visit('/');
cy.get('minds-newsfeed-poster .m-posterActionBar__CreateBlog')
.contains('Create blog')
.click();
......@@ -157,6 +181,8 @@ context('Newsfeed', () => {
})
it('clicking on "create blog" button in poster should prompt a confirm dialog and open a new blog with the currently inputted text', () => {
cy.visit('/');
cy.get('minds-newsfeed-poster textarea').type('thegreatmigration'); // TODO: fix UX issue when hashtag element is overlapping input
const stub = cy.stub();
......@@ -173,6 +199,8 @@ context('Newsfeed', () => {
})
it('should record a view when the user scrolls and an activity is visible', () => {
cy.visit('/');
cy.server();
cy.route("POST", "**/api/v2/analytics/views/activity/*").as("view");
// create the post
......@@ -180,13 +208,14 @@ context('Newsfeed', () => {
cy.get('.m-posterActionBar__PostButton').click();
cy.wait(200);
//await response
cy.wait('@newsfeedPOST').then((xhr) => {
expect(xhr.status).to.equal(200);
});
cy.scrollTo(0, '20px');
cy.wait(600);
cy.wait('@view', { requestTimeout: 2000 }).then((xhr) => {
cy.wait('@view').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body).to.deep.equal({ status: 'success' });
});
......
/**
* @author Ben Hayward
* @desc Spec tests for comment threads.
*/
import generateRandomId from '../support/utilities';
context('Notification', () => {
//secondary user for testing.
let username = '';
let password = '';
const commentText = 'test comment';
const postText = 'test comment'
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';
/**
* 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();
cy.login();
cy.post(postText);
cy.clearCookies();
});
/**
* After all log into new user and delete user.
*/
after(() => {
cy.clearCookies();
cy.login(true, username, password);
cy.deleteUser(username, password);
});
/**
* Before each test login, and visit env users channel.
* When testing, this means you will be ready to make a comment, remind etc,
* then switch users and check for the notification.
*/
beforeEach(() => {
cy.clearCookies();
cy.login(false, username, password);
cy.location('pathname')
.should('eq', '/newsfeed/subscriptions');
cy.visit(`/${Cypress.env().username}`);
});
it('should alert the user that a post has been commented on', () => {
// Comment on generated 2nd users post.
cy.get(commentButton).first().click();
cy.get(commentInput).first().type(commentText);
cy.get(postCommentButton).first().click();
cy.get(commentContent).first().contains(commentText);
// Logout, log into generated user.
cy.logout();
cy.login();
// Open their notifications
cy.get(notificationBell).click();
/**
* Notifications not working on test env.
* TODO: Check for notification - follow it
* through and check it leads to the post with postText.
*/
});
})
......@@ -24,6 +24,7 @@ context('Onboarding', () => {
const getTopic = (i) => `m-onboarding--topics > div > ul > li:nth-child(${i}) span`;
before(() => {
cy.clearCookies();
cy.visit('/login');
//type values
......
......@@ -2,19 +2,23 @@ context('Remind', () => {
const remindText = 'remind test text';
beforeEach(() => {
cy.login(true);
cy.location('pathname', { timeout: 30000 })
.should('eq', `/newsfeed/subscriptions`);
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.visit(`/${Cypress.env().username}`);
});
//nav to channel
cy.get('.m-v2-topbar__Top minds-avatar div')
.click();
beforeEach(() => {
cy.preserveCookies();
});
it('should allow a user to remind their post', () => {
cy.server();
cy.route("POST", "**/api/v2/newsfeed/remind/*").as("postRemind");
//post
cy.post("test!!");
......@@ -30,54 +34,10 @@ context('Remind', () => {
//post remind.
cy.get('.m-modal-remind-composer-send i')
.click();
softReload();
cy.wait(1000);
//expect to contain text
cy.get('m-newsfeed__entity:nth-child(3) span')
.contains(remindText);
})
it('should allow a user to delete their remind', () => {
// make sure top post has the reminded text.
cy.get('m-newsfeed__entity:nth-child(3) .m-activity--message-remind span')
.contains(remindText);
//open menu.
cy.get('m-newsfeed__entity:nth-child(3) m-post-menu > button > i')
.click();
//select delete.
cy.get('m-newsfeed__entity:nth-child(3) m-post-menu ul li:nth-child(4)')
.click();
//delete confirm.
cy.get('m-newsfeed__entity:nth-child(3) m-modal-confirm div:nth-child(1) button.mdl-button.mdl-color-text--white.mdl-button--colored.mdl-button--raised')
.click();
cy.wait(2000);
//check the post is gone.
cy.get('m-newsfeed__entity:nth-child(3) .m-activity--message-remind span')
.should('not.have.value', remindText)
cy.wait('@postRemind').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
});
/**
* Cycles by pressing home screen then back to channel
*/
function softReload() {
cy.wait(6000); //wait to let requests finish.
cy.get('.m-v2-topbarNavItem__Logo > img')
.click();
cy.get('.m-v2-topbar__Top minds-avatar div')
.click();
cy.wait(1000); //wait to let requests finish.
}
})
});
context('Topbar', () => {
beforeEach(() => {
cy.login(true);
cy.location('pathname', { timeout: 30000 })
.should('eq', '/newsfeed/subscriptions');
})
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
});
beforeEach(()=> {
cy.preserveCookies();
});
it("clicking on the dropdown on the right should allow to go to the user's channel", () => {
// open the menu
......@@ -15,7 +21,7 @@ context('Topbar', () => {
.click();
cy.location('pathname').should('eq', `/${Cypress.env().username}`);
})
});
it('clicking on the dropdown on the right should allow to go to settings', () => {
// open the menu
......@@ -26,41 +32,37 @@ context('Topbar', () => {
.click();
cy.location('pathname').should('eq', '/settings/general');
})
});
it('clicking on the dropdown on the right should allow to go to the boost console', () => {
// open the menu
cy.get('m-user-menu .m-user-menu__Anchor').click();
cy.get('m-user-menu .m-user-menu__Dropdown li')
.contains('Boost Console')
cy.get("m-user-menu .m-user-menu__Dropdown li:contains('Boost Console')")
.click();
// TOFIX: no boost redirects to create
// cy.location('pathname').should('eq', '/boost/console/newsfeed/history');
})
cy.location('pathname').should('contain', '/boost/console/newsfeed/');
});
it('clicking on the dropdown on the right should allow to go to the boost console', () => {
it('clicking on the dropdown on the right should allow to go to the help desk', () => {
// open the menu
cy.get('m-user-menu .m-user-menu__Anchor').click();
cy.get('m-user-menu .m-user-menu__Dropdown li')
.contains('Help Desk')
cy.get("m-user-menu .m-user-menu__Dropdown li:contains('Help Desk')")
.click();
cy.location('pathname').should('eq', '/help');
})
});
it('clicking on the dropdown on the right should redirect to /canary', () => {
// open the menu
cy.get('m-user-menu .m-user-menu__Anchor').click();
cy.get('m-user-menu .m-user-menu__Dropdown li')
.contains('Canary')
cy.get("m-user-menu .m-user-menu__Dropdown li:contains('Canary')")
.click();
cy.location('pathname').should('eq', '/canary');
})
});
it('clicking on the dropdown on the right should allow to toggle Dark Mode', () => {
// open the menu
......@@ -68,8 +70,7 @@ context('Topbar', () => {
cy.get('body.m-theme__light').should('be.visible');
cy.get('m-user-menu .m-user-menu__Dropdown li')
.contains('Dark Mode')
cy.get("m-user-menu .m-user-menu__Dropdown li:contains('Dark Mode')")
.click();
cy.get('body.m-theme__dark').should('be.visible');
......@@ -79,7 +80,9 @@ context('Topbar', () => {
.click();
cy.get('body.m-theme__light').should('be.visible');
})
cy.get('m-user-menu .m-user-menu__Anchor').click({ force: true });
});
it('clicking on the bulb on the topbar should redirect to /newsfeed/subscriptions', () => {
cy.get('.m-v2-topbarNavItem__Logo img').should('be.visible');
......@@ -87,7 +90,7 @@ context('Topbar', () => {
cy.get('.m-v2-topbarNavItem__Logo').click();
cy.location('pathname').should('eq', '/newsfeed/subscriptions');
})
});
it('clicking on the bell should open the notifications dropdown, and allow to view all notifications by redirecting to /notifications', () => {
cy.get('.m-v2-topbar__UserMenu m-notifications--flyout').should('not.be.visible');
......@@ -102,5 +105,5 @@ context('Topbar', () => {
.click();
cy.location('pathname').should('eq', '/notifications');
})
});
})
......@@ -10,12 +10,21 @@ context('Wire', () => {
const sendButton = '.m-wire--creator-section--last > div > button';
const modal = 'm-overlay-modal > div.m-overlay-modal';
beforeEach(() => {
cy.login();
cy.wait(2000);
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
});
beforeEach(()=> {
cy.preserveCookies();
});
it('should allow a user to send a wire to another user', () => {
//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');
......
// ***********************************************
// 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) => { ... })
// Staging requires cookie to be set
Cypress.Cookies.defaults({
whitelist: 'staging'
});
import 'cypress-file-upload';
/**
* @author Marcelo, Ben and Brian
* @create date 2019-08-09 22:54:02
* @modify date 2019-08-09 22:54:02
* @desc Custom commands for access through cy.[cmd]();
*
* 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) => { ... })
*/
const onboarding = {
welcomeText: 'Welcome to Minds',
welcomeTextContainer: 'm-onboarding--topics > div > h2:nth-child(1)',
nextButton: '.m-channelOnboarding__next',
};
//Login and register
const registerForm = {
username: 'minds-form-register #username',
email: 'minds-form-register #email',
password: 'minds-form-register #password',
password2: 'minds-form-register #password2',
checkbox: 'minds-form-register label:nth-child(2) .mdl-ripple--center',
submitButton: 'minds-form-register .mdl-card__actions button',
};
const settings = {
deleteAccountButton: 'm-settings--disable-channel > div:nth-child(2) > div > button',
deleteSubmitButton: 'm-confirm-password--modal > div > form > div:nth-child(2) > button',
};
const nav = {
hamburgerMenu: '.m-v2-topbar__UserMenu > m-user-menu > div.m-user-menu.m-dropdown > a',
logoutButton: '.m-user-menu.m-dropdown > ul > li:nth-child(11) > a',
byIndex: (i) => `.m-user-menu.m-dropdown > ul > li:nth-child(${i}) > a`,
};
const defaults = {
email: 'test@minds.com',
}
const loginForm = {
password: 'minds-form-login .m-login-box .mdl-cell:last-child input',
username: 'minds-form-login .m-login-box .mdl-cell:first-child input',
submit: 'minds-form-login .m-btn--login',
}
const poster = {
textArea: 'm-text-input--autocomplete-container textarea',
postButton: '.m-posterActionBar__PostButton',
}
Cypress.Commands.add('login', (canary) => {
/**
* Logs a user in.
* @param { boolean } canary - Currently not required
* @param { string } username - The username.
* @param { string } password - The users password.
* @returns void
*/
Cypress.Commands.add('login', (canary = false, username, password) => {
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.get('.m-btn--login').click();
cy.server();
cy.route("POST", "/api/v1/authenticate").as("postLogin");
cy.get('minds-form-login .m-login-box .mdl-cell:first-child input').type(Cypress.env().username);
cy.get('minds-form-login .m-login-box .mdl-cell:last-child input').type(Cypress.env().password);
cy.get(loginForm.username).type(username);
cy.get(loginForm.password).type(password);
cy.get(loginForm.submit).click();
cy.get('minds-form-login .m-btn--login').click();
cy.wait('@postLogin').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
});
});
/**
* Logs a user out of their session using the menu.
* @returns void
*/
Cypress.Commands.add('logout', () => {
cy.get(nav.hamburgerMenu).click();
cy.get(nav.logoutButton).click();
});
/**
* Register a user, be sure to delete the user following this.
*
* ! LOG-OUT PRIOR TO CALLING !
*
* @param { string } username - The username. Note that the requested username will NOT be freed up upon deletion
* @param { string } password - The users password.
* @returns void
*/
Cypress.Commands.add('newUser', (username = '', password = '') => {
cy.visit('/login');
cy.location('pathname', { timeout: 30000 })
.should('eq', `/login`);
cy.server();
cy.route("POST", '**/api/v1/register').as('registerPOST');
cy.get(registerForm.username).focus().type(username);
cy.get(registerForm.email).focus().type(defaults.email);
cy.get(registerForm.password).focus().type(password);
cy.wait(500); // give second password field chance to appear - not tied to a request.
cy.get(registerForm.password2).focus().type(password);
cy.get(registerForm.checkbox).click({force: true});
//submit.
cy.get(registerForm.submitButton).click({force: true})
.wait('@registerPOST').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
//onboarding modal shown.
cy.get(onboarding.welcomeTextContainer)
.contains(onboarding.welcomeText);
//skip onboarding.
cy.get(onboarding.nextButton).click()
cy.get(onboarding.nextButton).click()
cy.get(onboarding.nextButton).click()
cy.get(onboarding.nextButton).click()
});
Cypress.Commands.add('preserveCookies', () => {
Cypress.Cookies.preserveOnce('staging', 'minds_sess', 'mwa', 'XSRF-TOKEN');
});
/**
* Deletes a user. Use carefully on sandbox or you may lose your favorite test account.
*
* ! LOG-IN PRIOR TO CALLING !
*
* @param { string } username - The username. TODO: when both params provided log the user in too
* @param { string } password - The password.
* @returns void
*/
Cypress.Commands.add('deleteUser', (username, password) => {
cy.server();
cy.route("POST", '**/api/v2/settings/password/validate').as('validatePost');
cy.route("POST", '**/api/v2/settings/delete').as('deletePOST');
cy.visit('/settings/disable');
cy.location('pathname', { timeout: 30000 })
.should('eq', `/settings/disable`);
cy.get(settings.deleteAccountButton).click({ force: true });
cy.get('#password').focus().type(password);
cy.get(settings.deleteSubmitButton).click({ force: true })
.wait('@validatePost').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
})
.wait('@deletePOST').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
});
/**
* Uploads a file.
* @param { string } selector - The selector.
* @param { string } fileName - the file-name.
* @param { string } type - the file-type.
* @returns void
*/
Cypress.Commands.add('uploadFile', (selector, fileName, type = '') => {
cy.get(selector).then((subject) => {
cy.fixture(fileName, 'base64').then((content) => {
const el = subject[0];
const blob = b64toBlob(content, type);
cy.window().then((win) => {
const testFile = new win.File([blob], fileName, { type });
const dataTransfer = new DataTransfer();
dataTransfer.items.add(testFile);
el.files = dataTransfer.files;
// return cy.wrap(subject).trigger('change', {force: true});
});
cy.fixture(fileName).then((content) => {
cy.log("Content", fileName);
cy.get(selector).upload({
fileContent: content,
fileName: fileName,
mimeType: type
});
});
// cy.get(selector).trigger('change', { force: true });
});
/**
* Creates a new post. Must be logged in.
* @param { string } message - The message to be posted
* @returns void
*/
Cypress.Commands.add('post', (message) => {
cy.get('m-text-input--autocomplete-container textarea').type(message);
cy.get('.m-posterActionBar__PostButton').click();
cy.server();
cy.route("POST", '**/v1/newsfeed**').as('postActivity');
cy.get(poster.textArea).type(message);
cy.get(poster.postButton).click();
cy.wait('@postActivity').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
});
/**
* Converts base64 to blob format
* @param { string } b64Data - The base64 data.
* @param { string } contentType - The type of content.
* @param { number } sliceSize - The size of the slice.
* @returns void
*/
function b64toBlob(b64Data, contentType, sliceSize = 512) {
const byteCharacters = atob(b64Data);
const byteArrays = [];
......
......@@ -18,3 +18,5 @@ import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
Cypress.Cookies.debug(true);
/**
* @author Ben Hayward
* @create date 2019-08-10 00:38:46
* @modify date 2019-08-10 00:38:46
* @desc Space to put utilities and helper functions without cluttering up commands.js
*/
/**
* @returns a random 21 character string
*/
const generateRandomId = () => {
return Math.random().toString(36).substring(2, 15)
+ Math.random().toString(36).substring(2, 15);
}
export default generateRandomId;
This diff is collapsed.
......@@ -5,7 +5,7 @@
"scripts": {
"ng": "ng",
"start": "ng serve",
"preinstall": "git config core.hooksPath .githooks",
"preinstall": "git config core.hooksPath .git/hooks/",
"prebuild": "gulp build.sass",
"build": "ng build --prod",
"prebuild-dev": "gulp build.sass --deploy-url=http://localhost/en",
......@@ -62,12 +62,14 @@
"@types/node": "~10.12.18",
"codelyzer": "^4.5.0",
"cypress": "^3.4.1",
"cypress-file-upload": "^3.3.3",
"gulp": "~4.0.0",
"gulp-autoprefixer": "^6.0.0",
"gulp-css-globbing": "^0.2.2",
"gulp-minify-css": "^1.1.6",
"gulp-sass": "^4.0.2",
"gulp-template": "^5.0.0",
"husky": "^3.0.4",
"jasmine-core": "~2.99.0",
"jasmine-spec-reporter": "~4.2.1",
"jasmine-ts-async": "^1.0.0",
......@@ -78,9 +80,15 @@
"karma-jasmine-html-reporter": "^0.2.2",
"karma-mocha-reporter": "^2.2.5",
"prettier": "1.18.2",
"pretty-quick": "^1.11.1",
"protractor": "~5.4.2",
"ts-node": "~7.0.0",
"tslint": "~5.12.0",
"typescript": "~3.4.5"
},
"husky": {
"hooks": {
"pre-commit": ".githooks/pre-commit && pretty-quick --staged --bail --pattern '**/*.*(ts|html|scss)'"
}
}
}
......@@ -102,6 +102,7 @@ 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 { ChannelModeSelectorComponent } from './components/channel-mode-selector/channel-mode-selector.component';
import { ShareModalComponent } from '../modules/modals/share/share';
import { DraggableListComponent } from './components/draggable-list/list.component';
import { DndModule } from 'ngx-drag-drop';
......@@ -195,6 +196,7 @@ import { DndModule } from 'ngx-drag-drop';
DynamicFormComponent,
AndroidAppDownloadComponent,
SortSelectorComponent,
ChannelModeSelectorComponent,
NSFWSelectorComponent,
SwitchComponent,
......@@ -285,6 +287,7 @@ import { DndModule } from 'ngx-drag-drop';
SwitchComponent,
NSFWSelectorComponent,
FeaturedContentComponent,
ChannelModeSelectorComponent,
DraggableListComponent,
],
providers: [
......
<m-dropdown #channelModeDropdown class="m-channel-mode-selector--dropdown">
<label *ngIf="user.mode === channelModes.PUBLIC">
<i class="material-icons m-dropdown--label-icon">public</i>
<span>Public</span>
<i class="material-icons m-dropdown--label-icon" *ngIf="enabled"
>keyboard_arrow_down</i
>
</label>
<label *ngIf="user.mode === channelModes.MODERATED">
<i class="material-icons m-dropdown--label-icon">person_add</i>
<span>Moderated</span>
<i class="material-icons m-dropdown--label-icon" *ngIf="enabled"
>keyboard_arrow_down</i
>
</label>
<label *ngIf="user.mode === channelModes.CLOSED">
<i class="material-icons m-dropdown--label-icon">lock</i>
<span>Closed</span>
<i class="material-icons m-dropdown--label-icon" *ngIf="enabled"
>keyboard_arrow_down</i
>
</label>
<ul class="m-dropdown--list">
<li
class="m-dropdown--list--item"
[class.m-dropdown--list--item--selected]="
user.mode === channelModes.PUBLIC
"
(click)="setChannelMode(channelModes.PUBLIC)"
>
<i class="material-icons m-dropdown--label-icon">public</i>
<span>Public</span>
</li>
<li
class="m-dropdown--list--item"
[class.m-dropdown--list--item--selected]="
user.mode === channelModes.MODERATED
"
(click)="setChannelMode(channelModes.MODERATED)"
>
<i class="material-icons m-dropdown--label-icon">person_add</i>
<span>Moderated</span>
</li>
<li
class="m-dropdown--list--item"
[class.m-dropdown--list--item--selected]="
user.mode === channelModes.CLOSED
"
(click)="setChannelMode(channelModes.CLOSED)"
>
<i class="material-icons m-dropdown--label-icon">lock</i>
<span>Closed</span>
</li>
</ul>
</m-dropdown>
m-channel-mode-selector {
padding: 16px 4px;
display: flex;
flex-wrap: wrap;
flex-direction: row;
justify-content: center;
align-items: center;
position: relative;
margin: auto;
.m-dropdown--label-container {
text-align: center;
span {
vertical-align: middle;
}
}
.m-dropdown--label-icon {
margin-right: 4px;
}
.m-sort-selector--item-label-icon {
font-size: 18px;
margin-right: 4px;
}
.m-dropdown--list--item {
cursor: pointer;
}
}
///<reference path="../../../../../node_modules/@types/jasmine/index.d.ts"/>
import {
async,
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import { MockComponent } from '../../../utils/mock';
import { CommonModule as NgCommonModule } from '@angular/common';
import { ChannelModeSelectorComponent } from './channel-mode-selector.component';
import { ChannelMode } from '../../../interfaces/entities';
import { Client } from '../../../services/api/client';
import { clientMock } from '../../../../tests/client-mock.spec';
import { DropdownComponent } from '../dropdown/dropdown.component';
import { componentFactoryName } from '@angular/compiler';
describe('ChannelModeSelector', () => {
let comp: ChannelModeSelectorComponent;
let fixture: ComponentFixture<ChannelModeSelectorComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [DropdownComponent, ChannelModeSelectorComponent],
providers: [{ provide: Client, useValue: clientMock }],
}).compileComponents();
}));
beforeEach(done => {
fixture = TestBed.createComponent(ChannelModeSelectorComponent);
clientMock.response = {};
comp = fixture.componentInstance;
comp.user = {
guid: 'guidguid',
name: 'name',
username: 'username',
icontime: 11111,
subscribers_count: 182,
impressions: 18200,
mode: ChannelMode.PUBLIC,
};
clientMock.response['api/v1/channel/info'] = { status: 'success' };
fixture.detectChanges();
if (fixture.isStable()) {
done();
} else {
fixture.whenStable().then(() => {
done();
});
}
});
it('it should change mode to moderated', () => {
spyOn(comp.channelModeDropdown, 'close');
comp.setChannelMode(ChannelMode.MODERATED);
fixture.detectChanges();
expect(comp.user.mode).toEqual(ChannelMode.MODERATED);
expect(clientMock.post.calls.mostRecent().args[0]).toEqual(
'api/v1/channel/info'
);
expect(clientMock.post.calls.mostRecent().args[1]).toEqual(comp.user);
expect(comp.channelModeDropdown.close).toHaveBeenCalled();
});
it('it should change mode to closed', () => {
spyOn(comp.channelModeDropdown, 'close');
comp.setChannelMode(ChannelMode.CLOSED);
fixture.detectChanges();
expect(comp.user.mode).toEqual(ChannelMode.CLOSED);
expect(clientMock.post.calls.mostRecent().args[0]).toEqual(
'api/v1/channel/info'
);
expect(clientMock.post.calls.mostRecent().args[1]).toEqual(comp.user);
expect(comp.channelModeDropdown.close).toHaveBeenCalled();
});
it('it should change mode to open', () => {
spyOn(comp.channelModeDropdown, 'close');
comp.setChannelMode(ChannelMode.PUBLIC);
fixture.detectChanges();
expect(comp.user.mode).toEqual(ChannelMode.PUBLIC);
expect(clientMock.post.calls.mostRecent().args[0]).toEqual(
'api/v1/channel/info'
);
expect(clientMock.post.calls.mostRecent().args[1]).toEqual(comp.user);
expect(comp.channelModeDropdown.close).toHaveBeenCalled();
});
it('it should not change when disabled', () => {
spyOn(comp.channelModeDropdown, 'close');
clientMock.post.calls.reset();
comp.enabled = false;
fixture.detectChanges();
comp.setChannelMode(ChannelMode.PUBLIC);
fixture.detectChanges();
expect(comp.user.mode).toEqual(ChannelMode.PUBLIC);
expect(clientMock.post).not.toHaveBeenCalled();
expect(comp.channelModeDropdown.close).not.toHaveBeenCalled();
});
});
import { AfterViewInit, Component, Input, ViewChild } from '@angular/core';
import { DropdownComponent } from '../dropdown/dropdown.component';
import { ChannelMode, MindsUser } from '../../../interfaces/entities';
import { Client } from '../../../services/api';
@Component({
selector: 'm-channel-mode-selector',
templateUrl: './channel-mode-selector.component.html',
})
export class ChannelModeSelectorComponent implements AfterViewInit {
@ViewChild('channelModeDropdown', { static: false })
channelModeDropdown: DropdownComponent;
@Input() public enabled = true;
@Input() public user: MindsUser;
public channelModes = ChannelMode;
constructor(public client: Client) {}
/**
* Pass the enabled flag down to the ViewChild to control the dropdown functions
* Only owners can change their channel mode
*/
public ngAfterViewInit() {
this.channelModeDropdown.enabled = this.enabled;
}
/**
* @param mode ChannelMode
* Sets the channel mode on the user object and calls the api
*/
public setChannelMode(mode: ChannelMode) {
if (!this.enabled) {
return;
}
this.user.mode = mode;
this.channelModeDropdown.close();
this.update();
}
/**
* Sends the current user to the update endpoint
*/
async update() {
try {
await this.client.post('api/v1/channel/info', this.user);
} catch (ex) {
console.error(ex);
}
}
}
......@@ -35,11 +35,14 @@ import { Component, Input } from '@angular/core';
})
export class DropdownComponent {
@Input() expanded: boolean = false;
@Input() enabled: boolean = true;
toggled = false;
toggle() {
this.toggled = !this.toggled;
if (this.enabled) {
this.toggled = !this.toggled;
}
}
close() {
......
......@@ -203,22 +203,23 @@ export class ButtonsPlugin {
* Position buttons
*/
public positionButtons(activeAddon) {
let $buttons = this.$element.querySelector('.medium-insert-buttons'),
$p = this.$element.querySelector('.medium-insert-active'),
$lastCaption = $p.classList.contains('medium-insert-images-grid')
? []
: $p.querySelector(
'* .medium-insert-images:last-child .m-blog--image-caption'
),
elementsContainer = (<any>this.base).options.elementsContainer,
elementsContainerAbsolute =
['absolute', 'fixed'].indexOf(
window
.getComputedStyle(elementsContainer)
.getPropertyValue('position')
) > -1;
if ($p) {
let $buttons = this.$element.querySelector('.medium-insert-buttons');
let $p = this.$element.querySelector('.medium-insert-active');
if ($p !== null) {
let $lastCaption = $p.classList.contains('medium-insert-images-grid')
? []
: $p.querySelector(
'* .medium-insert-images:last-child .m-blog--image-caption'
),
elementsContainer = (<any>this.base).options.elementsContainer,
elementsContainerAbsolute =
['absolute', 'fixed'].indexOf(
window
.getComputedStyle(elementsContainer)
.getPropertyValue('position')
) > -1;
const pRect = $p.getBoundingClientRect();
$buttons.style.left = pRect.left + document.body.scrollLeft - 40 + 'px';
......
......@@ -150,6 +150,9 @@ export class EmbedImage {
this.base.checkContentChanged();
}
/**
* Event handler registration.
*/
public events() {
/* prevent default image drag&drop */
this.$element.addEventListener('dragover', e => {
......@@ -227,8 +230,17 @@ export class EmbedImage {
return data;
}
/**
* On image select, responds to image click.
* @param { event } e - event from DOM.
*/
public selectImage(e) {
let $image = e.target;
if (!$image || $image.tagName === null) {
return;
}
if ($image.tagName === 'SPAN') {
$image = $image.parentNode.querySelector('img');
}
......@@ -254,11 +266,15 @@ export class EmbedImage {
event.stopPropagation();
}
/**
* On image deselect, called when image clicked away from.
* @param { event } e - event from DOM.
*/
public unselectImage(e) {
let $el = e.target,
$image = document.querySelector('.medium-insert-image-active');
if (!$image) {
if (!$image || !$el || $el.tagName === null) {
return;
}
......
......@@ -4,8 +4,20 @@
m-phone-input {
position: relative;
display: inline-flex;
margin-bottom: $minds-padding;
@media (max-width: $max-mobile) {
width: 100%;
input {
width: 100%;
}
}
.m-phone-input--wrapper {
display: flex;
@media (min-width: $max-mobile) {
flex-flow: row wrap;
justify-content: flex-end;
}
margin-bottom: $minds-padding;
@include m-theme() {
background-color: themed($m-white);
......
......@@ -41,6 +41,9 @@ export type FeedsServiceGetResponse = {
next?: number;
};
/**
* Enables the grabbing of data through observable feeds.
*/
@Injectable()
export class FeedsService {
limit: BehaviorSubject<number> = new BehaviorSubject(12);
......@@ -98,16 +101,28 @@ export class FeedsService {
);
}
/**
* Sets the endpoint for this instance.
* @param { string } endpoint - the endpoint for this instance. For example `api/v1/entities/owner`.
*/
setEndpoint(endpoint: string): FeedsService {
this.endpoint = endpoint;
return this;
}
/**
* Sets the limit to be returned per next() call.
* @param { number } limit - the limit to retrieve.
*/
setLimit(limit: number): FeedsService {
this.limit.next(limit);
return this;
}
/**
* Sets parameters to be used.
* @param { Object } params - parameters to be used.
*/
setParams(params): FeedsService {
this.params = params;
if (!params.sync) {
......@@ -116,19 +131,31 @@ export class FeedsService {
return this;
}
/**
* Sets the offset of the request
* @param { number } offset - the offset of the request.
*/
setOffset(offset: number): FeedsService {
this.offset.next(offset);
return this;
}
/**
* Sets castToActivities
* @param { boolean } cast - whether or not to set as_activities to true.
*/
setCastToActivities(cast: boolean): FeedsService {
this.castToActivities = cast;
return this;
}
/**
* Fetches the data.
*/
fetch(): FeedsService {
if (!this.offset.getValue()) this.inProgress.next(true);
if (!this.offset.getValue()) {
this.inProgress.next(true);
}
this.client
.get(this.endpoint, {
...this.params,
......@@ -139,8 +166,12 @@ export class FeedsService {
},
})
.then((response: any) => {
if (!this.offset.getValue()) this.inProgress.next(false);
if (!this.offset.getValue()) {
this.inProgress.next(false);
}
if (!response.entities && response.activity) {
response.entities = response.activity;
}
if (response.entities.length) {
this.rawFeed.next(this.rawFeed.getValue().concat(response.entities));
this.pagingToken = response['load-next'];
......@@ -148,10 +179,13 @@ export class FeedsService {
this.canFetchMore = false;
}
})
.catch(err => {});
.catch(e => console.log(e));
return this;
}
/**
* To be called upload loading more data
*/
loadMore(): FeedsService {
if (!this.inProgress.getValue()) {
this.setOffset(this.limit.getValue() + this.offset.getValue());
......@@ -160,6 +194,9 @@ export class FeedsService {
return this;
}
/**
* To clear data.
*/
clear(): FeedsService {
this.offset.next(0);
this.pagingToken = '';
......
......@@ -42,6 +42,12 @@ export interface Tag {
selected?: boolean;
}
export enum ChannelMode {
PUBLIC = 0,
MODERATED = 1,
CLOSED = 2,
}
export interface MindsUser {
guid: string;
name: string;
......@@ -87,6 +93,7 @@ export interface MindsUser {
tile_ratio?: string;
styles?: { [key: string]: string };
};
mode: ChannelMode;
}
export interface MindsGroup {
......
......@@ -8,21 +8,18 @@
<div class="m-boost-console-booster--content">
<!-- Posts -->
<ng-container *ngIf="type == 'newsfeed' || type == 'offers'">
<ng-container *ngIf="posts && posts.length > 0">
<ng-container>
<div class="m-boost-console--booster--posts-list">
<minds-card
[object]="object"
[object]="entity | async"
class="m-border"
hostClass="mdl-card"
*ngFor="let object of posts"
*ngFor="let entity of feed$ | async"
></minds-card>
</div>
</ng-container>
<h3
[hidden]="inProgress || posts.length > 0"
i18n="@@BOOST__CONSOLE__BOOSTER__POST_SOMETHING"
>
<h3 [hidden]="noContent" i18n="@@BOOST__CONSOLE__BOOSTER__POST_SOMETHING">
You have no content yet. Why don't you post something?
</h3>
<div
......@@ -30,7 +27,7 @@
class="mdl-spinner mdl-js-spinner is-active"
[mdl]
></div>
<div #poster [hidden]="!inProgress && posts.length > 0"></div>
<div #poster [hidden]="!inProgress && noContent"></div>
</ng-container>
<!-- User and Media -->
......@@ -41,23 +38,26 @@
hostClass="mdl-shadow--2dp"
></minds-card>
<ng-container *ngIf="media.length > 0">
<ng-container *ngIf="(feed$ | async)?.length != 0">
<h3 i18n="@@BOOST__CONSOLE__BOOSTER__YOUR_RECENT_MEDIA_TITLE">
Your recent media
</h3>
<div class="mdl-grid m-boost-console-booster--content-grid">
<div class="mdl-cell mdl-cell--6-col" *ngFor="let object of media">
<div
class="mdl-cell mdl-cell--6-col"
*ngFor="let entity of feed$ | async"
>
<minds-card
[object]="object"
[object]="entity | async"
hostClass="mdl-shadow--2dp"
></minds-card>
<minds-button type="boost" [object]="object"></minds-button>
<minds-button type="boost" [object]="entity | async"></minds-button>
</div>
</div>
</ng-container>
<h3
[hidden]="inProgress || media.length > 0"
[hidden]="!inProgress && noContent"
i18n="@@BOOST__CONSOLE__BOOSTER__POST_SOMETHING"
>
You have no content yet. Why don't you post something?
......@@ -67,6 +67,13 @@
class="mdl-spinner mdl-js-spinner is-active"
[mdl]
></div>
<div #poster [hidden]="inProgress && media.length > 0"></div>
<div #poster [hidden]="!inProgress && noContent"></div>
</ng-container>
<infinite-scroll
distance="25%"
(load)="loadNext()"
[moreData]="feedsService.hasMore | async"
[inProgress]="inProgress"
></infinite-scroll>
</div>
......@@ -11,10 +11,14 @@ import { Client } from '../../../../services/api';
import { Session } from '../../../../services/session';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs/internal/observable/of';
import { FeedsService } from '../../../../common/services/feeds.service';
import { feedsServiceMock } from '../../../../../tests/feed-service-mock.spec';
import { BehaviorSubject } from 'rxjs';
describe('BoostConsoleBooster', () => {
let comp: BoostConsoleBooster;
let fixture: ComponentFixture<BoostConsoleBooster>;
window.Minds.user = { guid: 123 };
beforeEach(async(() => {
TestBed.configureTestingModule({
......@@ -25,6 +29,11 @@ describe('BoostConsoleBooster', () => {
inputs: ['object', 'hostClass'],
}),
MockComponent({ selector: 'minds-button', inputs: ['object', 'type'] }),
MockDirective({
selector: 'infinite-scroll',
inputs: ['moreData', 'inProgress'],
outputs: ['load'],
}),
BoostConsoleBooster,
],
imports: [RouterTestingModule, ReactiveFormsModule],
......@@ -35,28 +44,15 @@ describe('BoostConsoleBooster', () => {
provide: ActivatedRoute,
useValue: { parent: { url: of([{ path: 'newsfeed' }]) } },
},
{ provide: FeedsService, useValue: feedsServiceMock },
],
}).compileComponents();
}));
beforeEach(done => {
jasmine.MAX_PRETTY_PRINT_DEPTH = 2;
fixture = TestBed.createComponent(BoostConsoleBooster);
comp = fixture.componentInstance;
clientMock.response = {};
clientMock.response['api/v1/newsfeed/personal'] = {
status: 'success',
activity: [{ guid: '123' }, { guid: '456' }],
};
clientMock.response['api/v1/entities/owner'] = {
status: 'success',
entities: [{ guid: '789' }, { guid: '101112' }],
};
fixture.detectChanges();
if (fixture.isStable()) {
......@@ -70,8 +66,7 @@ describe('BoostConsoleBooster', () => {
});
it('should have loaded the lists', () => {
expect(comp.posts).toEqual([{ guid: '123' }, { guid: '456' }]);
expect(comp.media).toEqual([{ guid: '789' }, { guid: '101112' }]);
expect(comp.feed$).not.toBeFalsy();
});
it('should have a title', () => {
......@@ -89,13 +84,14 @@ describe('BoostConsoleBooster', () => {
By.css('.m-boost-console--booster--posts-list')
);
expect(list).not.toBeNull();
expect(list.nativeElement.children.length).toBe(2);
expect(list.nativeElement.children.length).toBe(1);
});
it("should have a poster if the user hasn't posted anything yet", () => {
comp.feed$ = of([]);
fixture.detectChanges();
comp.posts = [];
fixture.detectChanges();
comp.feed$.subscribe(feed => expect(feed.length).toBe(0));
const title = fixture.debugElement.query(
By.css('.m-boost-console-booster--content h3')
......@@ -106,9 +102,31 @@ describe('BoostConsoleBooster', () => {
);
const poster = fixture.debugElement.query(
By.css('.m-boost-console-booster--content div:last-child')
By.css('.m-boost-console-booster--content > div:nth-child(3)')
);
expect(poster).not.toBeNull();
expect(poster.nativeElement.hidden).toBeFalsy();
});
it('should not have a poster if the user has posted content', () => {
comp.feed$ = of([
BehaviorSubject.create({ id: 1 }),
BehaviorSubject.create({ id: 2 }),
]);
fixture.detectChanges();
comp.feed$.subscribe(feed => expect(feed.length).toBe(2));
const title = fixture.debugElement.query(
By.css('.m-boost-console-booster--content h3')
);
expect(title).toBeDefined();
expect(title.nativeElement.textContent).toContain(
"You have no content yet. Why don't you post something?"
);
const poster = fixture.debugElement.query(
By.css('.m-boost-console-booster--content > div:nth-child(3)')
);
expect(poster).toBeDefined();
});
});
import {
Component,
ComponentFactoryResolver,
ViewRef,
ChangeDetectorRef,
Input,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FeedsService } from '../../../../common/services/feeds.service';
import { BoostConsoleType } from '../console.component';
import { Client } from '../../../../services/api';
import { Session } from '../../../../services/session';
import { BehaviorSubject, Observable } from 'rxjs';
import { PosterComponent } from '../../../newsfeed/poster/poster.component';
/**
* The component for the boost console.
*/
@Component({
moduleId: module.id,
selector: 'm-boost-console-booster',
templateUrl: 'booster.component.html',
})
export class BoostConsoleBooster {
inProgress: boolean = false;
loaded: boolean = false;
posts: any[] = [];
media: any[] = [];
/* type of the feed to display */
@Input('type') type: BoostConsoleType;
componentRef;
componentInstance: PosterComponent;
/* poster component */
@ViewChild('poster', { read: ViewContainerRef, static: false })
poster: ViewContainerRef;
minds: Minds = window.Minds;
feed$: Observable<BehaviorSubject<Object>[]>;
componentRef;
componentInstance: PosterComponent;
inProgress = true;
loaded = false;
noContent = true;
constructor(
public client: Client,
public session: Session,
private route: ActivatedRoute,
private _componentFactoryResolver: ComponentFactoryResolver
public feedsService: FeedsService,
private cd: ChangeDetectorRef,
private componentFactoryResolver: ComponentFactoryResolver
) {}
/**
* subscribes to route parent url and loads component.
*/
ngOnInit() {
this.loaded = false;
this.route.parent.url.subscribe(segments => {
this.type = <BoostConsoleType>segments[0].path;
this.load();
this.load(true);
this.loaded = true;
this.loadPoster();
});
}
/**
* Loads the infinite feed for the respective parent route.
* @param { boolean } refresh - is the state refreshing?
*/
load(refresh?: boolean) {
if (this.inProgress) {
return Promise.resolve(false);
if (!refresh) {
return;
}
if (!refresh && this.loaded) {
return Promise.resolve(true);
if (refresh) {
this.feedsService.clear();
}
this.inProgress = true;
let promises = [
this.client.get('api/v1/newsfeed/personal'),
this.client.get('api/v1/entities/owner'),
];
return Promise.all(promises)
.then((responses: any[]) => {
this.loaded = true;
this.inProgress = false;
this.posts = responses[0].activity || [];
this.media = responses[1].entities || [];
// this.posts = [];
// this.media = [];
this.loadComponent();
})
.catch(e => {
this.inProgress = false;
return false;
});
this.feedsService
.setEndpoint(
this.type === 'content'
? `api/v2/feeds/container/${this.minds.user.guid}/all`
: `api/v2/feeds/container/${this.minds.user.guid}/activities`
)
.setParams({ sync: true })
.setLimit(12)
.fetch();
this.feed$ = this.feedsService.feed;
this.inProgress = false;
this.loaded = true;
this.feed$.subscribe(feed => (this.noContent = feed.length > 0));
}
loadComponent() {
this.poster.clear();
/**
* Loads next data in feed.
* @param feed - the feed to reload.
*/
loadNext() {
if (
((this.type === 'offers' || this.type === 'newsfeed') &&
this.posts.length === 0) ||
(this.type === 'content' && this.media.length === 0)
this.feedsService.canFetchMore &&
!this.feedsService.inProgress.getValue() &&
this.feedsService.offset.getValue()
) {
const componentFactory = this._componentFactoryResolver.resolveComponentFactory(
this.feedsService.fetch(); // load the next 150 in the background
}
this.feedsService.loadMore();
}
/**
* Detects changes if view is not destroyed.
*/
detectChanges() {
if (!(this.cd as ViewRef).destroyed) {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
/**
* Detaches change detector on destroy
*/
ngOnDestroy = () => this.cd.detach();
/**
* Loads the poster component if there are no activities loaded.
* @returns {boolean} success.
*/
loadPoster() {
this.feedsService.feed.subscribe(feed => {
if (feed.length > 0 && !this.inProgress && this.loaded) {
try {
this.poster.clear();
this.componentRef.clear();
this.noContent = true;
return false;
} catch (e) {
return false;
}
}
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(
PosterComponent
);
this.componentRef = this.poster.createComponent(componentFactory);
this.componentInstance = this.componentRef.instance;
this.componentInstance.load.subscribe(() => {
this.load();
});
}
return true;
});
}
}
......@@ -36,6 +36,7 @@ import { IfFeatureDirective } from '../../common/directives/if-feature.directive
import { FeaturesService } from '../../services/features.service';
import { featuresServiceMock } from '../../../tests/features-service-mock.spec';
import { BlockListService } from '../../common/services/block-list.service';
import { ChannelMode } from '../../interfaces/entities';
describe('ChannelComponent', () => {
let comp: ChannelComponent;
......@@ -127,6 +128,7 @@ describe('ChannelComponent', () => {
icontime: 11111,
subscribers_count: 182,
impressions: 18200,
mode: ChannelMode.PUBLIC,
};
comp.editing = false;
fixture.detectChanges();
......
......@@ -31,6 +31,7 @@ import { ScrollService } from '../../../services/ux/scroll';
import { FeaturesService } from '../../../services/features.service';
import { featuresServiceMock } from '../../../../tests/features-service-mock.spec';
import { FeedsService } from '../../../common/services/feeds.service';
import { ChannelMode } from '../../../interfaces/entities';
describe('ChannelFeed', () => {
let comp: ChannelFeedComponent;
......@@ -89,6 +90,7 @@ describe('ChannelFeed', () => {
subscribers_count: 182,
impressions: 18200,
pinned_posts: ['a', 'b', 'c'],
mode: ChannelMode.PUBLIC,
};
comp.feed = [
{ guid: 'aaaa' },
......
......@@ -5,14 +5,31 @@
(added)="upload_avatar($event)"
></minds-avatar>
<div class="m-channel--name">
<h2>{{user.name}}</h2>
<h2 [hidden]="editing">{{user.name}}</h2>
<div
class="minds-editable-container mdl-card__supporting-text m-channel--name--editor"
*ngIf="editing && isOwner()"
>
<input
[autoGrow]
class="mdl-textfield__input"
type="text"
name="briefdescription"
[(ngModel)]="user.name"
/>
</div>
<span
class="minds-button-edit"
(click)="toggleEditing()"
*ngIf="session.getLoggedInUser().guid == user.guid"
>
<button class="material-icons" [hidden]="editing">edit</button>
<button class="material-icons" [hidden]="!editing">done</button>
<button
class="material-icons m-channel-button-edit--done"
[hidden]="!editing"
>
done
</button>
</span>
<minds-button-user-dropdown
[(user)]="user"
......@@ -205,6 +222,16 @@
</div>
</div>
<div
class="m-channel__channel-mode-selector"
*ngIf="featuresService.has('permissions')"
>
<m-channel-mode-selector
[user]="user"
[enabled]="session.isLoggedIn() && session.getLoggedInUser().guid === user.guid"
></m-channel-mode-selector>
</div>
<div class="m-channel--action-buttons">
<minds-button-subscribe
[user]="user"
......
......@@ -4,6 +4,30 @@
border: 0 !important;
}
.m-channel--name--editor {
max-width: 88%;
}
.m-channel-button-edit--done {
position: relative;
right: 8px;
bottom: 8px;
}
.m-channel--name > div > input {
margin: 0;
font-size: 28px;
font-weight: 800;
letter-spacing: 0.25px;
line-height: 32px;
text-rendering: optimizeLegibilty;
-webkit-font-smoothing: antialiased;
text-align: center;
@include m-theme() {
border: 1px solid themed($m-grey-100);
}
}
.m-channel-bio-field {
div > i {
vertical-align: middle;
......
......@@ -35,6 +35,9 @@ import { featuresServiceMock } from '../../../../tests/features-service-mock.spe
import { IfFeatureDirective } from '../../../common/directives/if-feature.directive';
import { overlayModalServiceMock } from '../../../../tests/overlay-modal-service-mock.spec';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { ChannelMode } from '../../../interfaces/entities';
import { ifStmt } from '@angular/compiler/src/output/output_ast';
import { ChannelModulesComponent } from '../modules/modules';
describe('ChannelSidebar', () => {
let comp: ChannelSidebar;
......@@ -99,6 +102,10 @@ describe('ChannelSidebar', () => {
inputs: ['title', 'type', 'channel', 'linksTo', 'size'],
outputs: [],
}),
MockComponent({
selector: 'm-channel-mode-selector',
inputs: ['user', 'enabled'],
}),
IfFeatureDirective,
],
imports: [FormsModule, RouterTestingModule, NgCommonModule],
......@@ -132,6 +139,7 @@ describe('ChannelSidebar', () => {
jasmine.clock().install();
fixture = TestBed.createComponent(ChannelSidebar);
featuresServiceMock.mock('es-feeds', false);
featuresServiceMock.mock('permissions', true);
clientMock.response = {};
uploadMock.response = {};
comp = fixture.componentInstance;
......@@ -143,6 +151,7 @@ describe('ChannelSidebar', () => {
icontime: 11111,
subscribers_count: 182,
impressions: 18200,
mode: ChannelMode.PUBLIC,
};
comp.editing = false;
uploadMock.response[`api/v1/channel/avatar`] = {
......@@ -335,4 +344,9 @@ describe('ChannelSidebar', () => {
fixture.detectChanges();
expect(comp.changeEditing.next).toHaveBeenCalled();
});
it('should set a channel to public', () => {
fixture.detectChanges();
expect(comp.user.mode).toEqual(ChannelMode.PUBLIC);
});
});
import { Component, EventEmitter, Output } from '@angular/core';
import { Component, EventEmitter, Output, ViewChild } from '@angular/core';
import { Client, Upload } from '../../../services/api';
import { Session } from '../../../services/session';
import { MindsUser } from '../../../interfaces/entities';
......@@ -7,6 +7,7 @@ import { ChannelOnboardingService } from '../../onboarding/channel/onboarding.se
import { Storage } from '../../../services/storage';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { ReferralsLinksComponent } from '../../wallet/tokens/referrals/links/links.component';
import { FeaturesService } from '../../../services/features.service';
@Component({
moduleId: module.id,
......@@ -28,7 +29,7 @@ export class ChannelSidebar {
@Output() changeEditing = new EventEmitter<boolean>();
//@todo make a re-usable city selection module to avoid duplication here
// @todo make a re-usable city selection module to avoid duplication here
cities: Array<any> = [];
constructor(
......@@ -37,13 +38,15 @@ export class ChannelSidebar {
public session: Session,
public onboardingService: ChannelOnboardingService,
protected storage: Storage,
private overlayModal: OverlayModalService
private overlayModal: OverlayModalService,
public featuresService: FeaturesService
) {
if (onboardingService && onboardingService.onClose)
if (onboardingService && onboardingService.onClose) {
onboardingService.onClose.subscribe(progress => {
this.onboardingProgress = -1;
this.checkProgress();
});
}
}
ngOnInit() {
......@@ -84,6 +87,7 @@ export class ChannelSidebar {
}
this.changeEditing.next(!this.editing);
this.minds.user.name = this.user.name; //no need to refresh for other pages to update.
}
upload_avatar(file) {
......
......@@ -10,7 +10,7 @@ import { ActivatedRoute } from '@angular/router';
import { WireCreatorComponent } from '../../wire/creator/creator.component';
import { SessionsStorageService } from '../../../services/session-storage.service';
import { SiteService } from '../../../services/site.service';
import { Subscription } from 'rxjs';
import { BehaviorSubject, Subscription } from 'rxjs';
export type RouterLinkToType =
| 'home'
......@@ -33,12 +33,15 @@ export interface NavItems {
export class ProChannelService implements OnDestroy {
currentChannel: MindsUser;
readonly onChannelChange: BehaviorSubject<any> = new BehaviorSubject(null);
protected featuredContent: Array<any> | null;
protected menuNavItems: Array<NavItems> = [];
protected isLoggedIn$: Subscription;
constructor(
protected client: Client,
protected entitiesService: EntitiesService,
......@@ -92,6 +95,7 @@ export class ProChannelService implements OnDestroy {
)) as MindsChannelResponse;
this.currentChannel = response.channel;
this.onChannelChange.next(this.currentChannel);
return this.currentChannel;
}
......
......@@ -31,6 +31,8 @@ export class SubscribeButtonComponent implements OnInit, OnDestroy {
protected loggedIn$: Subscription;
protected channelChange$: Subscription;
constructor(
protected session: Session,
protected router: Router,
......@@ -44,6 +46,10 @@ export class SubscribeButtonComponent implements OnInit, OnDestroy {
ngOnInit() {
this.updateCount();
this.channelChange$ = this.channelService.onChannelChange.subscribe(() => {
this.updateCount();
});
this.loggedIn$ = this.session.loggedinEmitter.subscribe(is => {
if (!is) {
this.subscribed = false;
......@@ -54,6 +60,7 @@ export class SubscribeButtonComponent implements OnInit, OnDestroy {
ngOnDestroy() {
this.loggedIn$.unsubscribe();
this.channelChange$.unsubscribe();
}
async toggleSubscription() {
......
......@@ -4,6 +4,10 @@ m-videochat {
max-height: calc(100vh - 90px);
}
> div {
height: calc(100vh - 90px);
}
display: block;
position: relative;
}
.m-token--onboarding {
@media screen and (max-width: $max-mobile) {
h2 {
line-height: 30px;
font-size: 20px;
}
}
.m-token--onboarding--slide {
padding: 24px;
display: flex;
......@@ -24,6 +30,9 @@
line-height: 20px;
margin: 8px 0 16px;
padding: 0;
@media screen and (max-width: $max-mobile) {
font-size: 12px;
}
&.m-token--onboarding--subtext-note {
font-size: 12px;
......
......@@ -52,7 +52,15 @@
}
.m-token--onboarding--slide {
@media (max-width: $max-mobile) {
div p {
font-size: 12pt;
}
}
.m-phone-input {
@media (max-width: $max-mobile) {
width: 100%;
}
input {
max-width: 140px;
}
......
/**
* @author Ben Hayward
* @create date 2019-08-16 15:00:04
* @modify date 2019-08-16 15:00:04
* @desc Mock service for feed.spec.ts
*/
import { BehaviorSubject, of } from 'rxjs';
export let feedsServiceMock = {
feed: new BehaviorSubject([Promise.resolve('[1,2,3,4,5]')]),
clear() {
of({ response: false }, { response: false }, { response: true });
},
response() {
return { response: true };
},
setEndpoint(str) {
return this;
}, //chainable
setLimit(limit) {
return this;
},
setParams(params) {
return this;
},
fetch() {
return this;
},
};