diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 24350a81a..de956e17b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -55,9 +55,9 @@ jobs: # NEO4J ################################################################## ########################################################################## - name: Neo4J | Build `community` image - run: | - docker build --target community -t "ocelotsocialnetwork/neo4j:latest" -t "ocelotsocialnetwork/neo4j:community" -t "ocelotsocialnetwork/neo4j:${VERSION}" -t "ocelotsocialnetwork/neo4j:${BUILD_VERSION}" neo4j/ - docker save "ocelotsocialnetwork/neo4j" > /tmp/neo4j.tar + run: docker build --target community -t "ocelotsocialnetwork/neo4j:latest" -t "ocelotsocialnetwork/neo4j:community" -t "ocelotsocialnetwork/neo4j:${VERSION}" -t "ocelotsocialnetwork/neo4j:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT neo4j/ + - name: Neo4J | Save docker image + run: docker save "ocelotsocialnetwork/neo4j" > /tmp/neo4j.tar - name: Upload Artifact uses: actions/upload-artifact@v2 with: @@ -91,10 +91,10 @@ jobs: ########################################################################## # BUILD BACKEND DOCKER IMAGE (production) ################################ ########################################################################## - - name: backend | Build `production` image - run: | - docker build --target production -t "ocelotsocialnetwork/backend:latest" -t "ocelotsocialnetwork/backend:${VERSION}" -t "ocelotsocialnetwork/backend:${BUILD_VERSION}" backend/ - docker save "ocelotsocialnetwork/backend" > /tmp/backend.tar + - name: Backend | Build `production` image + run: docker build --target production -t "ocelotsocialnetwork/backend:latest" -t "ocelotsocialnetwork/backend:${VERSION}" -t "ocelotsocialnetwork/backend:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT backend/ + - name: Backend | Save docker image + run: docker save "ocelotsocialnetwork/backend" > /tmp/backend.tar - name: Upload Artifact uses: actions/upload-artifact@v2 with: @@ -128,10 +128,10 @@ jobs: ########################################################################## # BUILD WEBAPP DOCKER IMAGE (build) ###################################### ########################################################################## - - name: webapp | Build `production` image - run: | - docker build --target production -t "ocelotsocialnetwork/webapp:latest" -t "ocelotsocialnetwork/webapp:${VERSION}" -t "ocelotsocialnetwork/webapp:${BUILD_VERSION}" webapp/ - docker save "ocelotsocialnetwork/webapp" > /tmp/webapp.tar + - name: Webapp | Build `production` image + run: docker build --target production -t "ocelotsocialnetwork/webapp:latest" -t "ocelotsocialnetwork/webapp:${VERSION}" -t "ocelotsocialnetwork/webapp:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ + - name: Webapp | Save docker image + run: docker save "ocelotsocialnetwork/webapp" > /tmp/webapp.tar - name: Upload Artifact uses: actions/upload-artifact@v2 with: @@ -165,11 +165,10 @@ jobs: ########################################################################## # BUILD MAINTENANCE DOCKER IMAGE (build) ################################# ########################################################################## - - name: maintenance | Build `production` image - # TODO: --target production - run: | - docker build -t "ocelotsocialnetwork/maintenance:latest" -t "ocelotsocialnetwork/maintenance:${VERSION}" -t "ocelotsocialnetwork/maintenance:${BUILD_VERSION}" webapp/ -f webapp/Dockerfile.maintenance - docker save "ocelotsocialnetwork/maintenance" > /tmp/maintenance.tar + - name: Maintenance | Build `production` image + run: docker build --target production -t "ocelotsocialnetwork/maintenance:latest" -t "ocelotsocialnetwork/maintenance:${VERSION}" -t "ocelotsocialnetwork/maintenance:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ -f webapp/Dockerfile.maintenance + - name: Maintenance | Save docker image + run: docker save "ocelotsocialnetwork/maintenance" > /tmp/maintenance.tar - name: Upload Artifact uses: actions/upload-artifact@v2 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 15744bfe3..2bec6cebb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -253,7 +253,6 @@ jobs: ########################################################################## # COVERAGE REPORT FRONTEND ################################################ ########################################################################## - # TODO: Maybe remove this later on to avoid spam? #- name: frontend | Coverage report # uses: romeovs/lcov-reporter-action@v0.2.21 # with: @@ -268,5 +267,75 @@ jobs: report_name: Coverage Webapp type: lcov result_path: ./coverage/lcov.info - min_coverage: 52 - token: ${{ github.token }} \ No newline at end of file + min_coverage: 65 + token: ${{ github.token }} + + ############################################################################## + # JOB: FULLSTACK TESTS ####################################################### + ############################################################################## + fullstack_tests: + name: Fullstack tests + runs-on: ubuntu-latest + needs: [build_test_webapp, build_test_backend, build_test_neo4j] + env: + jobs: 8 + strategy: + matrix: + # run copies of the current job in parallel + job: [1, 2, 3, 4, 5, 6, 7, 8] + steps: + ########################################################################## + # CHECKOUT CODE ########################################################## + ########################################################################## + - name: Checkout code + uses: actions/checkout@v2 + ########################################################################## + # DOWNLOAD DOCKER IMAGES ################################################# + ########################################################################## + - name: Download Docker Image (Neo4J) + uses: actions/download-artifact@v2 + with: + name: docker-neo4j-image + path: /tmp + - name: Load Docker Image + run: docker load < /tmp/neo4j.tar + - name: Download Docker Image (Backend) + uses: actions/download-artifact@v2 + with: + name: docker-backend-test + path: /tmp + - name: Load Docker Image + run: docker load < /tmp/backend.tar + - name: Download Docker Image (Webapp) + uses: actions/download-artifact@v2 + with: + name: docker-webapp-test + path: /tmp + - name: Load Docker Image + run: docker load < /tmp/webapp.tar + ########################################################################## + # FULLSTACK TESTS CYPRESS ################################################ + ########################################################################## + - name: webapp | copy env files webapp + run: cp webapp/.env.template webapp/.env + - name: backend | copy env files backend + run: cp backend/.env.template backend/.env + - name: backend | docker-compose + run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend + - name: cypress | Fullstack tests + run: | + yarn install + yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} ) + ########################################################################## + # UPLOAD SCREENSHOTS & VIDEO ############################################# + ########################################################################## + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: cypress-screenshots + path: cypress/screenshots/ + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: cypress-videos + path: cypress/videos/ diff --git a/backend/Dockerfile b/backend/Dockerfile index 2f2b70f04..c632f8803 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -3,15 +3,18 @@ ################################################################################## FROM node:12.19.0-alpine3.10 as base -# ENVs (available in production aswell, can be overwritten by commandline or env file) +# ENVs ## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame ENV DOCKER_WORKDIR="/app" ## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0 -ENV BUILD_DATE="1970-01-01T00:00:00.00Z" +ARG BBUILD_DATE="1970-01-01T00:00:00.00Z" +ENV BUILD_DATE=$BBUILD_DATE ## We cannot do $(yarn run version)-${BUILD_NUMBER} here so we default to 0.0.0-0 -ENV BUILD_VERSION="0.0.0-0" +ARG BBUILD_VERSION="0.0.0-0" +ENV BUILD_VERSION=$BBUILD_VERSION ## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000 -ENV BUILD_COMMIT="0000000" +ARG BBUILD_COMMIT="0000000" +ENV BUILD_COMMIT=$BBUILD_COMMIT ## SET NODE_ENV ENV NODE_ENV="production" ## App relevant Envs diff --git a/backend/src/db/factories.js b/backend/src/db/factories.js index 07a0dacaa..64ee2009c 100644 --- a/backend/src/db/factories.js +++ b/backend/src/db/factories.js @@ -105,12 +105,12 @@ Factory.define('user') }) Factory.define('post') - .option('categoryIds', []) + /* .option('categoryIds', []) .option('categories', ['categoryIds'], (categoryIds) => { if (categoryIds.length) return Promise.all(categoryIds.map((id) => neode.find('Category', id))) // there must be at least one category return Promise.all([Factory.build('category')]) - }) + }) */ .option('tagIds', []) .option('tags', ['tagIds'], (tagIds) => { return Promise.all(tagIds.map((id) => neode.find('Tag', id))) @@ -147,16 +147,16 @@ Factory.define('post') return language || 'en' }) .after(async (buildObject, options) => { - const [post, author, image, categories, tags] = await Promise.all([ + const [post, author, image, /* categories, */ tags] = await Promise.all([ neode.create('Post', buildObject), options.author, options.image, - options.categories, + // options.categories, options.tags, ]) await Promise.all([ post.relateTo(author, 'author'), - Promise.all(categories.map((c) => c.relateTo(post, 'post'))), + // Promise.all(categories.map((c) => c.relateTo(post, 'post'))), Promise.all(tags.map((t) => t.relateTo(post, 'post'))), ]) if (image) await post.relateTo(image, 'image') diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index b7b77bbd5..d199b6f09 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -348,7 +348,7 @@ export default { undefinedToNull: ['activityId', 'objectId', 'language', 'pinnedAt', 'pinned'], hasMany: { tags: '-[:TAGGED]->(related:Tag)', - categories: '-[:CATEGORIZED]->(related:Category)', + // categories: '-[:CATEGORIZED]->(related:Category)', comments: '<-[:COMMENTS]-(related:Comment)', shoutedBy: '<-[:SHOUTED]-(related:User)', emotions: '<-[related:EMOTED]', diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index f0c57b8fb..1317d7c22 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -147,7 +147,7 @@ describe('Post', () => { }) }) - it('by categories', async () => { + /* it('by categories', async () => { const postQueryFilteredByCategories = gql` query Post($filter: _PostFilter) { Post(filter: $filter) { @@ -172,7 +172,7 @@ describe('Post', () => { await expect( query({ query: postQueryFilteredByCategories, variables }), ).resolves.toMatchObject(expected) - }) + }) */ describe('by emotions', () => { const postQueryFilteredByEmotions = gql` @@ -323,14 +323,8 @@ describe('CreatePost', () => { describe('UpdatePost', () => { let author, newlyCreatedPost const updatePostMutation = gql` - mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID], $image: ImageInput) { - UpdatePost( - id: $id - title: $title - content: $content - categoryIds: $categoryIds - image: $image - ) { + mutation($id: ID!, $title: String!, $content: String!, $image: ImageInput) { + UpdatePost(id: $id, title: $title, content: $content, image: $image) { id title content @@ -338,9 +332,6 @@ describe('UpdatePost', () => { name slug } - categories { - id - } createdAt updatedAt } @@ -428,7 +419,7 @@ describe('UpdatePost', () => { expect(newlyCreatedPost.updatedAt).not.toEqual(UpdatePost.updatedAt) }) - describe('no new category ids provided for update', () => { + /* describe('no new category ids provided for update', () => { it('resolves and keeps current categories', async () => { const expected = { data: { @@ -443,9 +434,9 @@ describe('UpdatePost', () => { expected, ) }) - }) + }) */ - describe('given category ids', () => { + /* describe('given category ids', () => { beforeEach(() => { variables = { ...variables, categoryIds: ['cat27'] } }) @@ -464,7 +455,7 @@ describe('UpdatePost', () => { expected, ) }) - }) + }) */ describe('params.image', () => { describe('is object', () => { diff --git a/cypress/README.md b/cypress/README.md index fb94cc522..d9e235786 100644 --- a/cypress/README.md +++ b/cypress/README.md @@ -13,16 +13,13 @@ $ docker-compose up ## Setup without docker -First, you have to tell cypress how to connect to your local neo4j database -among other things. You can copy our template configuration and change the new -file according to your needs. +To start the services that are required for cypress testing manually. You basically need the whole setup to run: -To start the services that are required for cypress testing, run: +- backend +- webapp +- neo4j -```bash -# in the top level folder Ocelot-Social/ -$ yarn cypress:setup -``` +Navigate to the corresponding folders and start the services. ## Install cypress @@ -35,21 +32,11 @@ without docker, you would have to install cypress and its dependencies first: $ yarn install ``` -## Run cypress - -After verifying that there are no errors with the servers starting, open another tab in your terminal and run the following command: - -```bash -$ yarn cypress:run -``` - -![Console output after running cypress test](../.gitbook/assets/grafik%20%281%29.png) - ### Open Interactive Test Console -If you are like me, you might want to see some visual output. The interactive cypress environment also helps at debugging your tests, you can even time travel between individual steps and see the exact state of the app. +The interactive cypress test console allows to run tests and have visual feedback on that. The interactive cypress environment also helps at debugging the tests, you can even time travel between individual steps and see the exact state of the app. -To use this feature, instead of `yarn cypress:run` you would run the following command: +To use this feature run: ```bash $ yarn cypress:open @@ -57,7 +44,19 @@ $ yarn cypress:open ![Interactive Cypress Environment](../.gitbook/assets/grafik-1%20%281%29.png) +## Run cypress + +To run cypress without the user interface: + +```bash +$ yarn cypress:run +``` + +This is used to run cypress in CI or in console + +![Console output after running cypress test](../.gitbook/assets/grafik%20%281%29.png) + ## Write some Tests Check out the Cypress documentation for further information on how to write tests: -[https://docs.cypress.io/guides/getting-started/writing-your-first-test.html\#Write-a-simple-test](https://docs.cypress.io/guides/getting-started/writing-your-first-test.html#Write-a-simple-test) +[Write-a-simple-test](https://docs.cypress.io/guides/getting-started/writing-your-first-test.html#Write-a-simple-test) diff --git a/cypress/constants/terms-and-conditions-version.js b/cypress/constants/terms-and-conditions-version.js deleted file mode 100644 index 7b2a8fb5d..000000000 --- a/cypress/constants/terms-and-conditions-version.js +++ /dev/null @@ -1,2 +0,0 @@ -// please change also version in file "webapp/constants/terms-and-conditions-version.js" -export const VERSION = '0.0.4' \ No newline at end of file diff --git a/cypress.json b/cypress/cypress.json similarity index 55% rename from cypress.json rename to cypress/cypress.json index 284bdbd34..3f41ca3e5 100644 --- a/cypress.json +++ b/cypress/cypress.json @@ -1,8 +1,10 @@ { "projectId": "qa7fe2", "ignoreTestFiles": "*.js", + "chromeWebSecurity": false, "baseUrl": "http://localhost:3000", - "env": { - "RETRIES": 2 + "retries": { + "runMode": 2, + "openMode": 0 } } diff --git a/cypress/integration/Admin.PinPost.feature b/cypress/integration/Admin.PinPost.feature new file mode 100644 index 000000000..a5297d894 --- /dev/null +++ b/cypress/integration/Admin.PinPost.feature @@ -0,0 +1,43 @@ +Feature: Admin pins a post + As an admin + I want to pin a post so that it always appears at the top + In order to make sure all network users read it + e.g. notify people about security incidents, maintenance downtimes + + Background: + Given the following "users" are in the database: + | slug | email | password | id | name | role | termsAndConditionsAgreedVersion | + | user | user@example.org | abcd | user | User-Chad | user | 0.0.4 | + | admin | admin@example.org | 1234 | admin | Admin-Man | admin | 0.0.4 | + Given the following "posts" are in the database: + | id | title | pinned | createdAt | + | p1 | Some other post | | 2020-01-21 | + | p2 | Houston we have a problem | x | 2020-01-20 | + | p3 | Yet another post | | 2020-01-19 | + + Scenario: Pinned post always appears on the top of the newsfeed + When I am logged in as "user" + And I navigate to page "/" + Then the first post on the newsfeed has the title: + """ + Houston we have a problem + """ + And the post with title "Houston we have a problem" has a ribbon for pinned posts + + Scenario: Ordinary users cannot pin a post + When I am logged in as "user" + And I navigate to page "/" + And I open the content menu of post "Yet another post" + Then there is no button to pin a post + + Scenario: Admins are allowed to pin a post + When I am logged in as "admin" + And I navigate to page "/" + And I open the content menu of post "Yet another post" + And I click on "pin post" + Then I see a toaster with "Post pinned successfully" + And the first post on the newsfeed has the title: + """ + Yet another post + """ + And the post with title "Yet another post" has a ribbon for pinned posts diff --git a/cypress/integration/Admin.PinPost/I_open_the_content_menu_of_post_{string}.js b/cypress/integration/Admin.PinPost/I_open_the_content_menu_of_post_{string}.js new file mode 100644 index 000000000..78e9ab1ea --- /dev/null +++ b/cypress/integration/Admin.PinPost/I_open_the_content_menu_of_post_{string}.js @@ -0,0 +1,7 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I open the content menu of post {string}", (title)=> { + cy.contains('.post-teaser', title) + .find('.content-menu .base-button') + .click() +}) \ No newline at end of file diff --git a/cypress/integration/Admin.PinPost/the_post_with_title_{string}_has_a_ribbon_for_pinned_posts.js b/cypress/integration/Admin.PinPost/the_post_with_title_{string}_has_a_ribbon_for_pinned_posts.js new file mode 100644 index 000000000..1db51d2b0 --- /dev/null +++ b/cypress/integration/Admin.PinPost/the_post_with_title_{string}_has_a_ribbon_for_pinned_posts.js @@ -0,0 +1,9 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("the post with title {string} has a ribbon for pinned posts", (title) => { + cy.get(".post-teaser").contains(title) + .parent() + .parent() + .find(".ribbon.--pinned") + .should("contain", "Announcement") +}) \ No newline at end of file diff --git a/cypress/integration/Admin.PinPost/there_is_no_button_to_pin_a_post.js b/cypress/integration/Admin.PinPost/there_is_no_button_to_pin_a_post.js new file mode 100644 index 000000000..859b9faf1 --- /dev/null +++ b/cypress/integration/Admin.PinPost/there_is_no_button_to_pin_a_post.js @@ -0,0 +1,7 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("there is no button to pin a post", () => { + cy.get("a.ds-menu-item-link") + .should('contain', "Report Post") // sanity check + .should('not.contain', "Pin post") +}) \ No newline at end of file diff --git a/cypress/integration/Admin.TagOverview.feature b/cypress/integration/Admin.TagOverview.feature new file mode 100644 index 000000000..fcec638ec --- /dev/null +++ b/cypress/integration/Admin.TagOverview.feature @@ -0,0 +1,31 @@ +Feature: Admin tag overview + As a database administrator + I would like to see a overview of all tags and their usage + In order to be able to decide which tags are popular or not + + Background: + Given the following "users" are in the database: + | slug | email | password | id | name | role | termsAndConditionsAgreedVersion | + | admin | admin@example.org | 1234 | admin | Admin-Man | admin | 0.0.4 | + | u1 | u1@example.org | 1234 | u1 | User1 | user | 0.0.4 | + | u2 | u2@example.org | 1234 | u2 | User2 | user | 0.0.4 | + | u3 | u3@example.org | 1234 | u3 | User3 | user | 0.0.4 | + And the following "tags" are in the database: + | id | + | Ecology | + | Nature | + | Democracy | + And the following "posts" are in the database: + | id | title | authorId | tagIds | + | p1 | P1 from U1 | u1 | Nature, Democracy | + | p2 | P2 from U2 | u2 | Ecology, Democracy | + | p3 | P3 from U3 | u3 | Nature, Democracy | + And I am logged in as "admin" + + Scenario: See an overview of tags + When I navigate to page "/admin/hashtags" + Then I can see the following table: + | No. | Hashtags | Users | Posts | + | 1 | #Democracy | 3 | 3 | + | 2 | #Nature | 2 | 2 | + | 3 | #Ecology | 1 | 1 | \ No newline at end of file diff --git a/cypress/integration/internationalization/Internationalization.feature b/cypress/integration/Internationalization.feature similarity index 89% rename from cypress/integration/internationalization/Internationalization.feature rename to cypress/integration/Internationalization.feature index 18070d888..5eb6bbc3f 100644 --- a/cypress/integration/internationalization/Internationalization.feature +++ b/cypress/integration/Internationalization.feature @@ -4,7 +4,7 @@ Feature: Internationalization In order to be able to understand the interface Background: - Given I am on the "login" page + Given I navigate to page "/login" Scenario Outline: I select "" in the language menu and see "" When I select "" in the language menu @@ -18,6 +18,6 @@ Feature: Internationalization | English | Login | Scenario: Keep preferred language after refresh - Given I previously switched the language to "Français" + When I select "Français" in the language menu And I refresh the page Then the whole user interface appears in "Français" diff --git a/cypress/integration/Internationalization/I_see_a_button_with_the_label_{string}.js b/cypress/integration/Internationalization/I_see_a_button_with_the_label_{string}.js new file mode 100644 index 000000000..a67f9d7df --- /dev/null +++ b/cypress/integration/Internationalization/I_see_a_button_with_the_label_{string}.js @@ -0,0 +1,5 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I see a button with the label {string}", label => { + cy.contains("button", label); +}); \ No newline at end of file diff --git a/cypress/integration/Internationalization/I_select_{string}_in_the_language_menu.js b/cypress/integration/Internationalization/I_select_{string}_in_the_language_menu.js new file mode 100644 index 000000000..b850a7573 --- /dev/null +++ b/cypress/integration/Internationalization/I_select_{string}_in_the_language_menu.js @@ -0,0 +1,8 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I select {string} in the language menu", language => { + cy.get(".locale-menu") + .click(); + cy.contains(".locale-menu-popover a", language) + .click(); +}); \ No newline at end of file diff --git a/cypress/integration/Internationalization/the_whole_user_interface_appears_in_{string}.js b/cypress/integration/Internationalization/the_whole_user_interface_appears_in_{string}.js new file mode 100644 index 000000000..4d80b8a0d --- /dev/null +++ b/cypress/integration/Internationalization/the_whole_user_interface_appears_in_{string}.js @@ -0,0 +1,8 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; +import locales from '../../../webapp/locales' + +Then("the whole user interface appears in {string}", language => { + const { code } = locales.find((entry) => entry.name === language); + cy.get(`html[lang=${code}]`); + cy.getCookie("locale").should("have.property", "value", code); +}); \ No newline at end of file diff --git a/cypress/integration/Moderation.HidePost.feature b/cypress/integration/Moderation.HidePost.feature new file mode 100644 index 000000000..0ef802267 --- /dev/null +++ b/cypress/integration/Moderation.HidePost.feature @@ -0,0 +1,40 @@ +Feature: Hide Posts + As a moderator + I would like to be able to hide posts from the public + to enforce our network's code of conduct and/or legal regulations + + Background: + Given the following "users" are in the database: + | slug | email | password | id | name | role | termsAndConditionsAgreedVersion | + | user | user@example.org | abcd | user | User-Chad | user | 0.0.4 | + | moderator | moderator@example.org | 1234 | moderator | Mod-Man | moderator | 0.0.4 | + Given the following "posts" are in the database: + | id | title | deleted | disabled | + | p1 | This post should be visible | | | + | p2 | This post is disabled | | x | + | p3 | This post is deleted | x | | + + Scenario: Disabled posts don't show up on the newsfeed as user + When I am logged in as "user" + And I navigate to page "/" + Then I should see only 1 posts on the newsfeed + And the first post on the newsfeed has the title: + """ + This post should be visible + """ + + Scenario: Disabled posts show up on the newsfeed as moderator + When I am logged in as "moderator" + And I navigate to page "/" + Then I should see only 2 posts on the newsfeed + And the first post on the newsfeed has the title: + """ + This post is disabled + """ + + Scenario: Visiting a disabled post's page should return 404 + Given I am logged in as "user" + Then the page "/post/this-post-is-disabled" returns a 404 error with a message: + """ + This post could not be found + """ diff --git a/cypress/integration/Moderation.HidePost/I_should_see_only_{int}_posts_on_the_newsfeed.js b/cypress/integration/Moderation.HidePost/I_should_see_only_{int}_posts_on_the_newsfeed.js new file mode 100644 index 000000000..611365bb0 --- /dev/null +++ b/cypress/integration/Moderation.HidePost/I_should_see_only_{int}_posts_on_the_newsfeed.js @@ -0,0 +1,7 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I should see only {int} posts on the newsfeed", posts => { + cy.get(".post-teaser") + .should("have.length", posts); +}); + \ No newline at end of file diff --git a/cypress/integration/Moderation.HidePost/the_page_{string}_returns_a_404_error_with_a_message.js b/cypress/integration/Moderation.HidePost/the_page_{string}_returns_a_404_error_with_a_message.js new file mode 100644 index 000000000..6d9cfb2ef --- /dev/null +++ b/cypress/integration/Moderation.HidePost/the_page_{string}_returns_a_404_error_with_a_message.js @@ -0,0 +1,14 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("the page {string} returns a 404 error with a message:", (route, message) => { + cy.request({ + url: route, + failOnStatusCode: false + }) + .its("status") + .should("eq", 404); + cy.visit(route, { + failOnStatusCode: false + }); + cy.get(".error-message").should("contain", message); +}); \ No newline at end of file diff --git a/cypress/integration/moderation/ReportContent.feature b/cypress/integration/Moderation.ReportContent.feature similarity index 61% rename from cypress/integration/moderation/ReportContent.feature rename to cypress/integration/Moderation.ReportContent.feature index be1a07786..518020bd0 100644 --- a/cypress/integration/moderation/ReportContent.feature +++ b/cypress/integration/Moderation.ReportContent.feature @@ -8,51 +8,47 @@ Feature: Report and Moderate So I can look into it and decide what to do Background: - Given we have the following user accounts: - | id | name | - | u67 | David Irving | - | annoying-user | I'm gonna mute Moderators and Admins HA HA HA | - - Given we have the following posts in our database: + Given the following "users" are in the database: + | slug | email | password | id | name | role | termsAndConditionsAgreedVersion | + | user | user@example.org | abcd | user | User-Chad | user | 0.0.4 | + | moderator | moderator@example.org | 1234 | moderator | Mod-Man | moderator | 0.0.4 | + | annoying | annoying@example.org | 1234 | annoying-user | I'm gonna mute Moderators and Admins HA HA HA | user | 0.0.4 | + And the following "posts" are in the database: | authorId | id | title | content | - | u67 | p1 | The Truth about the Holocaust | It never existed! | + | annoying-user | p1 | The Truth about the Holocaust | It never existed! | | annoying-user | p2 | Fake news | This content is demonstratably infactual in some way | + Scenario Outline: Report a post from various pages - Given I am logged in with a "user" role - When I see David Irving's post on the + When I am logged in as "user" + And I navigate to page "" And I click on "Report Post" from the content menu of the post And I confirm the reporting dialog because it is a criminal act under German law: """ Do you really want to report the contribution "The Truth about the Holocaust"? """ - Then I see a success message: - """ - Thanks for reporting! - """ + Then I see a toaster with "Thanks for reporting!" Examples: - | Page | - | landing page | - | post page | + | Page | + | / | + | /post/p1 | Scenario: Report user - Given I am logged in with a "user" role - And I see David Irving's post on the post page + Given I am logged in as "user" + And I navigate to page "/post/the-truth-about-the-holocaust" When I click on the author And I click on "Report User" from the content menu in the user info box And I confirm the reporting dialog because he is a holocaust denier: """ - Do you really want to report the user "David Irving"? - """ - Then I see a success message: - """ - Thanks for reporting! + Do you really want to report the user "I'm gonna mute Moderators and …"? """ + Then I see a toaster with "Thanks for reporting!" Scenario: Review reported content Given somebody reported the following posts: | submitterEmail | resourceId | reasonCategory | reasonDescription | | p1.submitter@example.org | p1 | discrimination_etc | Offensive content | - And I am logged in with a "moderator" role + And I am logged in as "moderator" + And I navigate to page "/" When I click on the avatar menu in the top right corner And I click on "Moderation" Then I see all the reported posts including the one from above @@ -62,7 +58,8 @@ Feature: Report and Moderate Given somebody reported the following posts: | submitterEmail | resourceId | reasonCategory | reasonDescription | | p2.submitter@example.org | p2 | other | Offensive content | - And I am logged in with a "moderator" role + And I am logged in as "moderator" + And I navigate to page "/" And there is an annoying user who has muted me When I click on the avatar menu in the top right corner And I click on "Moderation" @@ -70,6 +67,7 @@ Feature: Report and Moderate And I can visit the post page Scenario: Normal user can't see the moderation page - Given I am logged in with a "user" role + Given I am logged in as "user" + And I navigate to page "/" When I click on the avatar menu in the top right corner Then I can't see the moderation menu item diff --git a/cypress/integration/Moderation.ReportContent/I_can_t_see_the_moderation_menu_item.js b/cypress/integration/Moderation.ReportContent/I_can_t_see_the_moderation_menu_item.js new file mode 100644 index 000000000..96706281a --- /dev/null +++ b/cypress/integration/Moderation.ReportContent/I_can_t_see_the_moderation_menu_item.js @@ -0,0 +1,11 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then(`I can't see the moderation menu item`, () => { + cy.get('.avatar-menu-popover') + .find('a[href="/settings"]', 'Settings') + .should('exist') // OK, the dropdown is actually open + + cy.get('.avatar-menu-popover') + .find('a[href="/moderation"]', 'Moderation') + .should('not.exist') +}) \ No newline at end of file diff --git a/cypress/integration/Moderation.ReportContent/I_can_visit_the_post_page.js b/cypress/integration/Moderation.ReportContent/I_can_visit_the_post_page.js new file mode 100644 index 000000000..8ca69da50 --- /dev/null +++ b/cypress/integration/Moderation.ReportContent/I_can_visit_the_post_page.js @@ -0,0 +1,7 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then('I can visit the post page', () => { + cy.contains('Fake news').click() + cy.location('pathname').should('contain', '/post') + .get('.base-card .title').should('contain', 'Fake news') +}) \ No newline at end of file diff --git a/cypress/integration/Moderation.ReportContent/I_click_on_Report_Post_from_the_content_menu_of_the_post.js b/cypress/integration/Moderation.ReportContent/I_click_on_Report_Post_from_the_content_menu_of_the_post.js new file mode 100644 index 000000000..30682b009 --- /dev/null +++ b/cypress/integration/Moderation.ReportContent/I_click_on_Report_Post_from_the_content_menu_of_the_post.js @@ -0,0 +1,11 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When('I click on "Report Post" from the content menu of the post', () => { + cy.contains('.base-card', 'The Truth about the Holocaust') + .find('.content-menu .base-button') + .click({force: true}) + + cy.get('.popover .ds-menu-item-link') + .contains('Report Post') + .click() +}) \ No newline at end of file diff --git a/cypress/integration/Moderation.ReportContent/I_click_on_the_author.js b/cypress/integration/Moderation.ReportContent/I_click_on_the_author.js new file mode 100644 index 000000000..3a6600ff6 --- /dev/null +++ b/cypress/integration/Moderation.ReportContent/I_click_on_the_author.js @@ -0,0 +1,7 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When('I click on the author', () => { + cy.get('.user-teaser') + .click() + .url().should('include', '/profile/') +}) \ No newline at end of file diff --git a/cypress/integration/Moderation.ReportContent/I_click_on_the_avatar_menu_in_the_top_right_corner.js b/cypress/integration/Moderation.ReportContent/I_click_on_the_avatar_menu_in_the_top_right_corner.js new file mode 100644 index 000000000..27830b239 --- /dev/null +++ b/cypress/integration/Moderation.ReportContent/I_click_on_the_avatar_menu_in_the_top_right_corner.js @@ -0,0 +1,5 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I click on the avatar menu in the top right corner", () => { + cy.get(".avatar-menu").click(); +}); \ No newline at end of file diff --git a/cypress/integration/Moderation.ReportContent/I_confirm_the_reporting_dialog.js b/cypress/integration/Moderation.ReportContent/I_confirm_the_reporting_dialog.js new file mode 100644 index 000000000..4009fa4e8 --- /dev/null +++ b/cypress/integration/Moderation.ReportContent/I_confirm_the_reporting_dialog.js @@ -0,0 +1,16 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When(/^I confirm the reporting dialog .*:$/, message => { + cy.contains(message) // wait for element to become visible + cy.get('.ds-modal') + .within(() => { + cy.get('.ds-radio-option-label') + .first() + .click({ + force: true + }) + cy.get('button') + .contains('Report') + .click() + }) +}) \ No newline at end of file diff --git a/cypress/integration/Moderation.ReportContent/I_see_all_the_reported_posts_including_from_the_user_who_muted_me.js b/cypress/integration/Moderation.ReportContent/I_see_all_the_reported_posts_including_from_the_user_who_muted_me.js new file mode 100644 index 000000000..522cd6c78 --- /dev/null +++ b/cypress/integration/Moderation.ReportContent/I_see_all_the_reported_posts_including_from_the_user_who_muted_me.js @@ -0,0 +1,7 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then('I see all the reported posts including from the user who muted me', () => { + cy.get('table tbody').within(() => { + cy.contains('tr', 'Fake news') + }) +}) \ No newline at end of file diff --git a/cypress/integration/Moderation.ReportContent/I_see_all_the_reported_posts_including_the_one_from_above.js b/cypress/integration/Moderation.ReportContent/I_see_all_the_reported_posts_including_the_one_from_above.js new file mode 100644 index 000000000..66c9baf61 --- /dev/null +++ b/cypress/integration/Moderation.ReportContent/I_see_all_the_reported_posts_including_the_one_from_above.js @@ -0,0 +1,7 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then('I see all the reported posts including the one from above', () => { + cy.get('table tbody').within(() => { + cy.contains('tr', 'The Truth about the Holocaust') + }) +}) \ No newline at end of file diff --git a/cypress/integration/Moderation.ReportContent/each_list_item_links_to_the_post_page.js b/cypress/integration/Moderation.ReportContent/each_list_item_links_to_the_post_page.js new file mode 100644 index 000000000..9ce69d6de --- /dev/null +++ b/cypress/integration/Moderation.ReportContent/each_list_item_links_to_the_post_page.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then('each list item links to the post page', () => { + cy.contains('The Truth about the Holocaust').click(); + cy.location('pathname').should('contain', '/post') +}) \ No newline at end of file diff --git a/cypress/integration/Moderation.ReportContent/somebody_reported_the_following_posts.js b/cypress/integration/Moderation.ReportContent/somebody_reported_the_following_posts.js new file mode 100644 index 000000000..ce876a081 --- /dev/null +++ b/cypress/integration/Moderation.ReportContent/somebody_reported_the_following_posts.js @@ -0,0 +1,23 @@ +import { Given } from "cypress-cucumber-preprocessor/steps"; +import { gql } from '../../../backend/src/helpers/jest' + +Given('somebody reported the following posts:', table => { + table.hashes().forEach(({ submitterEmail, resourceId, reasonCategory, reasonDescription }) => { + const submitter = { + email: submitterEmail, + password: '1234' + } + cy.factory() + .build('user', {}, submitter) + .authenticateAs(submitter) + .mutate(gql`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { + fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) { + reportId + } + }`, { + resourceId, + reasonCategory, + reasonDescription + }) + }) +}) \ No newline at end of file diff --git a/cypress/integration/Moderation.ReportContent/there_is_an_annoying_user_who_has_muted_me.js b/cypress/integration/Moderation.ReportContent/there_is_an_annoying_user_who_has_muted_me.js new file mode 100644 index 000000000..8d475ee43 --- /dev/null +++ b/cypress/integration/Moderation.ReportContent/there_is_an_annoying_user_who_has_muted_me.js @@ -0,0 +1,13 @@ +Given("there is an annoying user who has muted me", () => { + cy.neode() + .first("User", { + role: 'moderator' + }) + .then(mutedUser => { + cy.neode() + .first("User", { + id: 'user' + }) + .relateTo(mutedUser, "muted"); + }); +}); \ No newline at end of file diff --git a/cypress/integration/Notification.Mention.feature b/cypress/integration/Notification.Mention.feature new file mode 100644 index 000000000..cadfe11dd --- /dev/null +++ b/cypress/integration/Notification.Mention.feature @@ -0,0 +1,29 @@ +Feature: Notification for a mention + As a user + I want to be notified if somebody mentions me in a post or comment + In order join conversations about or related to me + + Background: + Given the following "users" are in the database: + | slug | email | password | id | name | termsAndConditionsAgreedVersion | + | wolle-aus-hamburg | wolle@example.org | 1234 | wolle | Wolle aus Hamburg | 0.0.4 | + | matt-rider | matt@example.org | 4321 | matt | Matt Rider | 0.0.4 | + + Scenario: Mention another user, re-login as this user and see notifications + Given I am logged in as "wolle-aus-hamburg" + And I navigate to page "/" + And I navigate to page "/post/create" + And I start to write a new post with the title "Hey Matt" beginning with: + """ + Big shout to our fellow contributor + """ + And mention "@matt-rider" in the text + And I click on "save button" + And I am logged in as "matt-rider" + And I navigate to page "/" + And see 1 unread notifications in the top menu + And open the notification menu and click on the first item + And I wait for 750 milliseconds + Then I am on page "/post/.*/hey-matt" + And the unread counter is removed + And the notification menu button links to the all notifications page \ No newline at end of file diff --git a/cypress/integration/Notification.Mention/I_start_to_write_a_new_post_with_the_title_{string}_beginning_with.js b/cypress/integration/Notification.Mention/I_start_to_write_a_new_post_with_the_title_{string}_beginning_with.js new file mode 100644 index 000000000..fde5042c1 --- /dev/null +++ b/cypress/integration/Notification.Mention/I_start_to_write_a_new_post_with_the_title_{string}_beginning_with.js @@ -0,0 +1,8 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I start to write a new post with the title {string} beginning with:", (title, intro) => { + cy.get('input[name="title"]') + .type(title); + cy.get(".ProseMirror") + .type(intro); +}); \ No newline at end of file diff --git a/cypress/integration/Notification.Mention/mention_{string}_in_the_text.js b/cypress/integration/Notification.Mention/mention_{string}_in_the_text.js new file mode 100644 index 000000000..fa8a29d4a --- /dev/null +++ b/cypress/integration/Notification.Mention/mention_{string}_in_the_text.js @@ -0,0 +1,9 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("mention {string} in the text", mention => { + cy.get(".ProseMirror") + .type(" @"); + cy.get(".suggestion-list__item") + .contains(mention) + .click(); +}); \ No newline at end of file diff --git a/cypress/integration/Notification.Mention/open_the_notification_menu_and_click_on_the_first_item.js b/cypress/integration/Notification.Mention/open_the_notification_menu_and_click_on_the_first_item.js new file mode 100644 index 000000000..0d3917f38 --- /dev/null +++ b/cypress/integration/Notification.Mention/open_the_notification_menu_and_click_on_the_first_item.js @@ -0,0 +1,10 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("open the notification menu and click on the first item", () => { + cy.get(".notifications-menu") + .invoke('show') + .click(); // "invoke('show')" because of the delay for show the menu + cy.get(".notification .link") + .first() + .click({force: true}); +}); \ No newline at end of file diff --git a/cypress/integration/Notification.Mention/see_{int}_unread_notifications_in_the_top_menu.js b/cypress/integration/Notification.Mention/see_{int}_unread_notifications_in_the_top_menu.js new file mode 100644 index 000000000..124b61873 --- /dev/null +++ b/cypress/integration/Notification.Mention/see_{int}_unread_notifications_in_the_top_menu.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("see {int} unread notifications in the top menu", count => { + cy.get(".notifications-menu") + .should("contain", count); +}); \ No newline at end of file diff --git a/cypress/integration/Notification.Mention/the_notification_menu_button_links_to_the_all_notifications_page.js b/cypress/integration/Notification.Mention/the_notification_menu_button_links_to_the_all_notifications_page.js new file mode 100644 index 000000000..e40827a16 --- /dev/null +++ b/cypress/integration/Notification.Mention/the_notification_menu_button_links_to_the_all_notifications_page.js @@ -0,0 +1,8 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("the notification menu button links to the all notifications page", () => { + cy.get(".notifications-menu") + .click(); + cy.location("pathname") + .should("contain", "/notifications"); +}); \ No newline at end of file diff --git a/cypress/integration/Notification.Mention/the_unread_counter_is_removed.js b/cypress/integration/Notification.Mention/the_unread_counter_is_removed.js new file mode 100644 index 000000000..3859103e8 --- /dev/null +++ b/cypress/integration/Notification.Mention/the_unread_counter_is_removed.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("the unread counter is removed", () => { + cy.get('.notifications-menu .counter-icon') + .should('not.exist'); +}); \ No newline at end of file diff --git a/cypress/integration/PersistentLinks.feature b/cypress/integration/PersistentLinks.feature new file mode 100644 index 000000000..89f9d9654 --- /dev/null +++ b/cypress/integration/PersistentLinks.feature @@ -0,0 +1,31 @@ +Feature: Persistent Links + As a user + I want all links to carry permanent information that identifies the linked resource + In order to have persistent links even if a part of the URL might change + | | Modifiable | Referenceable | Unique | Purpose | + | -- | -- | -- | -- | -- | + | ID | no | yes | yes | Identity, Traceability, Links | + | Slug | yes | yes | yes | @-Mentions, SEO-friendly URL | + | Name | yes | no | no | Search, self-description | + + Background: + Given the following "users" are in the database: + | slug | email | password | id | name | termsAndConditionsAgreedVersion | + | thehawk | hawk@example.org | abcd | MHNqce98y1 | Stephen Hawking | 0.0.4 | + | narrator | narrator@example.org | 1234 | narrator | Nathan Narrator | 0.0.4 | + And the following "posts" are in the database: + | id | title | slug | + | bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays | + And I am logged in as "narrator" + + Scenario Outline: Link with healable information is valid and gets auto-completed + When I navigate to page "" + Then I am on page "" + Examples: + | url | redirectUrl | reason | + | /profile/thehawk | /profile/MHNqce98y1/thehawk | Identifiable user slug | + | /post/101-essays | /post/bWBjpkTKZp/101-essays | Identifiable post slug | + | /profile/MHNqce98y1 | /profile/MHNqce98y1/thehawk | Identifiable user ID | + | /post/bWBjpkTKZp | /post/bWBjpkTKZp/101-essays | Identifiable post ID | + | /profile/MHNqce98y1/stephen-hawking | /profile/MHNqce98y1/thehawk | Identifiable user ID takes precedence over slug | + | /post/bWBjpkTKZp/the-way-you-think | /post/bWBjpkTKZp/101-essays | Identifiable post ID takes precedence over slug | diff --git a/cypress/integration/Post.Comment.feature b/cypress/integration/Post.Comment.feature new file mode 100644 index 000000000..1ec0c602a --- /dev/null +++ b/cypress/integration/Post.Comment.feature @@ -0,0 +1,49 @@ +Feature: Comments on post + As a user + I want to comment and see comments on contributions of others + To be able to express my thoughts and emotions about these, discuss, and add give further information. + + Background: + Given the following "users" are in the database: + | slug | email | password | id | name | termsAndConditionsAgreedVersion | + | peter-pan| peter@pan.com | abcd | id-of-peter-pan| Peter Pan | 0.0.4 | + | narrator | narrator@example.org | 1234 | narrator | Nathan Narrator | 0.0.4 | + And the following "posts" are in the database: + | id | title | slug | authorId | + | bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays | id-of-peter-pan | + And the following "comments" are in the database: + | postId | content | authorId | + | bWBjpkTKZp | @peter-pan reply to me | id-of-peter-pan | + And I am logged in as "narrator" + + Scenario: Comment creation + Given I navigate to page "/post/bWBjpkTKZp/101-essays" + And I comment the following: + """ + Ocelot.social rocks + """ + And I click on "comment button" + Then my comment should be successfully created + And I should see my comment + And the editor should be cleared + + Scenario: View medium length comments + Given I navigate to page "/post/bWBjpkTKZp/101-essays" + And I type in a comment with 305 characters + And I click on "comment button" + Then my comment should be successfully created + And I should see the entirety of my comment + And the editor should be cleared + + Scenario: View long comments + Given I navigate to page "/post/bWBjpkTKZp/101-essays" + And I type in a comment with 1205 characters + And I click on "comment button" + Then my comment should be successfully created + And I should see an abbreviated version of my comment + And the editor should be cleared + + Scenario: Direct reply to Comment + Given I navigate to page "/post/bWBjpkTKZp/101-essays" + And I click on "reply button" + Then it should create a mention in the CommentForm diff --git a/cypress/integration/Post.Comment/I_comment_the_following.js b/cypress/integration/Post.Comment/I_comment_the_following.js new file mode 100644 index 000000000..0f5a5049c --- /dev/null +++ b/cypress/integration/Post.Comment/I_comment_the_following.js @@ -0,0 +1,7 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I comment the following:", async text => { + const comment = text.replace("\n", " ") + cy.task('pushValue', { name: 'lastComment', value: comment }) + cy.get(".editor .ProseMirror").type(comment); +}); \ No newline at end of file diff --git a/cypress/integration/Post.Comment/I_should_see_an_abbreviated_version_of_my_comment.js b/cypress/integration/Post.Comment/I_should_see_an_abbreviated_version_of_my_comment.js new file mode 100644 index 000000000..d0b7940f0 --- /dev/null +++ b/cypress/integration/Post.Comment/I_should_see_an_abbreviated_version_of_my_comment.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I should see an abbreviated version of my comment", () => { + cy.get("article.comment-card") + .should("contain", "show more") +}); \ No newline at end of file diff --git a/cypress/integration/Post.Comment/I_should_see_my_comment.js b/cypress/integration/Post.Comment/I_should_see_my_comment.js new file mode 100644 index 000000000..356593f9c --- /dev/null +++ b/cypress/integration/Post.Comment/I_should_see_my_comment.js @@ -0,0 +1,13 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I should see my comment", () => { + cy.get("article.comment-card p") + .should("contain", "Ocelot.social rocks") + .get(".user-teaser span.slug") + .should("contain", "@peter-pan") // specific enough + .get(".user-avatar img") + .should("have.attr", "src") + .and("contain", 'https://') // some url + .get(".user-teaser > .info > .text") + .should("contain", "today at"); +}); \ No newline at end of file diff --git a/cypress/integration/Post.Comment/I_should_see_the_entirety_of_my_comment.js b/cypress/integration/Post.Comment/I_should_see_the_entirety_of_my_comment.js new file mode 100644 index 000000000..a903fa4d0 --- /dev/null +++ b/cypress/integration/Post.Comment/I_should_see_the_entirety_of_my_comment.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I should see the entirety of my comment", () => { + cy.get("article.comment-card") + .should("not.contain", "show more") +}); \ No newline at end of file diff --git a/cypress/integration/Post.Comment/I_type_in_a_comment_with_{int}_characters.js b/cypress/integration/Post.Comment/I_type_in_a_comment_with_{int}_characters.js new file mode 100644 index 000000000..1522c0e64 --- /dev/null +++ b/cypress/integration/Post.Comment/I_type_in_a_comment_with_{int}_characters.js @@ -0,0 +1,9 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I type in a comment with {int} characters", size => { + var c=""; + for (var i = 0; i < size; i++) { + c += "c" + } + cy.get(".editor .ProseMirror").type(c); +}); \ No newline at end of file diff --git a/cypress/integration/Post.Comment/it_should_create_a_mention_in_the_CommentForm.js b/cypress/integration/Post.Comment/it_should_create_a_mention_in_the_CommentForm.js new file mode 100644 index 000000000..3468badad --- /dev/null +++ b/cypress/integration/Post.Comment/it_should_create_a_mention_in_the_CommentForm.js @@ -0,0 +1,7 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("it should create a mention in the CommentForm", () => { + cy.get(".ProseMirror a") + .should('have.class', 'mention') + .should('contain', '@peter-pan') +}) \ No newline at end of file diff --git a/cypress/integration/Post.Comment/my_comment_should_be_successfully_created.js b/cypress/integration/Post.Comment/my_comment_should_be_successfully_created.js new file mode 100644 index 000000000..766106ddf --- /dev/null +++ b/cypress/integration/Post.Comment/my_comment_should_be_successfully_created.js @@ -0,0 +1,5 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("my comment should be successfully created", () => { + cy.get(".iziToast-message").contains("Comment submitted!"); +}); \ No newline at end of file diff --git a/cypress/integration/Post.Comment/the_editor_should_be_cleared.js b/cypress/integration/Post.Comment/the_editor_should_be_cleared.js new file mode 100644 index 000000000..579fc2ca9 --- /dev/null +++ b/cypress/integration/Post.Comment/the_editor_should_be_cleared.js @@ -0,0 +1,5 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("the editor should be cleared", () => { + cy.get(".ProseMirror p").should("have.class", "is-empty"); +}); \ No newline at end of file diff --git a/cypress/integration/Post.Create.feature b/cypress/integration/Post.Create.feature new file mode 100644 index 000000000..cdb3e1008 --- /dev/null +++ b/cypress/integration/Post.Create.feature @@ -0,0 +1,24 @@ +Feature: Create a post + As an logged in user + I would like to create a post + To say something to everyone in the community + + Background: + Given the following "users" are in the database: + | slug | email | password | id | name | termsAndConditionsAgreedVersion | + | narrator | narrator@example.org | 1234 | narrator | Nathan Narrator | 0.0.4 | + And I am logged in as "narrator" + And I navigate to page "/" + + Scenario: Create a post + When I click on "create post button" + Then I am on page "post/create" + When I choose "My first post" as the title + And I choose the following text as content: + """ + Ocelot.social is a free and open-source social network + for active citizenship. + """ + And I click on "save button" + Then I am on page "/post/.*/my-first-post" + And the post was saved successfully diff --git a/cypress/integration/Post.Create/I_choose_{string}_as_the_title.js b/cypress/integration/Post.Create/I_choose_{string}_as_the_title.js new file mode 100644 index 000000000..9fbf8e58f --- /dev/null +++ b/cypress/integration/Post.Create/I_choose_{string}_as_the_title.js @@ -0,0 +1,8 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I choose {string} as the title", async title => { + const lastPost = {} + lastPost.title = title.replace("\n", " "); + cy.task('pushValue', { name: 'lastPost', value: lastPost }) + cy.get('input[name="title"]').type(lastPost.title); +}); \ No newline at end of file diff --git a/cypress/integration/Post.Create/the_post_was_saved_successfully.js b/cypress/integration/Post.Create/the_post_was_saved_successfully.js new file mode 100644 index 000000000..eec2f819b --- /dev/null +++ b/cypress/integration/Post.Create/the_post_was_saved_successfully.js @@ -0,0 +1,8 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("the post was saved successfully", async () => { + cy.task('getValue', 'lastPost').then(lastPost => { + cy.get(".base-card > .title").should("contain", lastPost.title); + cy.get(".content").should("contain", lastPost.content); + }) +}); \ No newline at end of file diff --git a/cypress/integration/Post.Images.feature b/cypress/integration/Post.Images.feature new file mode 100644 index 000000000..68c223394 --- /dev/null +++ b/cypress/integration/Post.Images.feature @@ -0,0 +1,66 @@ +Feature: Upload/Delete images on posts + As a user + I would like to be able to add/delete an image to/from my Post + So that I can personalize my posts + + Background: + Given the following "users" are in the database: + | slug | email | password | id | name | termsAndConditionsAgreedVersion | + | narrator | narrator@example.org | 1234 | narrator | Nathan Narrator | 0.0.4 | + And the following "posts" are in the database: + | authorId | id | title | content | + | narrator | p1 | Post to be updated | successfully updated | + And I am logged in as "narrator" + And I navigate to page "/" + + Scenario: Create a Post with a Teaser Image + When I click on "create post button" + Then I wait for 750 milliseconds + Then I should be able to "add" a teaser image + And I add all required fields + And I click on "save button" + And I wait for 750 milliseconds + Then I am on page "/post/.*/new-post" + And I wait for 750 milliseconds + And the post was saved successfully with the "new" teaser image + + Scenario: Update a Post to add an image + Given I navigate to page "/post/edit/p1" + Then I wait for 750 milliseconds + And I should be able to "change" a teaser image + And I click on "save button" + Then I see a toaster with "Saved!" + And I wait for 750 milliseconds + And I am on page "/post/.*/post-to-be-updated" + And I wait for 750 milliseconds + Then the post was saved successfully with the "updated" teaser image + + Scenario: Add image, then add a different image + When I click on "create post button" + Then I wait for 750 milliseconds + Then I should be able to "add" a teaser image + And I should be able to "change" a teaser image + And the first image should not be displayed anymore + + Scenario: Add image, then delete it + When I click on "create post button" + Then I wait for 750 milliseconds + Then I should be able to "add" a teaser image + Then I should be able to "remove" a teaser image + And I add all required fields + And I click on "save button" + And I wait for 750 milliseconds + Then I am on page "/post/.*/new-post" + And I wait for 750 milliseconds + And the "new" post was saved successfully without a teaser image + + Scenario: Delete existing image + Given I navigate to page "/post/edit/p1" + Then I wait for 750 milliseconds + And my post has a teaser image + Then I should be able to "remove" a teaser image + And I click on "save button" + And I wait for 750 milliseconds + Then I am on page "/post/.*/post-to-be-updated" + And I wait for 750 milliseconds + And the "updated" post was saved successfully without a teaser image \ No newline at end of file diff --git a/cypress/integration/Post.Images/I_add_all_required_fields.js b/cypress/integration/Post.Images/I_add_all_required_fields.js new file mode 100644 index 000000000..52a95ab52 --- /dev/null +++ b/cypress/integration/Post.Images/I_add_all_required_fields.js @@ -0,0 +1,8 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I add all required fields", () => { + cy.get('input[name="title"]') + .type('new post') + .get(".editor .ProseMirror") + .type('new post content') + }) \ No newline at end of file diff --git a/cypress/integration/Post.Images/I_should_be_able_to_{string}_a_teaser_image.js b/cypress/integration/Post.Images/I_should_be_able_to_{string}_a_teaser_image.js new file mode 100644 index 000000000..ce5b54f25 --- /dev/null +++ b/cypress/integration/Post.Images/I_should_be_able_to_{string}_a_teaser_image.js @@ -0,0 +1,30 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I should be able to {string} a teaser image", condition => { + // cy.reload() + switch(condition){ + case 'change': + cy.get('.delete-image-button') + .click() + cy.fixture('humanconnection.png').as('postTeaserImage').then(function() { + cy.get("#postdropzone").upload( + { fileContent: this.postTeaserImage, fileName: 'humanconnection.png', mimeType: "image/png" }, + { subjectType: "drag-n-drop", force: true } + ).wait(750); + }) + break; + case 'add': + cy.fixture('onourjourney.png').as('postTeaserImage').then(function() { + cy.get("#postdropzone").upload( + { fileContent: this.postTeaserImage, fileName: 'onourjourney.png', mimeType: "image/png" }, + { subjectType: "drag-n-drop", force: true } + ).wait(750); + }) + break; + case 'remove': + cy.get('.delete-image-button') + .click() + break; + } + +}) \ No newline at end of file diff --git a/cypress/integration/Post.Images/my_post_has_a_teaser_image.js b/cypress/integration/Post.Images/my_post_has_a_teaser_image.js new file mode 100644 index 000000000..66ff3c31d --- /dev/null +++ b/cypress/integration/Post.Images/my_post_has_a_teaser_image.js @@ -0,0 +1,7 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When('my post has a teaser image', () => { + cy.get('.contribution-form .image') + .should('exist') + .and('have.attr', 'src') +}) \ No newline at end of file diff --git a/cypress/integration/Post.Images/the_first_image_should_not_be_displayed_anymore.js b/cypress/integration/Post.Images/the_first_image_should_not_be_displayed_anymore.js new file mode 100644 index 000000000..867c97fdf --- /dev/null +++ b/cypress/integration/Post.Images/the_first_image_should_not_be_displayed_anymore.js @@ -0,0 +1,9 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("the first image should not be displayed anymore", () => { + cy.get(".hero-image") + .children() + .get('.hero-image > .image') + .should('have.length', 1) + .and('have.attr', 'src') +}) \ No newline at end of file diff --git a/cypress/integration/Post.Images/the_post_was_saved_successfully_with_the_{string}_teaser_image.js b/cypress/integration/Post.Images/the_post_was_saved_successfully_with_the_{string}_teaser_image.js new file mode 100644 index 000000000..ece83d878 --- /dev/null +++ b/cypress/integration/Post.Images/the_post_was_saved_successfully_with_the_{string}_teaser_image.js @@ -0,0 +1,11 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("the post was saved successfully with the {string} teaser image", condition => { + cy.get(".base-card > .title") + .should("contain", condition === 'updated' ? 'to be updated' : 'new post') + .get(".content") + .should("contain", condition === 'updated' ? 'successfully updated' : 'new post content') + .get('.post-page img') + .should("have.attr", "src") + .and("contains", condition === 'updated' ? 'humanconnection' : 'onourjourney') +}) \ No newline at end of file diff --git a/cypress/integration/Post.Images/the_{string}_post_was_saved_successfully_without_a_teaser_image.js b/cypress/integration/Post.Images/the_{string}_post_was_saved_successfully_without_a_teaser_image.js new file mode 100644 index 000000000..abafcf0cc --- /dev/null +++ b/cypress/integration/Post.Images/the_{string}_post_was_saved_successfully_without_a_teaser_image.js @@ -0,0 +1,12 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then('the {string} post was saved successfully without a teaser image', condition => { + cy.get(".base-card > .title") + .should("contain", condition === 'updated' ? 'to be updated' : 'new post') + .get(".content") + .should("contain", condition === 'updated' ? 'successfully updated' : 'new post content') + .get('.post-page') + .should('exist') + .get('.hero-image > .image') + .should('not.exist') +}) \ No newline at end of file diff --git a/cypress/integration/Post.feature b/cypress/integration/Post.feature new file mode 100644 index 000000000..7a572b955 --- /dev/null +++ b/cypress/integration/Post.feature @@ -0,0 +1,23 @@ +Feature: See a post + As an logged in user + I would like to see a post + And to see the whole content of it + + Background: + Given the following "users" are in the database: + | slug | email | password | id | name | termsAndConditionsAgreedVersion | + | peter-pan| peter@pan.com | abcd | id-of-peter-pan| Peter Pan | 0.0.4 | + | narrator | narrator@example.org | 1234 | narrator | Nathan Narrator | 0.0.4 | + And the following "posts" are in the database: + | id | title | slug | authorId | content | + | aBcDeFgHiJ | previously created post | previously-created-post | id-of-peter-pan | with some content | + And I am logged in as "narrator" + + Scenario: See a post on the newsfeed + When I navigate to page "/" + Then the post shows up on the newsfeed at position 1 + + Scenario: Navigate to the Post Page + When I navigate to page "/" + And I click on "the first post" + Then I am on page "/post/.*" diff --git a/cypress/integration/Post/the_post_shows_up_on_the_newsfeed_at_position_{int}.js b/cypress/integration/Post/the_post_shows_up_on_the_newsfeed_at_position_{int}.js new file mode 100644 index 000000000..3b42ea58e --- /dev/null +++ b/cypress/integration/Post/the_post_shows_up_on_the_newsfeed_at_position_{int}.js @@ -0,0 +1,8 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("the post shows up on the newsfeed at position {int}", index => { + const selector = `.post-teaser:nth-child(${index}) > .base-card`; + cy.get(selector).should("contain", 'previously created post'); + cy.get(selector).should("contain", 'with some content'); + +}); \ No newline at end of file diff --git a/cypress/integration/search/Search.feature b/cypress/integration/Search.feature.broken similarity index 59% rename from cypress/integration/search/Search.feature rename to cypress/integration/Search.feature.broken index d128838f3..a770c757c 100644 --- a/cypress/integration/search/Search.feature +++ b/cypress/integration/Search.feature.broken @@ -4,20 +4,21 @@ Feature: Search In order to find related content Background: - Given I have a user account - And we have the following posts in our database: + Given the following "users" are in the database: + | slug | email | password | id | name | termsAndConditionsAgreedVersion | + | narrator | narrator@example.org | 1234 | narrator | Nathan Narrator | 0.0.4 | + | search-for-me | u1@example.org | 1234 | user-for-search | Search for me | 0.0.4 | + | not-to-be-found | u2@example.org | 1234 | just-an-id | Not to be found | 0.0.4 | + And the following "posts" are in the database: | id | title | content | | p1 | 101 Essays that will change the way you think | 101 Essays, of course (PR)! | - | p2 | No content | will be found in this post, I guarantee | - And we have the following user accounts: - | slug | name | id | - | search-for-me | Search for me | user-for-search | - | not-to-be-found | Not to be found | just-an-id | - - Given I am logged in + | p2 | No content | will be found in this post, I guarantee | + And I am logged in as "narrator" + And I navigate to page "/" Scenario: Search for specific words When I search for "Essays" + And I wait for 3000 milliseconds Then I should have one item in the select dropdown Then I should see the following posts in the select dropdown: | title | @@ -25,8 +26,9 @@ Feature: Search Scenario: Press enter opens search page When I type "PR" and press Enter - Then I should see the search results page - Then I should see the following posts on the search results page + Then I am on page "/search/search-results" + And the search parameter equals "?search=PR" + Then I should see the following posts on the search results page: | title | | 101 Essays that will change the way you think | @@ -36,8 +38,9 @@ Feature: Search Scenario: Select entry goes to post When I search for "Essays" + And I wait for 3000 milliseconds And I select a post entry - Then I should be on the post's page + Then I am on page "/post/p1/101-essays-that-will-change-the-way-you-think" Scenario: Select dropdown content When I search for "Essays" @@ -52,4 +55,4 @@ Feature: Search | slug | | search-for-me | And I select a user entry - Then I should be on the user's profile \ No newline at end of file + Then I am on page "/profile/user-for-search/search-for-me" \ No newline at end of file diff --git a/cypress/integration/Search/I_select_a_post_entry.js b/cypress/integration/Search/I_select_a_post_entry.js new file mode 100644 index 000000000..25611f91e --- /dev/null +++ b/cypress/integration/Search/I_select_a_post_entry.js @@ -0,0 +1,7 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I select a post entry", () => { + cy.get(".searchable-input .search-post") + .first() + .trigger("click"); +}); \ No newline at end of file diff --git a/cypress/integration/Search/I_select_a_user_entry.js b/cypress/integration/Search/I_select_a_user_entry.js new file mode 100644 index 000000000..b7222b2fb --- /dev/null +++ b/cypress/integration/Search/I_select_a_user_entry.js @@ -0,0 +1,7 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I select a user entry", () => { + cy.get(".searchable-input .user-teaser") + .first() + .trigger("click"); +}) \ No newline at end of file diff --git a/cypress/integration/Search/I_should_have_one_item_in_the_select_dropdown.js b/cypress/integration/Search/I_should_have_one_item_in_the_select_dropdown.js new file mode 100644 index 000000000..7e5188ab6 --- /dev/null +++ b/cypress/integration/Search/I_should_have_one_item_in_the_select_dropdown.js @@ -0,0 +1,7 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I should have one item in the select dropdown", () => { + cy.get(".searchable-input .ds-select-dropdown").should($li => { + expect($li).to.have.length(1); + }); +}); \ No newline at end of file diff --git a/cypress/integration/Search/I_should_not_see_posts_without_the_searched-for_term_in_the_select_dropdown.js b/cypress/integration/Search/I_should_not_see_posts_without_the_searched-for_term_in_the_select_dropdown.js new file mode 100644 index 000000000..a76ed6a5d --- /dev/null +++ b/cypress/integration/Search/I_should_not_see_posts_without_the_searched-for_term_in_the_select_dropdown.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I should not see posts without the searched-for term in the select dropdown", () => { + cy.get(".ds-select-dropdown") + .should("not.contain","No searched for content"); +}); \ No newline at end of file diff --git a/cypress/integration/Search/I_should_see_posts_with_the_searched-for_term_in_the_select_dropdown.js b/cypress/integration/Search/I_should_see_posts_with_the_searched-for_term_in_the_select_dropdown.js new file mode 100644 index 000000000..ce755abb0 --- /dev/null +++ b/cypress/integration/Search/I_should_see_posts_with_the_searched-for_term_in_the_select_dropdown.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I should see posts with the searched-for term in the select dropdown", () => { + cy.get(".ds-select-dropdown") + .should("contain","101 Essays that will change the way you think"); +}); \ No newline at end of file diff --git a/cypress/integration/Search/I_should_see_the_following_posts_on_the_search_results_page.js b/cypress/integration/Search/I_should_see_the_following_posts_on_the_search_results_page.js new file mode 100644 index 000000000..f703a04f5 --- /dev/null +++ b/cypress/integration/Search/I_should_see_the_following_posts_on_the_search_results_page.js @@ -0,0 +1,8 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I should see the following posts on the search results page:", table => { + table.hashes().forEach(({ title }) => { + cy.get(".post-teaser") + .should("contain",title) + }); +}); \ No newline at end of file diff --git a/cypress/integration/Search/I_should_see_the_following_users_in_the_select_dropdown.js b/cypress/integration/Search/I_should_see_the_following_users_in_the_select_dropdown.js new file mode 100644 index 000000000..3e5e14043 --- /dev/null +++ b/cypress/integration/Search/I_should_see_the_following_users_in_the_select_dropdown.js @@ -0,0 +1,8 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I should see the following users in the select dropdown:", table => { + cy.get(".search-heading").should("contain", "Users"); + table.hashes().forEach(({ slug }) => { + cy.get(".ds-select-dropdown").should("contain", slug); + }); +}); \ No newline at end of file diff --git a/cypress/integration/Search/I_type_{string}_and_press_Enter.js b/cypress/integration/Search/I_type_{string}_and_press_Enter.js new file mode 100644 index 000000000..1a0fc6d42 --- /dev/null +++ b/cypress/integration/Search/I_type_{string}_and_press_Enter.js @@ -0,0 +1,8 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I type {string} and press Enter", value => { + cy.get(".searchable-input .ds-select input") + .focus() + .type(value) + .type("{enter}", { force: true }); +}); \ No newline at end of file diff --git a/cypress/integration/Search/I_type_{string}_and_press_escape.js b/cypress/integration/Search/I_type_{string}_and_press_escape.js new file mode 100644 index 000000000..a3cde6cda --- /dev/null +++ b/cypress/integration/Search/I_type_{string}_and_press_escape.js @@ -0,0 +1,8 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I type {string} and press escape", value => { + cy.get(".searchable-input .ds-select input") + .focus() + .type(value) + .type("{esc}"); +}); \ No newline at end of file diff --git a/cypress/integration/Search/the_search_field_should_clear.js b/cypress/integration/Search/the_search_field_should_clear.js new file mode 100644 index 000000000..f571cdbd4 --- /dev/null +++ b/cypress/integration/Search/the_search_field_should_clear.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("the search field should clear", () => { + cy.get(".searchable-input .ds-select input") + .should("have.text", ""); +}); \ No newline at end of file diff --git a/cypress/integration/Search/the_search_parameter_equals_{string}.js b/cypress/integration/Search/the_search_parameter_equals_{string}.js new file mode 100644 index 000000000..b8473584c --- /dev/null +++ b/cypress/integration/Search/the_search_parameter_equals_{string}.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("the search parameter equals {string}", search => { + cy.location("search") + .should("eq", search); +}); \ No newline at end of file diff --git a/cypress/integration/User.Authentication.feature b/cypress/integration/User.Authentication.feature new file mode 100644 index 000000000..db7680bd4 --- /dev/null +++ b/cypress/integration/User.Authentication.feature @@ -0,0 +1,26 @@ +Feature: User authentication + As an user + I want to sign in + In order to be able to posts and do other contributions as myself + Furthermore I want to be able to stay logged in and logout again + + Background: + Given the following "users" are in the database: + | email | password | id | name | slug | termsAndConditionsAgreedVersion | + | peterpan@example.org | 1234 | id-of-peter-pan | Peter Pan | peter-pan | 0.0.4 | + + Scenario: Log in + When I navigate to page "/login" + And I fill in my credentials "peterpan@example.org" "1234" + And I click on "submit button" + Then I am logged in with username "Peter Pan" + + Scenario: Refresh and stay logged in + Given I am logged in as "peter-pan" + When I refresh the page + Then I am logged in with username "Peter Pan" + + Scenario: Log out + Given I am logged in as "peter-pan" + When I log out + Then I am on page "login" diff --git a/cypress/integration/User.Authentication/I_am_logged_in_with_username_{string}.js b/cypress/integration/User.Authentication/I_am_logged_in_with_username_{string}.js new file mode 100644 index 000000000..4383282bd --- /dev/null +++ b/cypress/integration/User.Authentication/I_am_logged_in_with_username_{string}.js @@ -0,0 +1,7 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I am logged in with username {string}", name => { + cy.get(".avatar-menu").click(); + cy.get(".avatar-menu-popover").contains(name); + cy.get(".avatar-menu").click(); // Close menu again +}); \ No newline at end of file diff --git a/cypress/integration/user_profile/BlockUser.feature b/cypress/integration/User.Block.feature.broken similarity index 57% rename from cypress/integration/user_profile/BlockUser.feature rename to cypress/integration/User.Block.feature.broken index b5c510286..3d58c3c27 100644 --- a/cypress/integration/user_profile/BlockUser.feature +++ b/cypress/integration/User.Block.feature.broken @@ -1,16 +1,21 @@ -Feature: Block a User +Feature: User - block an user As a user I'd like to have a button to block another user To prevent him from seeing and interacting with my contributions Background: - Given I have a user account - And there is an annoying user called "Harassing User" - And I am logged in + Given the following "users" are in the database: + | email | password | id | name | slug | termsAndConditionsAgreedVersion | + | peterpan@example.org | 123 | id-of-peter-pan | Peter Pan | peter-pan | 0.0.4 | + | user@example.org | 123 | harassing-user | Harassing User | harassing-user | 0.0.4 | + And the following "posts" are in the database: + | id | title | slug | authorId | + | bWBjpkTKZp | previously created post | previously-created-post | id-of-peter-pan | + And I am logged in as "peter-pan" Scenario: Block a user - Given I am on the profile page of the annoying user - When I click on "Block user" from the content menu in the user info box + When I navigate to page "profile/harassing-user" + And I click on "Block user" from the content menu in the user info box And I "should" see "Unblock user" from the content menu in the user info box And I navigate to my "Blocked users" settings page Then I can see the following table: @@ -19,42 +24,46 @@ Feature: Block a User Scenario: Blocked user cannot interact with my contributions Given I block the user "Harassing User" - And I previously created a post - And a blocked user visits the post page of one of my authored posts + And I am logged in as "harassing-user" + And I navigate to page "/post/previously-created-post" Then they should see a text explaining why commenting is not possible And they should not see the comment form Scenario: Block a previously followed user Given I follow the user "Harassing User" - When I visit the profile page of the annoying user + When I navigate to page "/profile/harassing-user" And I click on "Block user" from the content menu in the user info box And I get removed from his follower collection And I "should" see "Unblock user" from the content menu in the user info box Scenario: Posts of blocked users are not filtered from search results - Given "Harassing User" wrote a post "You can still see my posts" + Given "harassing-user" wrote a post "You can still see my posts" And I block the user "Harassing User" When I search for "see" + And I wait for 3000 milliseconds Then I should see the following posts in the select dropdown: | title | | You can still see my posts | Scenario: Blocked users can still see my posts - Given I previously created a post - And I block the user "Harassing User" - And the "blocked" user searches for "previously created" + When I block the user "Harassing User" + And I am logged in as "harassing-user" + And I navigate to page "/" + And I search for "previously created" + And I wait for 3000 milliseconds Then I should see the following posts in the select dropdown: | title | | previously created post | Scenario: Blocked users cannot see they are blocked in their list Given a user has blocked me + And I navigate to page "/" And I navigate to my "Blocked users" settings page Then I should see no users in my blocked users list Scenario: Blocked users should not see link or button to unblock, only blocking users Given a user has blocked me - When I visit the profile page of the annoying user + When I navigate to page "/profile/harassing-user" And I should see the "Follow" button And I should not see "Unblock user" button And I "should not" see "Unblock user" from the content menu in the user info box diff --git a/cypress/integration/User.Block/I_block_the_user_{string}.js b/cypress/integration/User.Block/I_block_the_user_{string}.js new file mode 100644 index 000000000..cde1d96b9 --- /dev/null +++ b/cypress/integration/User.Block/I_block_the_user_{string}.js @@ -0,0 +1,11 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I block the user {string}", name => { + cy.neode() + .first("User", { name }) + .then(blockedUser => { + cy.neode() + .first("User", {id: "id-of-peter-pan"}) + .relateTo(blockedUser, "blocked"); + }); +}); \ No newline at end of file diff --git a/cypress/integration/User.Block/I_should_not_see_{string}_button.js b/cypress/integration/User.Block/I_should_not_see_{string}_button.js new file mode 100644 index 000000000..5bf0b7a68 --- /dev/null +++ b/cypress/integration/User.Block/I_should_not_see_{string}_button.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then('I should not see {string} button', button => { + cy.get('.base-card .action-buttons') + .should('have.length', 1) +}) \ No newline at end of file diff --git a/cypress/integration/User.Block/I_should_see_no_users_in_my_blocked_users_list.js b/cypress/integration/User.Block/I_should_see_no_users_in_my_blocked_users_list.js new file mode 100644 index 000000000..11161ef2f --- /dev/null +++ b/cypress/integration/User.Block/I_should_see_no_users_in_my_blocked_users_list.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I should see no users in my blocked users list", () => { + cy.get('.ds-placeholder') + .should('contain', "So far, you have not blocked anybody.") +}) \ No newline at end of file diff --git a/cypress/integration/User.Block/I_should_see_the_{string}_button.js b/cypress/integration/User.Block/I_should_see_the_{string}_button.js new file mode 100644 index 000000000..373800870 --- /dev/null +++ b/cypress/integration/User.Block/I_should_see_the_{string}_button.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then('I should see the {string} button', button => { + cy.get('.base-card .action-buttons .base-button') + .should('contain', button) +}) \ No newline at end of file diff --git a/cypress/integration/User.Block/I_{string}_see_{string}_from_the_content_menu_in_the_user_info_box.js b/cypress/integration/User.Block/I_{string}_see_{string}_from_the_content_menu_in_the_user_info_box.js new file mode 100644 index 000000000..0f44b5192 --- /dev/null +++ b/cypress/integration/User.Block/I_{string}_see_{string}_from_the_content_menu_in_the_user_info_box.js @@ -0,0 +1,7 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I {string} see {string} from the content menu in the user info box", (condition, link) => { + cy.get(".user-content-menu .base-button").click() + cy.get(".popover .ds-menu-item-link") + .should(condition === 'should' ? 'contain' : 'not.contain', link) +}) \ No newline at end of file diff --git a/cypress/integration/User.Block/a_user_has_blocked_me.js b/cypress/integration/User.Block/a_user_has_blocked_me.js new file mode 100644 index 000000000..d1703407f --- /dev/null +++ b/cypress/integration/User.Block/a_user_has_blocked_me.js @@ -0,0 +1,15 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("a user has blocked me", () => { + cy.neode() + .first("User", { + name: "Peter Pan" + }) + .then(blockedUser => { + cy.neode() + .first("User", { + name: 'Harassing User' + }) + .relateTo(blockedUser, "blocked"); + }); +}); \ No newline at end of file diff --git a/cypress/integration/User.Block/they_should_not_see_the_comment_form.js b/cypress/integration/User.Block/they_should_not_see_the_comment_form.js new file mode 100644 index 000000000..962934994 --- /dev/null +++ b/cypress/integration/User.Block/they_should_not_see_the_comment_form.js @@ -0,0 +1,5 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("they should not see the comment form", () => { + cy.get(".base-card").children().should('not.have.class', 'comment-form') +}) \ No newline at end of file diff --git a/cypress/integration/User.Block/they_should_see_a_text_explaining_why_commenting_is_not_possible.js b/cypress/integration/User.Block/they_should_see_a_text_explaining_why_commenting_is_not_possible.js new file mode 100644 index 000000000..d95f3229c --- /dev/null +++ b/cypress/integration/User.Block/they_should_see_a_text_explaining_why_commenting_is_not_possible.js @@ -0,0 +1,5 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("they should see a text explaining why commenting is not possible", () => { + cy.get('.ds-placeholder').should('contain', "Commenting is not possible at this time on this post.") +}) \ No newline at end of file diff --git a/cypress/integration/User.Mute.feature.broken b/cypress/integration/User.Mute.feature.broken new file mode 100644 index 000000000..1390063f7 --- /dev/null +++ b/cypress/integration/User.Mute.feature.broken @@ -0,0 +1,60 @@ +Feature: Mute a User + As a user + I'd like to have a button to mute another user + To prevent him from seeing and interacting with my contributions + + Background: + Given the following "users" are in the database: + | email | password | id | name | slug | termsAndConditionsAgreedVersion | + | peterpan@example.org | 123 | id-of-peter-pan | Peter Pan | peter-pan | 0.0.4 | + | user@example.org | 123 | annoying-user | Annoying User | annoying-user | 0.0.4 | + Given the following "posts" are in the database: + | id | title | content | authorId | + | im-not-muted | Post that should be seen | cause I'm not muted | id-of-peter-pan | + | bWBjpkTKZp | previously created post | previously-created-post | id-of-peter-pan | + And I am logged in as "peter-pan" + + Scenario: Mute a user + Given I navigate to page "/profile/annoying-user" + When I click on "Mute user" from the content menu in the user info box + And I navigate to my "Muted users" settings page + Then I can see the following table: + | Avatar | Name | + | | Annoying User | + + Scenario: Mute a previously followed user + Given I follow the user "Annoying User" + And "annoying-user" wrote a post "Spam Spam Spam" + When I navigate to page "/profile/annoying-user" + And I click on "Mute user" from the content menu in the user info box + Then the list of posts of this user is empty + And I get removed from his follower collection + + Scenario: Posts of muted users are filtered from search results, users are not + Given "annoying-user" wrote a post "Spam Spam Spam" + When I search for "Spam" + And I wait for 3000 milliseconds + Then I should see the following posts in the select dropdown: + | title | + | Spam Spam Spam | + When I mute the user "Annoying User" + And I refresh the page + And I search for "Anno" + And I wait for 3000 milliseconds + Then the search should not contain posts by the annoying user + But the search should contain the annoying user + But I search for "not muted" + And I wait for 3000 milliseconds + Then I should see the following posts in the select dropdown: + | title | + | Post that should be seen | + + Scenario: Muted users can still see my posts + And I mute the user "Annoying User" + And I am logged in as "annoying-user" + And I navigate to page "/" + And I search for "previously created" + And I wait for 3000 milliseconds + Then I should see the following posts in the select dropdown: + | title | + | previously created post | diff --git a/cypress/integration/User.Mute/I_mute_the_user_{string}.js b/cypress/integration/User.Mute/I_mute_the_user_{string}.js new file mode 100644 index 000000000..e0ed382cb --- /dev/null +++ b/cypress/integration/User.Mute/I_mute_the_user_{string}.js @@ -0,0 +1,13 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I mute the user {string}", name => { + cy.neode() + .first("User", { name }) + .then(mutedUser => { + cy.neode() + .first("User", { + name: "Peter Pan" + }) + .relateTo(mutedUser, "muted"); + }); +}); \ No newline at end of file diff --git a/cypress/integration/User.Mute/the_list_of_posts_of_this_user_is_empty.js b/cypress/integration/User.Mute/the_list_of_posts_of_this_user_is_empty.js new file mode 100644 index 000000000..038ca2168 --- /dev/null +++ b/cypress/integration/User.Mute/the_list_of_posts_of_this_user_is_empty.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("the list of posts of this user is empty", () => { + cy.get(".base-card").not(".post-link"); + cy.get(".main-container").find(".ds-space.hc-empty"); +}); \ No newline at end of file diff --git a/cypress/integration/User.Mute/the_search_should_contain_the_annoying_user.js b/cypress/integration/User.Mute/the_search_should_contain_the_annoying_user.js new file mode 100644 index 000000000..d29eafc2f --- /dev/null +++ b/cypress/integration/User.Mute/the_search_should_contain_the_annoying_user.js @@ -0,0 +1,13 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("the search should contain the annoying user", () => { + cy.get(".searchable-input .ds-select-dropdown") + .should($li => { + expect($li).to.have.length(1); + }) + cy.get(".ds-select-dropdown .user-teaser .slug") + .should("contain", '@annoying-user'); + cy.get(".searchable-input .ds-select input") + .focus() + .type("{esc}"); +}) \ No newline at end of file diff --git a/cypress/integration/User.Mute/the_search_should_not_contain_posts_by_the_annoying_user.js b/cypress/integration/User.Mute/the_search_should_not_contain_posts_by_the_annoying_user.js new file mode 100644 index 000000000..a2f0a01d7 --- /dev/null +++ b/cypress/integration/User.Mute/the_search_should_not_contain_posts_by_the_annoying_user.js @@ -0,0 +1,10 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("the search should not contain posts by the annoying user", () => { + cy.get(".searchable-input .ds-select-dropdown").should($li => { + expect($li).to.have.length(1); + }) + cy.get(".ds-select-dropdown") + .should("not.have.class", '.search-post') + .should("not.contain", 'Spam') +}); \ No newline at end of file diff --git a/cypress/integration/UserProfile.Avatar.feature b/cypress/integration/UserProfile.Avatar.feature new file mode 100644 index 000000000..abb3fea63 --- /dev/null +++ b/cypress/integration/UserProfile.Avatar.feature @@ -0,0 +1,20 @@ +Feature: User profile - Upload avatar image + As a user + I would like to be able to add an avatar image to my profile + So that I can personalize my profile + + Background: + Given the following "users" are in the database: + | email | password | id | name | slug | termsAndConditionsAgreedVersion | + | peterpan@example.org | 123 | id-of-peter-pan | Peter Pan | peter-pan | 0.0.4 | + | user@example.org | 123 | user | User | user | 0.0.4 | + And I am logged in as "peter-pan" + + Scenario: Change my UserProfile Image + And I navigate to page "/profile/peter-pan" + Then I should be able to change my profile picture + + Scenario: Unable to change another user's avatar + Given I am logged in as "user" + And I navigate to page "/profile/peter-pan" + Then I cannot upload a picture \ No newline at end of file diff --git a/cypress/integration/UserProfile.Avatar/I_cannot_upload_a_picture.js b/cypress/integration/UserProfile.Avatar/I_cannot_upload_a_picture.js new file mode 100644 index 000000000..d20a181f2 --- /dev/null +++ b/cypress/integration/UserProfile.Avatar/I_cannot_upload_a_picture.js @@ -0,0 +1,8 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I cannot upload a picture", () => { + cy.get(".base-card") + .children() + .should("not.have.id", "customdropzone") + .should("have.class", "user-avatar"); +}); \ No newline at end of file diff --git a/cypress/integration/UserProfile.Avatar/I_should_be_able_to_change_my_profile_picture.js b/cypress/integration/UserProfile.Avatar/I_should_be_able_to_change_my_profile_picture.js new file mode 100644 index 000000000..f92789ef8 --- /dev/null +++ b/cypress/integration/UserProfile.Avatar/I_should_be_able_to_change_my_profile_picture.js @@ -0,0 +1,17 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I should be able to change my profile picture", () => { + const avatarUpload = "onourjourney.png"; + + cy.fixture(avatarUpload, "base64").then(fileContent => { + cy.get("#customdropzone").upload( + { fileContent, fileName: avatarUpload, mimeType: "image/png" }, + { subjectType: "drag-n-drop", force: true } + ); + }); + cy.get(".profile-avatar img") + .should("have.attr", "src") + .and("contains", "onourjourney"); + cy.contains(".iziToast-message", "Upload successful") + .should("have.length",1); +}); \ No newline at end of file diff --git a/cypress/integration/UserProfile.ChangePassword.feature b/cypress/integration/UserProfile.ChangePassword.feature new file mode 100644 index 000000000..a7eec1cce --- /dev/null +++ b/cypress/integration/UserProfile.ChangePassword.feature @@ -0,0 +1,55 @@ +Feature: User profile - change password + As a user + I want to change my password in my settings + For security, e.g. if I exposed my password by accident + + Login via email and password is a well-known authentication procedure and you + can assure to the server that you are who you claim to be. Either if you + exposed your password by acccident and you want to invalidate the exposed + password or just out of an good habit, you want to change your password. + + Background: + Given the following "users" are in the database: + | email | password | id | name | slug | termsAndConditionsAgreedVersion | + | peterpan@example.org | exposed | id-of-peter-pan | Peter Pan | peter-pan | 0.0.4 | + And I am logged in as "peter-pan" + And I navigate to page "/settings" + And I click on "security menu" + + Scenario: Incorrect Old Password + When I fill the password form with: + | Your old password | incorrect | + | Your new password | secure | + | Confirm new password | secure | + And I submit the form + And I see a "failure toaster" message: + """ + Old password is not correct + """ + + Scenario: Incorrect Password Repeat + When I fill the password form with: + | Your old password | exposed | + | Your new password | secure | + | Confirm new password | eruces | + And I cannot submit the form + + Scenario: Change my password + Given I navigate to page "/settings" + And I click on "security menu" + When I fill the password form with: + | Your old password | exposed | + | Your new password | secure | + | Confirm new password | secure | + And I submit the form + And I see a "success toaster" message: + """ + Password successfully changed! + """ + And I log out + Then I fill in my credentials "peterpan@example.org" "exposed" + And I click on "submit button" + And I cannot login anymore + But I fill in my credentials "peterpan@example.org" "secure" + And I click on "submit button" + And I can login successfully diff --git a/cypress/integration/UserProfile.ChangePassword/I_can_login_successfully.js b/cypress/integration/UserProfile.ChangePassword/I_can_login_successfully.js new file mode 100644 index 000000000..d1a62cc4d --- /dev/null +++ b/cypress/integration/UserProfile.ChangePassword/I_can_login_successfully.js @@ -0,0 +1,7 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I can login successfully", () => { + // cy.reload(); + cy.get(".iziToast-wrapper") + .should("contain", "You are logged in!"); +}); \ No newline at end of file diff --git a/cypress/integration/UserProfile.ChangePassword/I_cannot_login_anymore.js b/cypress/integration/UserProfile.ChangePassword/I_cannot_login_anymore.js new file mode 100644 index 000000000..ff381d891 --- /dev/null +++ b/cypress/integration/UserProfile.ChangePassword/I_cannot_login_anymore.js @@ -0,0 +1,7 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I cannot login anymore", password => { + //cy.reload(); + cy.get(".iziToast-wrapper") + .should("contain", "Incorrect email address or password."); +}); \ No newline at end of file diff --git a/cypress/integration/UserProfile.ChangePassword/I_cannot_submit_the_form.js b/cypress/integration/UserProfile.ChangePassword/I_cannot_submit_the_form.js new file mode 100644 index 000000000..657d38bd8 --- /dev/null +++ b/cypress/integration/UserProfile.ChangePassword/I_cannot_submit_the_form.js @@ -0,0 +1,6 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I cannot submit the form", () => { + cy.get("button[type=submit]") + .should('be.disabled'); +}); \ No newline at end of file diff --git a/cypress/integration/UserProfile.ChangePassword/I_fill_the_password_form_with.js b/cypress/integration/UserProfile.ChangePassword/I_fill_the_password_form_with.js new file mode 100644 index 000000000..69345ecc6 --- /dev/null +++ b/cypress/integration/UserProfile.ChangePassword/I_fill_the_password_form_with.js @@ -0,0 +1,11 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I fill the password form with:", table => { + table = table.rowsHash(); + cy.get("input[id=oldPassword]") + .type(table["Your old password"]) + .get("input[id=password]") + .type(table["Your new password"]) + .get("input[id=passwordConfirmation]") + .type(table["Confirm new password"]); +}); \ No newline at end of file diff --git a/cypress/integration/UserProfile.ChangePassword/I_see_a_{string}_message.js b/cypress/integration/UserProfile.ChangePassword/I_see_a_{string}_message.js new file mode 100644 index 000000000..90ddf0bd3 --- /dev/null +++ b/cypress/integration/UserProfile.ChangePassword/I_see_a_{string}_message.js @@ -0,0 +1,5 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I see a {string} message:", (type, message) => { + cy.contains(message); +}); \ No newline at end of file diff --git a/cypress/integration/UserProfile.ChangePassword/I_submit_the_form copy.js b/cypress/integration/UserProfile.ChangePassword/I_submit_the_form copy.js new file mode 100644 index 000000000..18349cff8 --- /dev/null +++ b/cypress/integration/UserProfile.ChangePassword/I_submit_the_form copy.js @@ -0,0 +1,5 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I submit the form", () => { + cy.get("form").submit(); +}); \ No newline at end of file diff --git a/cypress/integration/UserProfile.NameDescriptionLocation.feature b/cypress/integration/UserProfile.NameDescriptionLocation.feature new file mode 100644 index 000000000..891d98748 --- /dev/null +++ b/cypress/integration/UserProfile.NameDescriptionLocation.feature @@ -0,0 +1,38 @@ +Feature: User profile - name, description and location + As a user + I would like to change my name, add a description and a location + So others can see my name, get some info about me and my location + + Background: + Given the following "users" are in the database: + | email | password | id | name | slug | termsAndConditionsAgreedVersion | + | peterpan@example.org | 123 | id-of-peter-pan | Peter Pan | peter-pan | 0.0.4 | + And I am logged in as "peter-pan" + And I navigate to page "settings" + + Scenario: Change username + When I save "Hansi" as my new name + Then I can see my new name "Hansi" when I click on my profile picture in the top right + When I refresh the page + Then I can see my new name "Hansi" when I click on my profile picture in the top right + + Scenario Outline: I set my location to "" + When I save "" as my location + And I navigate to page "/profile/peter-pan" + Then they can see "" in the info box below my avatar + Examples: Location + | location | type | + | Paris | City | + | Saxony-Anhalt | Region | + | Germany | Country | + + Scenario: Display a description on profile page + Given I have the following self-description: + """ + Ich lebe fettlos, fleischlos, fischlos dahin, fühle mich aber ganz wohl dabei + """ + When I navigate to page "/profile/peter-pan" + Then they can see the following text in the info box below my avatar: + """ + Ich lebe fettlos, fleischlos, fischlos dahin, fühle mich aber ganz wohl dabei + """ diff --git a/cypress/integration/UserProfile.NameDescriptionLocation/I_can_see_my_new_name_{string}_when_I_click_on_my_profile_picture_in_the_top_right.js b/cypress/integration/UserProfile.NameDescriptionLocation/I_can_see_my_new_name_{string}_when_I_click_on_my_profile_picture_in_the_top_right.js new file mode 100644 index 000000000..b9e97a304 --- /dev/null +++ b/cypress/integration/UserProfile.NameDescriptionLocation/I_can_see_my_new_name_{string}_when_I_click_on_my_profile_picture_in_the_top_right.js @@ -0,0 +1,7 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then('I can see my new name {string} when I click on my profile picture in the top right', name => { + cy.get('.avatar-menu').click() // open + cy.get('.avatar-menu-popover').contains(name) + cy.get('.avatar-menu').click() // close again +}) \ No newline at end of file diff --git a/cypress/integration/UserProfile.NameDescriptionLocation/I_have_the_following_self-description.js b/cypress/integration/UserProfile.NameDescriptionLocation/I_have_the_following_self-description.js new file mode 100644 index 000000000..a1bc1c524 --- /dev/null +++ b/cypress/integration/UserProfile.NameDescriptionLocation/I_have_the_following_self-description.js @@ -0,0 +1,12 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When('I have the following self-description:', text => { + cy.get('textarea[id=about]') + .clear() + .type(text) + cy.get('[type=submit]') + .click() + .not('[disabled]') + cy.get('.iziToast-message') + .should('contain', 'Your data was successfully updated') + }) \ No newline at end of file diff --git a/cypress/integration/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_location.js b/cypress/integration/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_location.js new file mode 100644 index 000000000..de5143b9f --- /dev/null +++ b/cypress/integration/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_location.js @@ -0,0 +1,13 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When('I save {string} as my location', location => { + cy.get('input[id=city]').type(location) + cy.get('.ds-select-option') + .contains(location) + .click() + cy.get('[type=submit]') + .click() + .not('[disabled]') + cy.get('.iziToast-message') + .should('contain', 'Your data was successfully updated') + }) \ No newline at end of file diff --git a/cypress/integration/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_new_name.js b/cypress/integration/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_new_name.js new file mode 100644 index 000000000..22e26cbc5 --- /dev/null +++ b/cypress/integration/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_new_name.js @@ -0,0 +1,12 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When('I save {string} as my new name', name => { + cy.get('input[id=name]') + .clear() + .type(name) + cy.get('[type=submit]') + .click() + .not('[disabled]') + cy.get('.iziToast-message') + .should('contain', 'Your data was successfully updated') +}) \ No newline at end of file diff --git a/cypress/integration/UserProfile.NameDescriptionLocation/they_can_see_the_following_text_in_the_info_box_below_my_avatar.js b/cypress/integration/UserProfile.NameDescriptionLocation/they_can_see_the_following_text_in_the_info_box_below_my_avatar.js new file mode 100644 index 000000000..6d375a406 --- /dev/null +++ b/cypress/integration/UserProfile.NameDescriptionLocation/they_can_see_the_following_text_in_the_info_box_below_my_avatar.js @@ -0,0 +1,5 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When('they can see the following text in the info box below my avatar:', text => { + cy.contains(text) +}) \ No newline at end of file diff --git a/cypress/integration/UserProfile.NameDescriptionLocation/they_can_see_{string}_in_the_info_box_below_my_avatar.js b/cypress/integration/UserProfile.NameDescriptionLocation/they_can_see_{string}_in_the_info_box_below_my_avatar.js new file mode 100644 index 000000000..ea328f441 --- /dev/null +++ b/cypress/integration/UserProfile.NameDescriptionLocation/they_can_see_{string}_in_the_info_box_below_my_avatar.js @@ -0,0 +1,5 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then('they can see {string} in the info box below my avatar', location => { + cy.contains(location) +}) \ No newline at end of file diff --git a/cypress/integration/UserProfile.SocialMedia.feature b/cypress/integration/UserProfile.SocialMedia.feature new file mode 100644 index 000000000..5ab1feb25 --- /dev/null +++ b/cypress/integration/UserProfile.SocialMedia.feature @@ -0,0 +1,41 @@ +Feature: User profile - list social media accounts + As a User + I'd like to enter my social media + So I can show them to other users to get in contact + + Background: + Given the following "users" are in the database: + | email | password | id | name | slug | termsAndConditionsAgreedVersion | + | peterpan@example.org | 123 | id-of-peter-pan | Peter Pan | peter-pan | 0.0.4 | + And I am logged in as "peter-pan" + + Scenario: Adding Social Media + When I navigate to page "/settings/my-social-media" + Then I am on page "/settings/my-social-media" + When I add a social media link + Then I see a toaster with "Added social media" + And the new social media link shows up on the page + + Scenario: Other users viewing my Social Media + Given I have added a social media link + When I navigate to page "/profile/peter-pan" + Then they should be able to see my social media links + + Scenario: Deleting Social Media + When I navigate to page "/settings/my-social-media" + Then I am on page "/settings/my-social-media" + Given I have added a social media link + When I delete a social media link + Then I see a toaster with "Deleted social media" + + Scenario: Editing Social Media + When I navigate to page "/settings/my-social-media" + Then I am on page "/settings/my-social-media" + Given I have added a social media link + When I start editing a social media link + Then I can cancel editing + When I start editing a social media link + And I edit and save the link + Then I see a toaster with "Added social media" + And the new url is displayed + But the old url is not displayed diff --git a/cypress/integration/UserProfile.SocialMedia/I_add_a_social_media_link.js b/cypress/integration/UserProfile.SocialMedia/I_add_a_social_media_link.js new file mode 100644 index 000000000..9253709f9 --- /dev/null +++ b/cypress/integration/UserProfile.SocialMedia/I_add_a_social_media_link.js @@ -0,0 +1,9 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When('I add a social media link', () => { + cy.get('input#addSocialMedia') + .type('https://freeradical.zone/peter-pan') + .get('button') + .contains('Add link') + .click() +}) \ No newline at end of file diff --git a/cypress/integration/UserProfile.SocialMedia/I_can_cancel_editing.js b/cypress/integration/UserProfile.SocialMedia/I_can_cancel_editing.js new file mode 100644 index 000000000..03d60c44a --- /dev/null +++ b/cypress/integration/UserProfile.SocialMedia/I_can_cancel_editing.js @@ -0,0 +1,8 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then('I can cancel editing', () => { + cy.get('button#cancel') + .click() + .get('input#editSocialMedia') + .should('have.length', 0) +}) \ No newline at end of file diff --git a/cypress/integration/UserProfile.SocialMedia/I_delete_a_social_media_link.js b/cypress/integration/UserProfile.SocialMedia/I_delete_a_social_media_link.js new file mode 100644 index 000000000..10daffca1 --- /dev/null +++ b/cypress/integration/UserProfile.SocialMedia/I_delete_a_social_media_link.js @@ -0,0 +1,6 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When('I delete a social media link', () => { + cy.get(".base-button[title='Delete']") + .click() +}) \ No newline at end of file diff --git a/cypress/integration/UserProfile.SocialMedia/I_edit_and_save_the_link.js b/cypress/integration/UserProfile.SocialMedia/I_edit_and_save_the_link.js new file mode 100644 index 000000000..714e6b701 --- /dev/null +++ b/cypress/integration/UserProfile.SocialMedia/I_edit_and_save_the_link.js @@ -0,0 +1,10 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When('I edit and save the link', () => { + cy.get('input#editSocialMedia') + .clear() + .type('https://freeradical.zone/tinkerbell') + .get('button') + .contains('Save') + .click() +}) \ No newline at end of file diff --git a/cypress/integration/UserProfile.SocialMedia/I_have_added_a_social_media_link.js b/cypress/integration/UserProfile.SocialMedia/I_have_added_a_social_media_link.js new file mode 100644 index 000000000..203b97032 --- /dev/null +++ b/cypress/integration/UserProfile.SocialMedia/I_have_added_a_social_media_link.js @@ -0,0 +1,10 @@ +import { Given } from "cypress-cucumber-preprocessor/steps"; + +Given('I have added a social media link', () => { + cy.visit('/settings/my-social-media') + .get('input#addSocialMedia') + .type('https://freeradical.zone/peter-pan') + .get('button') + .contains('Add link') + .click() +}) \ No newline at end of file diff --git a/cypress/integration/UserProfile.SocialMedia/I_start_editing_a_social_media_link.js b/cypress/integration/UserProfile.SocialMedia/I_start_editing_a_social_media_link.js new file mode 100644 index 000000000..1da05cfa5 --- /dev/null +++ b/cypress/integration/UserProfile.SocialMedia/I_start_editing_a_social_media_link.js @@ -0,0 +1,6 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When('I start editing a social media link', () => { + cy.get(".base-button[title='Edit']") + .click() +}) \ No newline at end of file diff --git a/cypress/integration/UserProfile.SocialMedia/the_new_social_media_link_shows_up_on_the_page.js b/cypress/integration/UserProfile.SocialMedia/the_new_social_media_link_shows_up_on_the_page.js new file mode 100644 index 000000000..e72546f2a --- /dev/null +++ b/cypress/integration/UserProfile.SocialMedia/the_new_social_media_link_shows_up_on_the_page.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then('the new social media link shows up on the page', () => { + cy.get('a[href="https://freeradical.zone/peter-pan"]') + .should('have.length', 1) +}) \ No newline at end of file diff --git a/cypress/integration/UserProfile.SocialMedia/the_new_url_is_displayed.js b/cypress/integration/UserProfile.SocialMedia/the_new_url_is_displayed.js new file mode 100644 index 000000000..c25e6f0bb --- /dev/null +++ b/cypress/integration/UserProfile.SocialMedia/the_new_url_is_displayed.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then('the new url is displayed', () => { + cy.get("a[href='https://freeradical.zone/tinkerbell']") + .should('have.length', 1) +}) \ No newline at end of file diff --git a/cypress/integration/UserProfile.SocialMedia/the_old_url_is_not_displayed.js b/cypress/integration/UserProfile.SocialMedia/the_old_url_is_not_displayed.js new file mode 100644 index 000000000..b3e804124 --- /dev/null +++ b/cypress/integration/UserProfile.SocialMedia/the_old_url_is_not_displayed.js @@ -0,0 +1,7 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then('the old url is not displayed', () => { + cy.get("a[href='https://freeradical.zone/peter-pan']") + .should('have.length', 0) +}) + \ No newline at end of file diff --git a/cypress/integration/UserProfile.SocialMedia/they_should_be_able_to_see_my_social_media_links.js b/cypress/integration/UserProfile.SocialMedia/they_should_be_able_to_see_my_social_media_links.js new file mode 100644 index 000000000..249e4f420 --- /dev/null +++ b/cypress/integration/UserProfile.SocialMedia/they_should_be_able_to_see_my_social_media_links.js @@ -0,0 +1,8 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then('they should be able to see my social media links', () => { + cy.get('.base-card') + .contains('Where else can I find Peter Pan?') + .get('a[href="https://freeradical.zone/peter-pan"]') + .should('have.length', 1) +}) \ No newline at end of file diff --git a/cypress/integration/administration/PinPost.feature b/cypress/integration/administration/PinPost.feature deleted file mode 100644 index 40ff9cda5..000000000 --- a/cypress/integration/administration/PinPost.feature +++ /dev/null @@ -1,36 +0,0 @@ -Feature: Pin a post - As an admin - I want to pin a post so that it always appears at the top - In order to make sure all network users read it - e.g. notify people about security incidents, maintenance downtimes - - - Background: - Given we have the following posts in our database: - | id | title | pinned | createdAt | - | p1 | Some other post | | 2020-01-21 | - | p2 | Houston we have a problem | x | 2020-01-20 | - | p3 | Yet another post | | 2020-01-19 | - - Scenario: Pinned post always appears on the top of the newsfeed - Given I am logged in with a "user" role - Then the first post on the landing page has the title: - """ - Houston we have a problem - """ - And the post with title "Houston we have a problem" has a ribbon for pinned posts - - Scenario: Ordinary users cannot pin a post - Given I am logged in with a "user" role - When I open the content menu of post "Yet another post" - Then there is no button to pin a post - - Scenario: Admins are allowed to pin a post - Given I am logged in with a "admin" role - And I open the content menu of post "Yet another post" - When I click on 'Pin post' - Then I see a toaster with "Post pinned successfully" - And the first post on the landing page has the title: - """ - Yet another post - """ - And the post with title "Yet another post" has a ribbon for pinned posts diff --git a/cypress/integration/administration/TagsAndCategories.feature b/cypress/integration/administration/TagsAndCategories.feature deleted file mode 100644 index 516966c6b..000000000 --- a/cypress/integration/administration/TagsAndCategories.feature +++ /dev/null @@ -1,36 +0,0 @@ -Feature: Tags and Categories - As a database administrator - I would like to see a summary of all tags and categories and their usage - In order to be able to decide which tags and categories are popular or not - - The currently deployed application, codename "Alpha", distinguishes between - categories and tags. Each post can have a number of categories and/or tags. - A few categories are required for each post, tags are completely optional. - Both help to find relevant posts in the database, e.g. users can filter for - categories. - - If administrators summary of all tags and categories and how often they are - used, they learn what new category might be convenient for users, e.g. by - looking at the popularity of a tag. - - Background: - Given I am logged in with a "admin" role - And we have a selection of tags and categories as well as posts - - Scenario: See an overview of categories - When I navigate to the administration dashboard - And I click on the menu item "Categories" - Then I can see the following table: - | | Name | Posts | - | | Just For Fun | 2 | - | | Happiness & Values | 1 | - | | Health & Wellbeing | 1 | - - Scenario: See an overview of tags - When I navigate to the administration dashboard - And I click on the menu item "Hashtags" - Then I can see the following table: - | No. | Hashtags | Users | Posts | - | 1 | #Democracy | 3 | 4 | - | 2 | #Nature | 2 | 3 | - | 3 | #Ecology | 1 | 1 | diff --git a/cypress/integration/common/I_am_logged_in_as_{string}.js b/cypress/integration/common/I_am_logged_in_as_{string}.js new file mode 100644 index 000000000..96d1c28ab --- /dev/null +++ b/cypress/integration/common/I_am_logged_in_as_{string}.js @@ -0,0 +1,18 @@ +import { Given } from "cypress-cucumber-preprocessor/steps"; +import encode from '../../../backend/src/jwt/encode' + +Given("I am logged in as {string}", slug => { + cy.neode() + .first("User", { slug }) + .then(user => { + return new Cypress.Promise((resolve, reject) => { + if(!user) { + return reject(`User ${email} not found in database`) + } + return user.toJson().then((user) => resolve(user)) + }) + }) + .then(user => { + cy.setCookie('ocelot-social-token', encode(user)) + }) +}); \ No newline at end of file diff --git a/cypress/integration/common/I_am_on_page_{string}.js b/cypress/integration/common/I_am_on_page_{string}.js new file mode 100644 index 000000000..5ef1b9852 --- /dev/null +++ b/cypress/integration/common/I_am_on_page_{string}.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I am on page {string}", page => { + cy.location("pathname") + .should("match", new RegExp(page)); +}); \ No newline at end of file diff --git a/cypress/integration/common/I_can_see_the_following_table.js b/cypress/integration/common/I_can_see_the_following_table.js new file mode 100644 index 000000000..9ebe1208c --- /dev/null +++ b/cypress/integration/common/I_can_see_the_following_table.js @@ -0,0 +1,16 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then('I can see the following table:', table => { + const headers = table.raw()[0] + headers.forEach((expected, i) => { + cy.get('thead th') + .eq(i) + .should('contain', expected) + }) + const flattened = [].concat.apply([], table.rows()) + flattened.forEach((expected, i) => { + cy.get('tbody td') + .eq(i) + .should('contain', expected) + }) +}) \ No newline at end of file diff --git a/cypress/integration/common/I_choose_the_following_text_as_content.js b/cypress/integration/common/I_choose_the_following_text_as_content.js new file mode 100644 index 000000000..62b5426d5 --- /dev/null +++ b/cypress/integration/common/I_choose_the_following_text_as_content.js @@ -0,0 +1,9 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I choose the following text as content:", async text => { + cy.task('getValue', 'lastPost').then(lastPost => { + lastPost.content = text.replace("\n", " "); + cy.task('pushValue', { name: 'lastPost', value: lastPost }) + cy.get(".editor .ProseMirror").type(lastPost.content); + }) +}); \ No newline at end of file diff --git a/cypress/integration/common/I_click_on_{string}.js b/cypress/integration/common/I_click_on_{string}.js new file mode 100644 index 000000000..5f43eb912 --- /dev/null +++ b/cypress/integration/common/I_click_on_{string}.js @@ -0,0 +1,19 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I click on {string}", element => { + const elementSelectors = { + 'submit button': 'button[name=submit]', + 'create post button': '.post-add-button', + 'save button': 'button[type=submit]', + 'the first post': '.post-teaser:first-child', + 'comment button': 'button[type=submit]', + 'reply button': '.reply-button', + 'security menu': 'a[href="/settings/security"]', + 'pin post': '.ds-menu-item:first-child', + 'Moderation': 'a[href="/moderation"]', + } + + cy.get(elementSelectors[element]) + .click() + .wait(750); +}); \ No newline at end of file diff --git a/cypress/integration/common/I_click_on_{string}_from_the_content_menu_in_the_user_info_box.js b/cypress/integration/common/I_click_on_{string}_from_the_content_menu_in_the_user_info_box.js new file mode 100644 index 000000000..f1a859bfe --- /dev/null +++ b/cypress/integration/common/I_click_on_{string}_from_the_content_menu_in_the_user_info_box.js @@ -0,0 +1,12 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I click on {string} from the content menu in the user info box", + button => { + cy.get(".user-content-menu .base-button").click(); + cy.get(".popover .ds-menu-item-link") + .contains(button) + .click({ + force: true + }); + } +); \ No newline at end of file diff --git a/cypress/integration/common/I_fill_in_my_credentials_{string}_{string}.js b/cypress/integration/common/I_fill_in_my_credentials_{string}_{string}.js new file mode 100644 index 000000000..e2227f454 --- /dev/null +++ b/cypress/integration/common/I_fill_in_my_credentials_{string}_{string}.js @@ -0,0 +1,12 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I fill in my credentials {string} {string}", (email,password) => { + cy.get("input[name=email]") + .trigger("focus") + .type('{selectall}{backspace}') + .type(email) + .get("input[name=password]") + .trigger("focus") + .type('{selectall}{backspace}') + .type(password); +}); \ No newline at end of file diff --git a/cypress/integration/common/I_follow_the_user_{string}.js b/cypress/integration/common/I_follow_the_user_{string}.js new file mode 100644 index 000000000..56d50a5ae --- /dev/null +++ b/cypress/integration/common/I_follow_the_user_{string}.js @@ -0,0 +1,11 @@ +Given("I follow the user {string}", name => { + cy.neode() + .first("User", {name}) + .then(followed => { + cy.neode() + .first("User", { + name: "Peter Pan" + }) + .relateTo(followed, "following"); + }); +}); \ No newline at end of file diff --git a/cypress/integration/common/I_get_removed_from_his_follower_collection.js b/cypress/integration/common/I_get_removed_from_his_follower_collection.js new file mode 100644 index 000000000..b32ca5961 --- /dev/null +++ b/cypress/integration/common/I_get_removed_from_his_follower_collection.js @@ -0,0 +1,8 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I get removed from his follower collection", () => { + cy.get(".base-card") + .not(".post-link"); + cy.get(".main-container") + .contains(".base-card","is not followed by anyone"); + }); \ No newline at end of file diff --git a/cypress/integration/common/I_log_out.js b/cypress/integration/common/I_log_out.js new file mode 100644 index 000000000..51605f17e --- /dev/null +++ b/cypress/integration/common/I_log_out.js @@ -0,0 +1,9 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I log out", () => { + cy.get(".avatar-menu") + .click(); + cy.get(".avatar-menu-popover") + .find('a[href="/logout"]') + .click(); +}); \ No newline at end of file diff --git a/cypress/integration/common/I_navigate_to_my_{string}_settings_page.js b/cypress/integration/common/I_navigate_to_my_{string}_settings_page.js new file mode 100644 index 000000000..4d369eab2 --- /dev/null +++ b/cypress/integration/common/I_navigate_to_my_{string}_settings_page.js @@ -0,0 +1,10 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I navigate to my {string} settings page", settingsPage => { + cy.get(".avatar-menu-trigger").click(); + cy.get(".avatar-menu-popover") + .find("a[href]") + .contains("Settings") + .click(); + cy.contains(".ds-menu-item-link", settingsPage).click(); +}); \ No newline at end of file diff --git a/cypress/integration/common/I_navigate_to_page_{string}.js b/cypress/integration/common/I_navigate_to_page_{string}.js new file mode 100644 index 000000000..aa929c80a --- /dev/null +++ b/cypress/integration/common/I_navigate_to_page_{string}.js @@ -0,0 +1,5 @@ +import { Given } from "cypress-cucumber-preprocessor/steps"; + +Given("I navigate to page {string}", page => { + cy.visit(page); +}); \ No newline at end of file diff --git a/cypress/integration/common/I_refresh_the_page.js b/cypress/integration/common/I_refresh_the_page.js new file mode 100644 index 000000000..1ac655cb4 --- /dev/null +++ b/cypress/integration/common/I_refresh_the_page.js @@ -0,0 +1,6 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When('I refresh the page', () => { + cy.visit('/') + .reload(); +}); \ No newline at end of file diff --git a/cypress/integration/common/I_search_for_{string}.js b/cypress/integration/common/I_search_for_{string}.js new file mode 100644 index 000000000..eaad481f7 --- /dev/null +++ b/cypress/integration/common/I_search_for_{string}.js @@ -0,0 +1,12 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I search for {string}", value => { + cy.intercept({ + method: "POST", + url: "http://localhost:3000/api", + }).as("graphqlRequest"); + cy.get(".searchable-input .ds-select input") + .focus() + .type(value); + cy.wait("@graphqlRequest"); +}); \ No newline at end of file diff --git a/cypress/integration/common/I_see_a_toaster_with_{string}.js b/cypress/integration/common/I_see_a_toaster_with_{string}.js new file mode 100644 index 000000000..1cf7da285 --- /dev/null +++ b/cypress/integration/common/I_see_a_toaster_with_{string}.js @@ -0,0 +1,5 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I see a toaster with {string}", (title) => { + cy.get(".iziToast-message").should("contain", title); +}) \ No newline at end of file diff --git a/cypress/integration/common/I_should_see_the_following_posts_in_the_select_dropdown.js b/cypress/integration/common/I_should_see_the_following_posts_in_the_select_dropdown.js new file mode 100644 index 000000000..420c3376a --- /dev/null +++ b/cypress/integration/common/I_should_see_the_following_posts_in_the_select_dropdown.js @@ -0,0 +1,8 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I should see the following posts in the select dropdown:", table => { + table.hashes().forEach(({ title }) => { + cy.get(".ds-select-dropdown") + .should("contain", title); + }); +}); \ No newline at end of file diff --git a/cypress/integration/common/I_wait_for_{int}_milliseconds.js b/cypress/integration/common/I_wait_for_{int}_milliseconds.js new file mode 100644 index 000000000..bc8ef906a --- /dev/null +++ b/cypress/integration/common/I_wait_for_{int}_milliseconds.js @@ -0,0 +1,5 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("I wait for {int} milliseconds", time => { + cy.wait(time) +}); \ No newline at end of file diff --git a/cypress/integration/common/admin.js b/cypress/integration/common/admin.js deleted file mode 100644 index 346fe64fb..000000000 --- a/cypress/integration/common/admin.js +++ /dev/null @@ -1,21 +0,0 @@ -import { When, Then } from 'cypress-cucumber-preprocessor/steps' - -/* global cy */ - -When('I navigate to the administration dashboard', () => { - cy.get('.avatar-menu').click() - cy.get('.avatar-menu-popover') - .find('a[href="/admin"]') - .click() -}) - -Then('I can see the following table:', table => { - const headers = table.raw()[0] - headers.forEach((expected, i) => { - cy.get('thead th').eq(i).should('contain', expected) - }) - const flattened = [].concat.apply([], table.rows()) - flattened.forEach((expected, i) => { - cy.get('tbody td').eq(i).should('contain', expected) - }) -}) diff --git a/cypress/integration/common/post.js b/cypress/integration/common/post.js deleted file mode 100644 index cba238a63..000000000 --- a/cypress/integration/common/post.js +++ /dev/null @@ -1,165 +0,0 @@ -import { When, Then } from "cypress-cucumber-preprocessor/steps"; -import locales from '../../../webapp/locales' -import orderBy from 'lodash/orderBy' - -const languages = orderBy(locales, 'name') - -When("I type in a comment with {int} characters", size => { - var c=""; - for (var i = 0; i < size; i++) { - c += "c" - } - cy.get(".editor .ProseMirror").type(c); -}); - -Then("I click on the {string} button", text => { - cy.get("button") - .contains(text) - .click(); -}); - -Then("I click on the reply button", () => { - cy.get(".reply-button") - .click(); -}); - -Then("my comment should be successfully created", () => { - cy.get(".iziToast-message").contains("Comment submitted!"); -}); - -Then("I should see my comment", () => { - cy.get("article.comment-card p") - .should("contain", "Human Connection rocks") - .get(".user-teaser span.slug") - .should("contain", "@peter-pan") // specific enough - .get(".user-avatar img") - .should("have.attr", "src") - .and("contain", 'https://') // some url - .get(".user-teaser > .info > .text") - .should("contain", "today at"); -}); - -Then("I should see the entirety of my comment", () => { - cy.get("article.comment-card") - .should("not.contain", "show more") -}); - -Then("I should see an abreviated version of my comment", () => { - cy.get("article.comment-card") - .should("contain", "show more") -}); - -Then("the editor should be cleared", () => { - cy.get(".ProseMirror p").should("have.class", "is-empty"); -}); - -Then("it should create a mention in the CommentForm", () => { - cy.get(".ProseMirror a") - .should('have.class', 'mention') - .should('contain', '@peter-pan') -}) - -When("I open the content menu of post {string}", (title)=> { - cy.contains('.post-teaser', title) - .find('.content-menu .base-button') - .click() -}) - -When("I click on 'Pin post'", (string)=> { - cy.get("a.ds-menu-item-link").contains("Pin post") - .click() -}) - -Then("there is no button to pin a post", () => { - cy.get("a.ds-menu-item-link") - .should('contain', "Report Post") // sanity check - .should('not.contain', "Pin post") -}) - -And("the post with title {string} has a ribbon for pinned posts", (title) => { - cy.get(".post-teaser").contains(title) - .parent() - .parent() - .find(".ribbon.--pinned") - .should("contain", "Announcement") -}) - -Then("I see a toaster with {string}", (title) => { - cy.get(".iziToast-message").should("contain", title); -}) - -Then("I should be able to {string} a teaser image", condition => { - cy.reload() - const teaserImageUpload = (condition === 'change') ? "humanconnection.png" : "onourjourney.png"; - cy.fixture(teaserImageUpload).as('postTeaserImage').then(function() { - cy.get("#postdropzone").upload( - { fileContent: this.postTeaserImage, fileName: teaserImageUpload, mimeType: "image/png" }, - { subjectType: "drag-n-drop", force: true } - ); - }) -}) - -Then('confirm crop', () => { - cy.get('.crop-confirm') - .click() -}) - -Then("I add all required fields", () => { - cy.get('input[name="title"]') - .type('new post') - .get(".editor .ProseMirror") - .type('new post content') - .get(".categories-select .base-button") - .first() - .click() - .get('.base-card > .select-field input') - .click() - .get('.ds-select-option') - .eq(languages.findIndex(l => l.code === 'en')) - .click() -}) - -Then("the post was saved successfully with the {string} teaser image", condition => { - cy.get(".base-card > .title") - .should("contain", condition === 'updated' ? 'to be updated' : 'new post') - .get(".content") - .should("contain", condition === 'updated' ? 'successfully updated' : 'new post content') - .get('.post-page img') - .should("have.attr", "src") - .and("contains", condition === 'updated' ? 'humanconnection' : 'onourjourney') -}) - -Then("the first image should not be displayed anymore", () => { - cy.get(".hero-image") - .children() - .get('.hero-image > .image') - .should('have.length', 1) - .and('have.attr', 'src') -}) - -Then('the {string} post was saved successfully without a teaser image', condition => { - cy.get(".base-card > .title") - .should("contain", condition === 'updated' ? 'to be updated' : 'new post') - .get(".content") - .should("contain", condition === 'updated' ? 'successfully updated' : 'new post content') - .get('.post-page') - .should('exist') - .get('.hero-image > .image') - .should('not.exist') -}) - -Then('I should be able to remove it', () => { - cy.get('.crop-cancel') - .click() -}) - -When('my post has a teaser image', () => { - cy.get('.contribution-form .image') - .should('exist') - .and('have.attr', 'src') -}) - -Then('I should be able to remove the image', () => { - cy.get('.dz-message > .base-button') - .click() -}) diff --git a/cypress/integration/common/profile.js b/cypress/integration/common/profile.js deleted file mode 100644 index a0be8a2cf..000000000 --- a/cypress/integration/common/profile.js +++ /dev/null @@ -1,36 +0,0 @@ -import { When, Then } from "cypress-cucumber-preprocessor/steps"; - -/* global cy */ - -When("I visit my profile page", () => { - cy.openPage("profile/peter-pan"); -}); - -Then("I should be able to change my profile picture", () => { - const avatarUpload = "onourjourney.png"; - - cy.fixture(avatarUpload, "base64").then(fileContent => { - cy.get("#customdropzone").upload( - { fileContent, fileName: avatarUpload, mimeType: "image/png" }, - { subjectType: "drag-n-drop", force: true } - ); - }); - cy.get(".profile-avatar img") - .should("have.attr", "src") - .and("contains", "onourjourney"); - cy.contains(".iziToast-message", "Upload successful").should( - "have.length", - 1 - ); -}); - -When("I visit another user's profile page", () => { - cy.openPage("profile/peter-pan"); -}); - -Then("I cannot upload a picture", () => { - cy.get(".base-card") - .children() - .should("not.have.id", "customdropzone") - .should("have.class", "user-avatar"); -}); diff --git a/cypress/integration/common/report.js b/cypress/integration/common/report.js deleted file mode 100644 index 4c6d2f6c3..000000000 --- a/cypress/integration/common/report.js +++ /dev/null @@ -1,182 +0,0 @@ -import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps' -import { VERSION } from '../../constants/terms-and-conditions-version.js' -import { gql } from '../../../backend/src/helpers/jest' - -/* global cy */ - -let lastReportTitle -let davidIrvingPostTitle = 'The Truth about the Holocaust' -let davidIrvingPostSlug = 'the-truth-about-the-holocaust' -let annoyingUserWhoMutedModeratorTitle = 'Fake news' - -const savePostTitle = $post => { - return $post - .first() - .find('.title') - .first() - .invoke('text') - .then(title => { - lastReportTitle = title - }) -} - -Given("I see David Irving's post on the landing page", page => { - cy.openPage('landing') -}) - -Given("I see David Irving's post on the post page", page => { - cy.visit(`/post/${davidIrvingPostSlug}`) - cy.contains(davidIrvingPostTitle) // wait -}) - -Given('I am logged in with a {string} role', role => { - cy.factory().build('user', { - termsAndConditionsAgreedVersion: VERSION, - role, - name: `${role} is my name` - }, { - email: `${role}@example.org`, - password: '1234', - }) - cy.neode() - .first("User", { - name: `${role} is my name`, - }) - .then(user => { - return new Cypress.Promise((resolve, reject) => { - return user.toJson().then((user) => resolve(user)) - }) - }) - .then(user => cy.login(user)) -}) - -When('I click on "Report Post" from the content menu of the post', () => { - cy.contains('.base-card', davidIrvingPostTitle) - .find('.content-menu .base-button') - .click({force: true}) - - cy.get('.popover .ds-menu-item-link') - .contains('Report Post') - .click() -}) - -When('I click on "Report User" from the content menu in the user info box', () => { - cy.contains('.base-card', davidIrvingPostTitle) - .get('.user-content-menu .base-button') - .click({ force: true }) - - cy.get('.popover .ds-menu-item-link') - .contains('Report User') - .click() -}) - -When('I click on the author', () => { - cy.get('.user-teaser') - .click() - .url().should('include', '/profile/') -}) - -When('I report the author', () => { - cy.get('.page-name-profile-id-slug').then(() => { - invokeReportOnElement('.base-card').then(() => { - cy.get('button') - .contains('Send') - .click() - }) - }) -}) - -When('I click on send in the confirmation dialog', () => { - cy.get('button') - .contains('Send') - .click() -}) - -Then('I get a success message', () => { - cy.get('.iziToast-message').contains('Thanks') -}) - -Then('I see my reported user', () => { - cy.get('table').then(() => { - cy.get('tbody tr') - .first() - .contains(lastReportTitle.trim()) - }) -}) - -Then(`I can't see the moderation menu item`, () => { - cy.get('.avatar-menu-popover') - .find('a[href="/settings"]', 'Settings') - .should('exist') // OK, the dropdown is actually open - - cy.get('.avatar-menu-popover') - .find('a[href="/moderation"]', 'Moderation') - .should('not.exist') -}) - -When(/^I confirm the reporting dialog .*:$/, message => { - cy.contains(message) // wait for element to become visible - cy.get('.ds-modal').within(() => { - cy.get('.ds-radio-option-label') - .first() - .click({ - force: true - }) - cy.get('button') - .contains('Report') - .click() - }) -}) - -Given('somebody reported the following posts:', table => { - table.hashes().forEach(({ submitterEmail, resourceId, reasonCategory, reasonDescription }) => { - const submitter = { - email: submitterEmail, - password: '1234' - } - cy.factory() - .build('user', {}, submitter) - .authenticateAs(submitter) - .mutate(gql`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { - fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) { - reportId - } - }`, { - resourceId, - reasonCategory, - reasonDescription - }) - }) -}) - -Then('I see all the reported posts including the one from above', () => { - cy.get('table tbody').within(() => { - cy.contains('tr', davidIrvingPostTitle) - }) -}) - -Then('I see all the reported posts including from the user who muted me', () => { - cy.get('table tbody').within(() => { - cy.contains('tr', annoyingUserWhoMutedModeratorTitle) - }) -}) - -Then('each list item links to the post page', () => { - cy.contains(davidIrvingPostTitle).click() - cy.location('pathname').should('contain', '/post') -}) - -Then('I can visit the post page', () => { - cy.contains(annoyingUserWhoMutedModeratorTitle).click() - cy.location('pathname').should('contain', '/post') - .get('.base-card .title').should('contain', annoyingUserWhoMutedModeratorTitle) -}) - -When("they have a post someone has reported", () => { - cy.factory() - .build("post", { - title, - }, { - authorId: 'annnoying-user', - }); -}) diff --git a/cypress/integration/common/search.js b/cypress/integration/common/search.js deleted file mode 100644 index 5eae20a22..000000000 --- a/cypress/integration/common/search.js +++ /dev/null @@ -1,126 +0,0 @@ -import { When, Then } from "cypress-cucumber-preprocessor/steps"; -When("I search for {string}", value => { - cy.get(".searchable-input .ds-select input") - .focus() - .type(value); -}); - -Then("I should have one item in the select dropdown", () => { - cy.get(".searchable-input .ds-select-dropdown").should($li => { - expect($li).to.have.length(1); - }); -}); - -Then("the search should not contain posts by the annoying user", () => { - cy.get(".searchable-input .ds-select-dropdown").should($li => { - expect($li).to.have.length(1); - }) - cy.get(".ds-select-dropdown") - .should("not.have.class", '.search-post') - .should("not.contain", 'Spam') -}); - -Then("the search should contain the annoying user", () => { - cy.get(".searchable-input .ds-select-dropdown").should($li => { - expect($li).to.have.length(1); - }) - cy.get(".ds-select-dropdown .user-teaser .slug").should("contain", '@spammy-spammer'); - cy.get(".searchable-input .ds-select input") - .focus() - .type("{esc}"); -}) - -Then("I should see the following posts in the select dropdown:", table => { - table.hashes().forEach(({ title }) => { - cy.get(".ds-select-dropdown").should("contain", title); - }); -}); - -Then("I should see the following users in the select dropdown:", table => { - cy.get(".search-heading").should("contain", "Users"); - table.hashes().forEach(({ slug }) => { - cy.get(".ds-select-dropdown").should("contain", slug); - }); -}); - -When("I type {string} and press Enter", value => { - cy.get(".searchable-input .ds-select input") - .focus() - .type(value) - .type("{enter}", { force: true }); -}); - -When("I type {string} and press escape", value => { - cy.get(".searchable-input .ds-select input") - .focus() - .type(value) - .type("{esc}"); -}); - -Then("the search field should clear", () => { - cy.get(".searchable-input .ds-select input").should("have.text", ""); -}); - -When("I select a post entry", () => { - cy.get(".searchable-input .search-post") - .first() - .trigger("click"); -}); - -Then("I should be on the post's page", () => { - cy.location("pathname").should("contain", "/post/"); - cy.location("pathname").should( - "eq", - "/post/p1/101-essays-that-will-change-the-way-you-think" - ); -}); - -Then( - "I should see posts with the searched-for term in the select dropdown", - () => { - cy.get(".ds-select-dropdown").should( - "contain", - "101 Essays that will change the way you think" - ); - } -); - -Then("I should see the search results page", () => { - cy.location("pathname").should( - "eq", - "/search/search-results" - ); - cy.location("search").should( - "eq", - "?search=PR" - ); -}); - -Then("I should see the following posts on the search results page", - () => { - cy.get(".post-teaser").should( - "contain", - "101 Essays that will change the way you think" - ); - } -); - -Then( - "I should not see posts without the searched-for term in the select dropdown", - () => { - cy.get(".ds-select-dropdown").should( - "not.contain", - "No searched for content" - ); - } -); - -Then("I select a user entry", () => { - cy.get(".searchable-input .user-teaser") - .first() - .trigger("click"); -}) - -Then("I should be on the user's profile", () => { - cy.location("pathname").should("eq", "/profile/user-for-search/search-for-me") -}) diff --git a/cypress/integration/common/settings.js b/cypress/integration/common/settings.js deleted file mode 100644 index 3dcff141d..000000000 --- a/cypress/integration/common/settings.js +++ /dev/null @@ -1,160 +0,0 @@ -import { When, Then } from 'cypress-cucumber-preprocessor/steps' - -/* global cy */ - -let aboutMeText -let myLocation - -const matchNameInUserMenu = name => { - cy.get('.avatar-menu').click() // open - cy.get('.avatar-menu-popover').contains(name) - cy.get('.avatar-menu').click() // close again -} - -When('I save {string} as my new name', name => { - cy.get('input[id=name]') - .clear() - .type(name) - cy.get('[type=submit]') - .click() - .not('[disabled]') - cy.get('.iziToast-message') - .should('contain', 'Your data was successfully updated') -}) - -When('I save {string} as my location', location => { - cy.get('input[id=city]').type(location) - cy.get('.ds-select-option') - .contains(location) - .click() - cy.get('[type=submit]') - .click() - .not('[disabled]') - cy.get('.iziToast-message') - .should('contain', 'Your data was successfully updated') - myLocation = location -}) - -When('I have the following self-description:', text => { - cy.get('textarea[id=bio]') - .clear() - .type(text) - cy.get('[type=submit]') - .click() - .not('[disabled]') - cy.get('.iziToast-message') - .should('contain', 'Your data was successfully updated') - aboutMeText = text -}) - -When('people visit my profile page', url => { - cy.openPage('/profile/peter-pan') -}) - - -When('they can see the text in the info box below my avatar', () => { - cy.contains(aboutMeText) -}) - -Then('they can see the location in the info box below my avatar', () => { - cy.contains(myLocation) -}) - -Then('the name {string} is still there', name => { - matchNameInUserMenu(name) -}) - -Then( - 'I can see my new name {string} when I click on my profile picture in the top right', - name => matchNameInUserMenu(name) -) - -When('I click on the {string} link', link => { - cy.get('a') - .contains(link) - .click() -}) - -Then('I should be on the {string} page', page => { - cy.location() - .should(loc => { - expect(loc.pathname).to.eq(page) - }) - .get('h2') - .should('contain', 'Social media') -}) - -When('I add a social media link', () => { - cy.get('input#addSocialMedia') - .type('https://freeradical.zone/peter-pan') - .get('button') - .contains('Add link') - .click() -}) - -Then('it gets saved successfully', () => { - cy.get('.iziToast-message') - .should('contain', 'Added social media') -}) - -Then('the new social media link shows up on the page', () => { - cy.get('a[href="https://freeradical.zone/peter-pan"]') - .should('have.length', 1) -}) - -Given('I have added a social media link', () => { - cy.openPage('/settings/my-social-media') - .get('input#addSocialMedia') - .type('https://freeradical.zone/peter-pan') - .get('button') - .contains('Add link') - .click() -}) - -Then('they should be able to see my social media links', () => { - cy.get('.base-card') - .contains('Where else can I find Peter Pan?') - .get('a[href="https://freeradical.zone/peter-pan"]') - .should('have.length', 1) -}) - -When('I delete a social media link', () => { - cy.get(".base-button[title='Delete']") - .click() -}) - -Then('it gets deleted successfully', () => { - cy.get('.iziToast-message') - .should('contain', 'Deleted social media') -}) - -When('I start editing a social media link', () => { - cy.get(".base-button[title='Edit']") - .click() -}) - -Then('I can cancel editing', () => { - cy.get('button#cancel') - .click() - .get('input#editSocialMedia') - .should('have.length', 0) -}) - -When('I edit and save the link', () => { - cy.get('input#editSocialMedia') - .clear() - .type('https://freeradical.zone/tinkerbell') - .get('button') - .contains('Save') - .click() -}) - -Then('the new url is displayed', () => { - cy.get("a[href='https://freeradical.zone/tinkerbell']") - .should('have.length', 1) -}) - -Then('the old url is not displayed', () => { - cy.get("a[href='https://freeradical.zone/peter-pan']") - .should('have.length', 0) -}) diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js deleted file mode 100644 index 22a9d016e..000000000 --- a/cypress/integration/common/steps.js +++ /dev/null @@ -1,609 +0,0 @@ -import { - Given, - When, - Then -} from "cypress-cucumber-preprocessor/steps"; -import helpers from "../../support/helpers"; -import { VERSION } from '../../constants/terms-and-conditions-version.js' -import locales from '../../../webapp/locales' -import orderBy from 'lodash/orderBy' - -/* global cy */ - -const languages = orderBy(locales, 'name') -let lastPost = {}; - -let loginCredentials = { - email: "peterpan@example.org", - password: "1234" -}; -const termsAndConditionsAgreedVersion = { - termsAndConditionsAgreedVersion: VERSION -}; -const narratorParams = { - id: 'id-of-peter-pan', - name: "Peter Pan", - slug: "peter-pan", - ...termsAndConditionsAgreedVersion, -}; - -const annoyingParams = { - email: "spammy-spammer@example.org", - slug: 'spammy-spammer', - password: "1234", -}; - -Given("I am logged in", () => { - cy.neode() - .first("User", { - name: narratorParams.name - }) - .then(user => { - return new Cypress.Promise((resolve, reject) => { - return user.toJson().then((user) => resolve(user)) - }) - }) - .then(user => cy.login(user)) -}); - -Given("I log in as {string}", name => { - cy.logout() - cy.neode() - .first("User", { - name - }) - .then(user => { - return new Cypress.Promise((resolve, reject) => { - return user.toJson().then((user) => resolve(user)) - }) - }) - .then(user => cy.login(user)) -}) - -Given("the {string} user searches for {string}", (_, postTitle) => { - cy.logout() - cy.neode() - .first("User", { - id: "annoying-user" - }) - .then(user => { - return new Cypress.Promise((resolve, reject) => { - return user.toJson().then((user) => resolve(user)) - }) - }) - .then(user => cy.login(user)) - cy.get(".searchable-input .ds-select input") - .focus() - .type(postTitle); -}); - -Given("we have a selection of categories", () => { - cy.factory().build('category', { id: "cat0", slug: "just-for-fun" }); -}); - -Given("we have a selection of tags and categories as well as posts", () => { - cy.factory() - .build('category', { id: 'cat12', name: "Just For Fun", icon: "smile", }) - .build('category', { id: 'cat121', name: "Happiness & Values", icon: "heart-o"}) - .build('category', { id: 'cat122', name: "Health & Wellbeing", icon: "medkit"}) - .build("tag", { id: "Ecology" }) - .build("tag", { id: "Nature" }) - .build("tag", { id: "Democracy" }) - .build("user", { id: 'a1' }) - .build("post", {}, { - authorId: 'a1', - tagIds: ["Ecology", "Nature", "Democracy"], - categoryIds: ["cat12"] - }) - .build("post", {}, { - authorId: 'a1', - tagIds: ["Nature", "Democracy"], - categoryIds: ["cat121"] - }) - .build("user", { id: 'a2' }) - .build("post", {}, { - authorId: 'a2', - tagIds: ['Nature', 'Democracy'], - categoryIds: ["cat12"] - }) - .build("post", {}, { - tagIds: ['Democracy'], - categoryIds: ["cat122"] - }) -}); - -Given("we have the following user accounts:", table => { - table.hashes().forEach(params => { - cy.factory().build("user", { - ...params, - ...termsAndConditionsAgreedVersion - }, params); - }); -}); - -Given("I have a user account", () => { - cy.factory().build("user", narratorParams, loginCredentials); -}); - -Given("my user account has the role {string}", role => { - cy.factory().build("user", { - role, - ...termsAndConditionsAgreedVersion, - }, loginCredentials); -}); - -When("I log out", cy.logout); - -When("I visit {string}", page => { - cy.openPage(page); -}); - -When("I visit the {string} page", page => { - cy.openPage(page); -}); - -When("a blocked user visits the post page of one of my authored posts", () => { - cy.logout() - cy.neode() - .first("User", { - name: 'Harassing User' - }) - .then(user => { - return new Cypress.Promise((resolve, reject) => { - return user.toJson().then((user) => resolve(user)) - }) - }) - .then(user => cy.login(user)) - cy.openPage('post/previously-created-post') -}) - -Given("I am on the {string} page", page => { - cy.openPage(page); -}); - -When("I fill in my email and password combination and click submit", () => { - cy.manualLogin(loginCredentials); -}); - -When(/(?:when )?I refresh the page/, () => { - cy.visit('/') - .reload(); -}); - -When("I log out through the menu in the top right corner", () => { - cy.get(".avatar-menu").click(); - cy.get(".avatar-menu-popover") - .find('a[href="/logout"]') - .click(); -}); - -Then("I can see my name {string} in the dropdown menu", () => { - cy.get(".avatar-menu-popover").should("contain", narratorParams.name); -}); - -Then("I see the login screen again", () => { - cy.location("pathname").should("contain", "/login"); -}); - -Then("I can click on my profile picture in the top right corner", () => { - cy.get(".avatar-menu").click(); - cy.get(".avatar-menu-popover"); -}); - -Then("I am still logged in", () => { - cy.get(".avatar-menu").click(); - cy.get(".avatar-menu-popover").contains(narratorParams.name); -}); - -When("I select {string} in the language menu", name => { - cy.switchLanguage(name, true); -}); - -Given("I previously switched the language to {string}", name => { - cy.switchLanguage(name, true); -}); - -Then("the whole user interface appears in {string}", name => { - const { - code - } = helpers.getLangByName(name); - cy.get(`html[lang=${code}]`); - cy.getCookie("locale").should("have.property", "value", code); -}); - -Then("I see a button with the label {string}", label => { - cy.contains("button", label); -}); - -When(`I click on {string}`, linkOrButton => { - cy.contains(linkOrButton).click(); -}); - -When(`I click on the menu item {string}`, linkOrButton => { - cy.contains(".ds-menu-item", linkOrButton).click(); -}); - -When("I press {string}", label => { - cy.contains(label).click(); -}); - -Given("we have the following comments in our database:", table => { - table.hashes().forEach((attributesOrOptions, i) => { - cy.factory().build("comment", { - ...attributesOrOptions, - }, { - ...attributesOrOptions, - }); - }) -}); - -Given("we have the following posts in our database:", table => { - table.hashes().forEach((attributesOrOptions, i) => { - cy.factory().build("post", { - ...attributesOrOptions, - deleted: Boolean(attributesOrOptions.deleted), - disabled: Boolean(attributesOrOptions.disabled), - pinned: Boolean(attributesOrOptions.pinned), - }, { - ...attributesOrOptions, - }); - }) -}) - -Then("I see a success message:", message => { - cy.contains(message); -}); - -When("I click on the avatar menu in the top right corner", () => { - cy.get(".avatar-menu").click(); -}); - -When( - "I click on the big plus icon in the bottom right corner to create post", - () => { - cy.get(".post-add-button").click(); - cy.location("pathname").should('eq', '/post/create') - } -); - -Given("I previously created a post", () => { - lastPost = { - lastPost, - title: "previously created post", - content: "with some content", - }; - cy.factory() - .build("post", lastPost, { - authorId: narratorParams.id - }); -}); - -When("I choose {string} as the title of the post", title => { - lastPost.title = title.replace("\n", " "); - cy.get('input[name="title"]').type(lastPost.title); -}); - -When("I type in the following text:", text => { - lastPost.content = text.replace("\n", " "); - cy.get(".editor .ProseMirror").type(lastPost.content); -}); - -Then("I select a category", () => { - cy.get(".base-button") - .contains("Just for Fun") - .click(); -}); - -When("I choose {string} as the language for the post", (languageCode) => { - cy.get('.contribution-form .ds-select') - .click().get('.ds-select-option') - .eq(languages.findIndex(l => l.code === languageCode)).click() -}) - -Then("the post shows up on the landing page at position {int}", index => { - cy.openPage("landing"); - const selector = `.post-teaser:nth-child(${index}) > .base-card`; - cy.get(selector).should("contain", lastPost.title); - cy.get(selector).should("contain", lastPost.content); -}); - -Then("I get redirected to {string}", route => { - cy.location("pathname").should("contain", route.replace("...", "")); -}); - -Then("the post was saved successfully", () => { - cy.get(".base-card > .title").should("contain", lastPost.title); - cy.get(".content").should("contain", lastPost.content); -}); - -Then(/^I should see only ([0-9]+) posts? on the landing page/, postCount => { - cy.get(".post-teaser").should("have.length", postCount); -}); - -Then("the first post on the landing page has the title:", title => { - cy.get(".post-teaser:first").should("contain", title); -}); - -Then( - "the page {string} returns a 404 error with a message:", - (route, message) => { - cy.request({ - url: route, - failOnStatusCode: false - }) - .its("status") - .should("eq", 404); - cy.visit(route, { - failOnStatusCode: false - }); - cy.get(".error-message").should("contain", message); - } -); - -Given("I am logged in with these credentials:", table => { - loginCredentials = table.hashes()[0]; - cy.factory().build("user", { - ...termsAndConditionsAgreedVersion, - name: loginCredentials.email, - }, loginCredentials); - cy.neode() - .first("User", { - name: loginCredentials.email, - }) - .then(user => { - return new Cypress.Promise((resolve, reject) => { - return user.toJson().then((user) => resolve(user)) - }) - }) - .then(user => cy.login(user)) -}); - -When("I fill the password form with:", table => { - table = table.rowsHash(); - cy.get("input[id=oldPassword]") - .type(table["Your old password"]) - .get("input[id=password]") - .type(table["Your new passsword"]) - .get("input[id=passwordConfirmation]") - .type(table["Confirm new password"]); -}); - -When("submit the form", () => { - cy.get("form").submit(); -}); - -Then("I cannot login anymore with password {string}", password => { - cy.reload(); - const { email } = loginCredentials - cy.manualLogin({ email, password }) - .get(".iziToast-wrapper").should("contain", "Incorrect email address or password."); -}); - -Then("I can login successfully with password {string}", password => { - cy.reload(); - const { email } = loginCredentials - cy.manualLogin({ email, password }) - .get(".iziToast-wrapper").should("contain", "You are logged in!"); -}); - -When("open the notification menu and click on the first item", () => { - cy.get(".notifications-menu").invoke('show').click(); // "invoke('show')" because of the delay for show the menu - cy.get(".notification .link") - .first() - .click({ - force: true - }); -}); - -Then("see {int} unread notifications in the top menu", count => { - cy.get(".notifications-menu").should("contain", count); -}); - -Then("I get to the post page of {string}", path => { - path = path.replace("...", ""); - cy.url().should("contain", "/post/"); - cy.url().should("contain", path); -}); - -When( - "I start to write a new post with the title {string} beginning with:", - (title, intro) => { - cy.get(".post-add-button").click(); - cy.get('input[name="title"]').type(title); - cy.get(".ProseMirror").type(intro); - } -); - -When("mention {string} in the text", mention => { - cy.get(".ProseMirror").type(" @"); - cy.get(".suggestion-list__item") - .contains(mention) - .click(); -}); - -Then("the unread counter is removed", () => { - cy.get('.notifications-menu .counter-icon').should('not.exist'); -}); - -Then("the notification menu button links to the all notifications page", () => { - cy.get(".notifications-menu").click(); - cy.location("pathname").should("contain", "/notifications"); -}); - -Given("there is an annoying user called {string}", name => { - cy.factory().build("user", { - id: "annoying-user", - name, - ...termsAndConditionsAgreedVersion, - }, annoyingParams); -}); - -Given("there is an annoying user who has muted me", () => { - cy.neode() - .first("User", { - role: 'moderator' - }) - .then(mutedUser => { - cy.neode() - .first("User", { - id: 'annoying-user' - }) - .relateTo(mutedUser, "muted"); - }); -}); - -Given("I am on the profile page of the annoying user", name => { - cy.openPage("profile/annoying-user/spammy-spammer"); -}); - -When("I visit the profile page of the annoying user", name => { - cy.openPage("profile/annoying-user"); -}); - -When("I ", name => { - cy.openPage("profile/annoying-user"); -}); - -When( - "I click on {string} from the content menu in the user info box", - button => { - cy.get(".user-content-menu .base-button").click(); - cy.get(".popover .ds-menu-item-link") - .contains(button) - .click({ - force: true - }); - } -); - -When("I navigate to my {string} settings page", settingsPage => { - cy.get(".avatar-menu-trigger").click(); - cy.get(".avatar-menu-popover") - .find("a[href]") - .contains("Settings") - .click(); - cy.contains(".ds-menu-item-link", settingsPage).click(); -}); - -Given("I follow the user {string}", name => { - cy.neode() - .first("User", { - name - }) - .then(followed => { - cy.neode() - .first("User", { - name: narratorParams.name - }) - .relateTo(followed, "following"); - }); -}); - -Given('{string} wrote a post {string}', (_, title) => { - cy.factory() - .build("post", { - title, - }, { - authorId: 'annoying-user', - }); -}); - -Then("the list of posts of this user is empty", () => { - cy.get(".base-card").not(".post-link"); - cy.get(".main-container").find(".ds-space.hc-empty"); -}); - -Then("I get removed from his follower collection", () => { - cy.get(".base-card").not(".post-link"); - cy.get(".main-container").contains( - ".base-card", - "is not followed by anyone" - ); -}); - -Given("I wrote a post {string}", title => { - cy.factory() - .build("post", { - title, - }, { - authorId: narratorParams.id, - }); -}); - -When("I mute the user {string}", name => { - cy.neode() - .first("User", { - name - }) - .then(mutedUser => { - cy.neode() - .first("User", { - name: narratorParams.name - }) - .relateTo(mutedUser, "muted"); - }); -}); - -When("I block the user {string}", name => { - cy.neode() - .first("User", { - name - }) - .then(blockedUser => { - cy.neode() - .first("User", { - id: narratorParams.id - }) - .relateTo(blockedUser, "blocked"); - }); -}); - -When("a user has blocked me", () => { - cy.neode() - .first("User", { - name: narratorParams.name - }) - .then(blockedUser => { - cy.neode() - .first("User", { - name: 'Harassing User' - }) - .relateTo(blockedUser, "blocked"); - }); -}); - -Then("I see only one post with the title {string}", title => { - cy.get(".main-container") - .find(".post-link") - .should("have.length", 1); - cy.get(".main-container").contains(".post-link", title); -}); - -Then("they should not see the comment form", () => { - cy.get(".base-card").children().should('not.have.class', 'comment-form') -}) - -Then("they should see a text explaining why commenting is not possible", () => { - cy.get('.ds-placeholder').should('contain', "Commenting is not possible at this time on this post.") -}) - -Then("I should see no users in my blocked users list", () => { - cy.get('.ds-placeholder') - .should('contain', "So far, you have not blocked anybody.") -}) - -Then("I {string} see {string} from the content menu in the user info box", (condition, link) => { - cy.get(".user-content-menu .base-button").click() - cy.get(".popover .ds-menu-item-link") - .should(condition === 'should' ? 'contain' : 'not.contain', link) -}) - -Then('I should not see {string} button', button => { - cy.get('.base-card .action-buttons') - .should('have.length', 1) -}) - -Then('I should see the {string} button', button => { - cy.get('.base-card .action-buttons .base-button') - .should('contain', button) -}) diff --git a/cypress/integration/common/the_first_post_on_the_newsfeed_has_the_title.js b/cypress/integration/common/the_first_post_on_the_newsfeed_has_the_title.js new file mode 100644 index 000000000..afe370e90 --- /dev/null +++ b/cypress/integration/common/the_first_post_on_the_newsfeed_has_the_title.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("the first post on the newsfeed has the title:", title => { + cy.get(".post-teaser:first") + .should("contain", title); +}); \ No newline at end of file diff --git a/cypress/integration/common/the_following_{string}_are_in_the_database.js b/cypress/integration/common/the_following_{string}_are_in_the_database.js new file mode 100644 index 000000000..1d17ec686 --- /dev/null +++ b/cypress/integration/common/the_following_{string}_are_in_the_database.js @@ -0,0 +1,35 @@ +import { Given } from "cypress-cucumber-preprocessor/steps"; + +Given("the following {string} are in the database:", (table,data) => { + switch(table){ + case "posts": + data.hashes().forEach( entry => { + cy.factory().build("post", { + ...entry, + deleted: Boolean(entry.deleted), + disabled: Boolean(entry.disabled), + pinned: Boolean(entry.pinned), + },{ + ...entry, + tagIds: entry.tagIds ? entry.tagIds.split(',').map(item => item.trim()) : [], + }); + }) + break + case "comments": + data.hashes().forEach( entry => { + cy.factory() + .build("comment",entry,entry); + }) + break + case "users": + data.hashes().forEach( entry => { + cy.factory().build("user",entry,entry); + }); + break + case "tags": + data.hashes().forEach( entry => { + cy.factory().build("tag", entry, entry) + }); + break + } +}) \ No newline at end of file diff --git a/cypress/integration/common/{string}_wrote_a_post_{string}.js b/cypress/integration/common/{string}_wrote_a_post_{string}.js new file mode 100644 index 000000000..42ac98028 --- /dev/null +++ b/cypress/integration/common/{string}_wrote_a_post_{string}.js @@ -0,0 +1,10 @@ +import { Given } from "cypress-cucumber-preprocessor/steps"; + +Given('{string} wrote a post {string}', (author, title) => { + cy.factory() + .build("post", { + title, + }, { + authorId: author, + }); +}); \ No newline at end of file diff --git a/cypress/integration/moderation/HidePosts.feature b/cypress/integration/moderation/HidePosts.feature deleted file mode 100644 index bb82c7188..000000000 --- a/cypress/integration/moderation/HidePosts.feature +++ /dev/null @@ -1,26 +0,0 @@ -Feature: Hide Posts - As the moderator team - we'd like to be able to hide posts from the public - to enforce our network's code of conduct and/or legal regulations - - Background: - Given we have the following posts in our database: - | id | title | deleted | disabled | - | p1 | This post should be visible | | | - | p2 | This post is disabled | | x | - | p3 | This post is deleted | x | | - - Scenario: Disabled posts don't show up on the landing page - Given I am logged in with a "user" role - Then I should see only 1 post on the landing page - And the first post on the landing page has the title: - """ - This post should be visible - """ - - Scenario: Visiting a disabled post's page should return 404 - Given I am logged in with a "user" role - Then the page "/post/this-post-is-disabled" returns a 404 error with a message: - """ - This post could not be found - """ diff --git a/cypress/integration/notifications/Mentions.feature b/cypress/integration/notifications/Mentions.feature deleted file mode 100644 index 1cf265624..000000000 --- a/cypress/integration/notifications/Mentions.feature +++ /dev/null @@ -1,29 +0,0 @@ -Feature: Notification for a mention - As a user - I want to be notified if sb. mentions me in a post or comment - In order join conversations about or related to me - - Background: - Given we have a selection of categories - And we have the following user accounts: - | name | slug | email | password | - | Wolle aus Hamburg | wolle-aus-hamburg | wolle@example.org | 1234 | - | Matt Rider | matt-rider | matt@example.org | 4321 | - - Scenario: Mention another user, re-login as this user and see notifications - Given I log in as "Wolle aus Hamburg" - And I start to write a new post with the title "Hey Matt" beginning with: - """ - Big shout to our fellow contributor - """ - And mention "@matt-rider" in the text - And I select a category - And I choose "en" as the language for the post - And I click on "Save" - And I log in as "Matt Rider" - And see 1 unread notifications in the top menu - And open the notification menu and click on the first item - Then I get to the post page of ".../hey-matt" - And the unread counter is removed - And the notification menu button links to the all notifications page - diff --git a/cypress/integration/post/Comment.feature b/cypress/integration/post/Comment.feature deleted file mode 100644 index da261726b..000000000 --- a/cypress/integration/post/Comment.feature +++ /dev/null @@ -1,46 +0,0 @@ -Feature: Post Comment - As a user - I want to comment on contributions of others - To be able to express my thoughts and emotions about these, discuss, and add give further information. - - Background: - Given I have a user account - And we have the following posts in our database: - | id | title | slug | authorId | - | bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays | id-of-peter-pan | - And we have the following comments in our database: - | postId | content | authorId | - | bWBjpkTKZp | @peter-pan reply to me | id-of-peter-pan | - And I am logged in - - Scenario: Comment creation - Given I visit "post/bWBjpkTKZp/101-essays" - And I type in the following text: - """ - Human Connection rocks - """ - And I click on the "Comment" button - Then my comment should be successfully created - And I should see my comment - And the editor should be cleared - - Scenario: View medium length comments - Given I visit "post/bWBjpkTKZp/101-essays" - And I type in a comment with 305 characters - And I click on the "Comment" button - Then my comment should be successfully created - And I should see the entirety of my comment - And the editor should be cleared - - Scenario: View long comments - Given I visit "post/bWBjpkTKZp/101-essays" - And I type in a comment with 1205 characters - And I click on the "Comment" button - Then my comment should be successfully created - And I should see an abreviated version of my comment - And the editor should be cleared - - Scenario: Direct reply to Comment - Given I visit "post/bWBjpkTKZp/101-essays" - And I click on the reply button - Then it should create a mention in the CommentForm diff --git a/cypress/integration/post/DeleteImage.feature b/cypress/integration/post/DeleteImage.feature deleted file mode 100644 index 07bfe43b1..000000000 --- a/cypress/integration/post/DeleteImage.feature +++ /dev/null @@ -1,19 +0,0 @@ -Feature: Delete Teaser Image - As a user - I would like to be able to remove an image I have previously added to my Post - So that I have control over the content of my Post - - Background: - Given I have a user account - Given I am logged in - Given we have the following posts in our database: - | authorId | id | title | content | - | id-of-peter-pan | p1 | Post to be updated | successfully updated | - - Scenario: Delete existing image - Given I am on the 'post/edit/p1' page - And my post has a teaser image - Then I should be able to remove the image - And I click on "Save" - Then I get redirected to ".../post-to-be-updated" - And the "updated" post was saved successfully without a teaser image diff --git a/cypress/integration/post/ImageUploader.feature b/cypress/integration/post/ImageUploader.feature deleted file mode 100644 index 1bbd80c78..000000000 --- a/cypress/integration/post/ImageUploader.feature +++ /dev/null @@ -1,47 +0,0 @@ -Feature: Upload Teaser Image - As a user - I would like to be able to add a teaser image to my Post - So that I can personalize my posts - - - Background: - Given I have a user account - Given I am logged in - Given we have the following posts in our database: - | authorId | id | title | content | - | id-of-peter-pan | p1 | Post to be updated | successfully updated | - - Scenario: Create a Post with a Teaser Image - When I click on the big plus icon in the bottom right corner to create post - Then I should be able to "add" a teaser image - And confirm crop - And I add all required fields - And I click on "Save" - Then I get redirected to ".../new-post" - And the post was saved successfully with the "new" teaser image - - Scenario: Update a Post to add an image - Given I am on the 'post/edit/p1' page - And I should be able to "change" a teaser image - And confirm crop - And I click on "Save" - Then I see a toaster with "Saved!" - And I get redirected to ".../post-to-be-updated" - Then the post was saved successfully with the "updated" teaser image - - Scenario: Add image, then add a different image - When I click on the big plus icon in the bottom right corner to create post - Then I should be able to "add" a teaser image - And confirm crop - And I should be able to "change" a teaser image - And confirm crop - And the first image should not be displayed anymore - - Scenario: Add image, then delete it - When I click on the big plus icon in the bottom right corner to create post - Then I should be able to "add" a teaser image - And I should be able to remove it - And I add all required fields - And I click on "Save" - Then I get redirected to ".../new-post" - And the "new" post was saved successfully without a teaser image diff --git a/cypress/integration/post/PersistentLinks.feature b/cypress/integration/post/PersistentLinks.feature deleted file mode 100644 index 5ea48ef6a..000000000 --- a/cypress/integration/post/PersistentLinks.feature +++ /dev/null @@ -1,41 +0,0 @@ -Feature: Persistent Links - As a user - I want all links to carry permanent information that identifies the linked resource - In order to have persistent links even if a part of the URL might change - - | | Modifiable | Referenceable | Unique | Purpose | - | -- | -- | -- | -- | -- | - | ID | no | yes | yes | Identity, Traceability, Links | - | Slug | yes | yes | yes | @-Mentions, SEO-friendly URL | - | Name | yes | no | no | Search, self-description | - - - Background: - Given we have the following user accounts: - | id | name | slug | - | MHNqce98y1 | Stephen Hawking | thehawk | - And we have the following posts in our database: - | id | title | slug | - | bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays | - And I have a user account - And I am logged in - - Scenario Outline: Link with slug only is valid and gets auto-completed - When I visit "" - Then I get redirected to "" - Examples: - | url | redirectUrl | - | /profile/thehawk | /profile/MHNqce98y1/thehawk | - | /post/101-essays | /post/bWBjpkTKZp/101-essays | - - Scenario: Link with id only will always point to the same user - When I visit "/profile/MHNqce98y1" - Then I get redirected to "/profile/MHNqce98y1/thehawk" - - Scenario Outline: ID takes precedence over slug - When I visit "" - Then I get redirected to "" - Examples: - | url | redirectUrl | - | /profile/MHNqce98y1/stephen-hawking | /profile/MHNqce98y1/thehawk | - | /post/bWBjpkTKZp/the-way-you-think | /post/bWBjpkTKZp/101-essays | diff --git a/cypress/integration/post/WritePost.feature b/cypress/integration/post/WritePost.feature deleted file mode 100644 index 0d74606ad..000000000 --- a/cypress/integration/post/WritePost.feature +++ /dev/null @@ -1,28 +0,0 @@ -Feature: Create a post - As a user - I would like to create a post - To say something to everyone in the community - - Background: - Given I have a user account - And I am logged in - And we have a selection of categories - And I am on the "landing" page - - Scenario: Create a post - When I click on the big plus icon in the bottom right corner to create post - And I choose "My first post" as the title of the post - And I type in the following text: - """ - Human Connection is a free and open-source social network - for active citizenship. - """ - And I select a category - And I choose "en" as the language for the post - And I click on "Save" - Then I get redirected to ".../my-first-post" - And the post was saved successfully - - Scenario: See a post on the landing page - Given I previously created a post - Then the post shows up on the landing page at position 1 diff --git a/cypress/integration/user_account/ChangePassword.feature b/cypress/integration/user_account/ChangePassword.feature deleted file mode 100644 index dbdf724f7..000000000 --- a/cypress/integration/user_account/ChangePassword.feature +++ /dev/null @@ -1,30 +0,0 @@ -Feature: Change password - As a user - I want to change my password in my settings - For security, e.g. if I exposed my password by accident - - Login via email and password is a well-known authentication procedure and you - can assure to the server that you are who you claim to be. Either if you - exposed your password by acccident and you want to invalidate the exposed - password or just out of an good habit, you want to change your password. - - Background: - Given I am logged in with these credentials: - | email | password | - | user@example.org | exposed | - - Scenario: Change my password - Given I am on the "settings" page - And I click on "Security" - When I fill the password form with: - | Your old password | exposed | - | Your new passsword | secure | - | Confirm new password | secure | - And submit the form - And I see a success message: - """ - Password successfully changed! - """ - And I log out through the menu in the top right corner - Then I cannot login anymore with password "exposed" - But I can login successfully with password "secure" diff --git a/cypress/integration/user_account/Login.feature b/cypress/integration/user_account/Login.feature deleted file mode 100644 index 6e8f60a56..000000000 --- a/cypress/integration/user_account/Login.feature +++ /dev/null @@ -1,23 +0,0 @@ -Feature: Authentication - As a database administrator - I want users to sign in - In order to attribute posts and other contributions to their authors - - Background: - Given I have a user account - - Scenario: Log in - When I visit the "login" page - And I fill in my email and password combination and click submit - Then I can click on my profile picture in the top right corner - And I can see my name "Peter Lustig" in the dropdown menu - - Scenario: Refresh and stay logged in - Given I am logged in - When I refresh the page - Then I am still logged in - - Scenario: Log out - Given I am logged in - When I log out through the menu in the top right corner - Then I see the login screen again diff --git a/cypress/integration/user_profile/AboutMeAndLocation.feature b/cypress/integration/user_profile/AboutMeAndLocation.feature deleted file mode 100644 index 2a512bf3f..000000000 --- a/cypress/integration/user_profile/AboutMeAndLocation.feature +++ /dev/null @@ -1,37 +0,0 @@ -Feature: About me and location - As a user - I would like to add some about me text and a location - So others can get some info about me and my location - - The location and about me are displayed on the user profile. Later it will be possible - to search for users by location. - - Background: - Given I have a user account - And I am logged in - And I am on the "settings" page - - Scenario: Change username - When I save "Hansi" as my new name - Then I can see my new name "Hansi" when I click on my profile picture in the top right - And when I refresh the page - Then the name "Hansi" is still there - - Scenario Outline: I set my location to "" - When I save "" as my location - When people visit my profile page - Then they can see the location in the info box below my avatar - - Examples: Location - | location | type | - | Paris | City | - | Saxony-Anhalt | Region | - | Germany | Country | - - Scenario: Display a description on profile page - Given I have the following self-description: - """ - Ich lebe fettlos, fleischlos, fischlos dahin, fühle mich aber ganz wohl dabei - """ - When people visit my profile page - Then they can see the text in the info box below my avatar diff --git a/cypress/integration/user_profile/SocialMedia.feature b/cypress/integration/user_profile/SocialMedia.feature deleted file mode 100644 index e6090a0a4..000000000 --- a/cypress/integration/user_profile/SocialMedia.feature +++ /dev/null @@ -1,42 +0,0 @@ -Feature: List Social Media Accounts - As a User - I'd like to enter my social media - So I can show them to other users to get in contact - - Background: - Given I have a user account - And I am logged in - - Scenario: Adding Social Media - Given I am on the "settings" page - And I click on the "Social media" link - Then I should be on the "/settings/my-social-media" page - When I add a social media link - Then it gets saved successfully - And the new social media link shows up on the page - - Scenario: Other users viewing my Social Media - Given I have added a social media link - When people visit my profile page - Then they should be able to see my social media links - - Scenario: Deleting Social Media - Given I am on the "settings" page - And I click on the "Social media" link - Then I should be on the "/settings/my-social-media" page - Given I have added a social media link - When I delete a social media link - Then it gets deleted successfully - - Scenario: Editing Social Media - Given I am on the "settings" page - And I click on the "Social media" link - Then I should be on the "/settings/my-social-media" page - Given I have added a social media link - When I start editing a social media link - Then I can cancel editing - When I start editing a social media link - And I edit and save the link - Then it gets saved successfully - And the new url is displayed - But the old url is not displayed diff --git a/cypress/integration/user_profile/UploadUserProfileImage.feature b/cypress/integration/user_profile/UploadUserProfileImage.feature deleted file mode 100644 index b46a31de8..000000000 --- a/cypress/integration/user_profile/UploadUserProfileImage.feature +++ /dev/null @@ -1,18 +0,0 @@ -Feature: Upload UserProfile Image - As a user - I would like to be able to add an avatar/profile pic to my profile - So that I can personalize my profile - - - Background: - Given I have a user account - - Scenario: Change my UserProfile Image - Given I am logged in - And I visit my profile page - Then I should be able to change my profile picture - - Scenario: Unable to change another user's avatar - Given I am logged in with a "user" role - And I visit another user's profile page - Then I cannot upload a picture \ No newline at end of file diff --git a/cypress/integration/user_profile/mute-users/Mute.feature b/cypress/integration/user_profile/mute-users/Mute.feature deleted file mode 100644 index 03ac4370b..000000000 --- a/cypress/integration/user_profile/mute-users/Mute.feature +++ /dev/null @@ -1,51 +0,0 @@ -Feature: Mute a User - As a user - I'd like to have a button to mute another user - To prevent him from seeing and interacting with my contributions - Background: - Given I have a user account - And there is an annoying user called "Spammy Spammer" - And I am logged in - - Scenario: Mute a user - Given I am on the profile page of the annoying user - When I click on "Mute user" from the content menu in the user info box - And I navigate to my "Muted users" settings page - Then I can see the following table: - | Avatar | Name | - | | Spammy Spammer | - - Scenario: Mute a previously followed user - Given I follow the user "Spammy Spammer" - And "Spammy Spammer" wrote a post "Spam Spam Spam" - When I visit the profile page of the annoying user - And I click on "Mute user" from the content menu in the user info box - Then the list of posts of this user is empty - And I get removed from his follower collection - - Scenario: Posts of muted users are filtered from search results, users are not - Given we have the following posts in our database: - | id | title | content | - | im-not-muted | Post that should be seen | cause I'm not muted | - Given "Spammy Spammer" wrote a post "Spam Spam Spam" - When I search for "Spam" - Then I should see the following posts in the select dropdown: - | title | - | Spam Spam Spam | - When I mute the user "Spammy Spammer" - And I refresh the page - And I search for "Spam" - Then the search should not contain posts by the annoying user - But the search should contain the annoying user - But I search for "not muted" - Then I should see the following posts in the select dropdown: - | title | - | Post that should be seen | - - Scenario: Muted users can still see my posts - Given I previously created a post - And I mute the user "Spammy Spammer" - And the "muted" user searches for "previously created" - Then I should see the following posts in the select dropdown: - | title | - | previously created post | diff --git a/cypress/parallel-features.sh b/cypress/parallel-features.sh new file mode 100755 index 000000000..a234b1d0e --- /dev/null +++ b/cypress/parallel-features.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Functions +function join_by { local IFS="$1"; shift; echo "$*"; } + +# Arguments: +CUR_JOB=$1 +MAX_JOBS=$2 + +# Features +FEATURE_LIST=( $(find cypress/integration/ -maxdepth 1 -name "*.feature") ) + +# Calculation +MAX_FEATURES=$(find cypress/integration/ -maxdepth 1 -name "*.feature" -printf '.' | wc -m) +FEATURES_PER_JOB=$(expr $(expr ${MAX_FEATURES} + ${MAX_JOBS} - 1) / ${MAX_JOBS} ) +FEATURES_SKIP=$(expr $(expr ${CUR_JOB} - 1 ) \* ${FEATURES_PER_JOB} ) + +# Comma separated list +echo $(join_by , ${FEATURE_LIST[@]:${FEATURES_SKIP}:${FEATURES_PER_JOB}}) \ No newline at end of file diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index cc6ac0e91..4e6b440ef 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -15,15 +15,27 @@ const cucumber = require('cypress-cucumber-preprocessor').default const dotenv = require('dotenv') +// Import backend .env (smart)? +const { parsed } = dotenv.config({ path: require.resolve('../../backend/.env') }) + +// Test persistent(between commands) store +const testStore = {} + module.exports = (on, config) => { - // (on, config) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config - const { parsed } = dotenv.config({ path: require.resolve('../../backend/.env') }) config.env.NEO4J_URI = parsed.NEO4J_URI config.env.NEO4J_USERNAME = parsed.NEO4J_USERNAME config.env.NEO4J_PASSWORD = parsed.NEO4J_PASSWORD config.env.JWT_SECRET = parsed.JWT_SECRET on('file:preprocessor', cucumber()) + on('task', { + pushValue({ name, value }) { + testStore[name] = value + return true + }, + getValue(name) { + console.log("getValue",name,testStore) + return testStore[name] + }, + }) return config -} +} \ No newline at end of file diff --git a/cypress/support/commands.js b/cypress/support/commands.js index a15e57007..335e00390 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -14,16 +14,9 @@ /* globals Cypress cy */ import "cypress-file-upload"; -import helpers from "./helpers"; import { GraphQLClient, request } from 'graphql-request' import { gql } from '../../backend/src/helpers/jest' import config from '../../backend/src/config' -import encode from '../../backend/src/jwt/encode' - -const switchLang = name => { - cy.get(".locale-menu").click(); - cy.contains(".locale-menu-popover a", name).click(); -}; const authenticatedHeaders = (variables) => { const mutation = gql` @@ -38,50 +31,11 @@ const authenticatedHeaders = (variables) => { }) } -Cypress.Commands.add("switchLanguage", (name, force) => { - const { code } = helpers.getLangByName(name); - if (force) { - switchLang(name); - } else { - cy.get("html").then($html => { - if ($html && $html.attr("lang") !== code) { - switchLang(name); - } - }); - } -}); - -Cypress.Commands.add("login", user => { - const token = encode(user) - cy.setCookie('human-connection-token', token) - .visit("/") -}); - -Cypress.Commands.add("manualLogin", ({ email, password }) => { - cy.visit(`/login`) - .get("input[name=email]") - .trigger("focus") - .type(email) - .get("input[name=password]") - .trigger("focus") - .type(password) - .get("button[name=submit]") - .as("submitButton") - .click(); -}); - Cypress.Commands.add("logout", () => { cy.visit(`/logout`); cy.location("pathname").should("contain", "/login"); // we're out }); -Cypress.Commands.add("openPage", page => { - if (page === "landing") { - page = ""; - } - cy.visit(`/${page}`); -}); - Cypress.Commands.add( 'authenticateAs', ({email, password}) => { diff --git a/cypress/support/helpers.js b/cypress/support/helpers.js deleted file mode 100644 index 7d66af5d6..000000000 --- a/cypress/support/helpers.js +++ /dev/null @@ -1,8 +0,0 @@ -import find from 'lodash/find' -import locales from '../../webapp/locales' - -export default { - getLangByName(name) { - return find(locales, { name }) - } -} diff --git a/cypress/support/index.js b/cypress/support/index.js index 3290d2a5a..453c8476f 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -19,7 +19,7 @@ import './commands' import './factories' // intermittent failing tests -import 'cypress-plugin-retries' +// import 'cypress-plugin-retries' // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/docker-compose.test.yml b/docker-compose.test.yml index be130881e..eef71e67b 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -30,6 +30,17 @@ services: ######################################################## neo4j: image: ocelotsocialnetwork/neo4j:community + #environment: + # - NEO4J_dbms_connector_bolt_enabled=true + # - NEO4J_dbms_connector_bolt_tls__level=OPTIONAL + # - NEO4J_dbms_connector_bolt_listen__address=0.0.0.0:7687 + # - NEO4J_auth=none + # - NEO4J_dbms_connectors_default__listen__address=0.0.0.0 + # - NEO4J_dbms_connector_http_listen__address=0.0.0.0:7474 + # - NEO4J_dbms_connector_https_listen__address=0.0.0.0:7473 + networks: + # So we can access the neo4j query browser from our host machine + - external-net ######################################################## # MAINTENANCE ########################################## diff --git a/neo4j/Dockerfile b/neo4j/Dockerfile index e7931378a..4bdc4ef1f 100644 --- a/neo4j/Dockerfile +++ b/neo4j/Dockerfile @@ -3,13 +3,16 @@ ################################################################################## FROM neo4j:3.5.14 as community -# ENVs (available in production aswell, can be overwritten by commandline or env file) +# ENVs ## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0 -ENV BUILD_DATE="1970-01-01T00:00:00.00Z" +ARG BBUILD_DATE="1970-01-01T00:00:00.00Z" +ENV BUILD_DATE=$BBUILD_DATE ## We cannot do $(yarn run version)-${BUILD_NUMBER} here so we default to 0.0.0-0 -ENV BUILD_VERSION="0.0.0-0" +ARG BBUILD_VERSION="0.0.0-0" +ENV BUILD_VERSION=$BBUILD_VERSION ## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000 -ENV BUILD_COMMIT="0000000" +ARG BBUILD_COMMIT="0000000" +ENV BUILD_COMMIT=$BBUILD_COMMIT # Labels LABEL org.label-schema.build-date="${BUILD_DATE}" diff --git a/package.json b/package.json index d01596707..937b0a6bc 100644 --- a/package.json +++ b/package.json @@ -13,14 +13,10 @@ "nonGlobalStepDefinitions": true }, "scripts": { - "install:all": "yarn install && cd backend && yarn install && cd ../webapp && yarn install", "db:seed": "cd backend && yarn run db:seed", "db:reset": "cd backend && yarn run db:reset", - "cypress:backend": "cd backend && yarn run dev", - "cypress:webapp": "cd webapp && yarn run dev", - "cypress:setup": "run-p cypress:backend cypress:webapp", - "cypress:run": "cross-env cypress run --browser firefox", - "cypress:open": "cross-env cypress open --browser firefox", + "cypress:run": "cypress run --browser electron --config-file ./cypress/cypress.json", + "cypress:open": "cypress open --browser electron --config-file ./cypress/cypress.json", "cucumber:setup": "cd backend && yarn run dev", "cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit", "release": "yarn version --no-git-tag-version --no-commit-hooks --no-commit && auto-changelog --latest-version $(node -p -e \"require('./package.json').version\") && cd backend && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../package.json').version\") && cd ../webapp && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../package.json').version\")" @@ -34,10 +30,9 @@ "codecov": "^3.7.1", "cross-env": "^7.0.2", "cucumber": "^6.0.5", - "cypress": "^4.2.0", + "cypress": "^7.0.1", "cypress-cucumber-preprocessor": "^2.2.1", "cypress-file-upload": "^3.5.3", - "cypress-plugin-retries": "^1.5.2", "date-fns": "^2.12.0", "dotenv": "^8.2.0", "expect": "^25.3.0", diff --git a/webapp/Dockerfile b/webapp/Dockerfile index eda437920..75d43bf59 100644 --- a/webapp/Dockerfile +++ b/webapp/Dockerfile @@ -3,15 +3,18 @@ ################################################################################## FROM node:12.19.0-alpine3.10 as base -# ENVs (available in production aswell, can be overwritten by commandline or env file) +# ENVs ## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame ENV DOCKER_WORKDIR="/app" ## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0 -ENV BUILD_DATE="1970-01-01T00:00:00.00Z" +ARG BBUILD_DATE="1970-01-01T00:00:00.00Z" +ENV BUILD_DATE=$BBUILD_DATE ## We cannot do $(yarn run version)-${BUILD_NUMBER} here so we default to 0.0.0-0 -ENV BUILD_VERSION="0.0.0-0" +ARG BBUILD_VERSION="0.0.0-0" +ENV BUILD_VERSION=$BBUILD_VERSION ## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000 -ENV BUILD_COMMIT="0000000" +ARG BBUILD_COMMIT="0000000" +ENV BUILD_COMMIT=$BBUILD_COMMIT ## SET NODE_ENV ENV NODE_ENV="production" ## App relevant Envs diff --git a/webapp/Dockerfile.maintenance b/webapp/Dockerfile.maintenance index b02fe352b..0a7616240 100644 --- a/webapp/Dockerfile.maintenance +++ b/webapp/Dockerfile.maintenance @@ -1,18 +1,56 @@ -FROM node:12.19.0-alpine3.10 as build -LABEL Description="Maintenance page of the Social Network ocelot.social" Vendor="ocelot.social Community" Version="0.0.1" Maintainer="ocelot.social Community (devops@ocelot.social)" +################################################################################## +# BASE ########################################################################### +################################################################################## +FROM node:12.19.0-alpine3.10 as base + +# ENVs +## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame +ENV DOCKER_WORKDIR="/app" +## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0 +ARG BBUILD_DATE="1970-01-01T00:00:00.00Z" +ENV BUILD_DATE=$BBUILD_DATE +## We cannot do $(yarn run version)-${BUILD_NUMBER} here so we default to 0.0.0-0 +ARG BBUILD_VERSION="0.0.0-0" +ENV BUILD_VERSION=$BBUILD_VERSION +## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000 +ARG BBUILD_COMMIT="0000000" +ENV BUILD_COMMIT=$BBUILD_COMMIT +## SET NODE_ENV +ENV NODE_ENV="production" +## App relevant Envs +ENV PORT="3000" + +# Labels +LABEL org.label-schema.build-date="${BUILD_DATE}" +LABEL org.label-schema.name="ocelot.social:backend" +LABEL org.label-schema.description="Maintenance page of the Social Network Software ocelot.social" +LABEL org.label-schema.usage="https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/README.md" +LABEL org.label-schema.url="https://ocelot.social" +LABEL org.label-schema.vcs-url="https://github.com/Ocelot-Social-Community/Ocelot-Social/tree/master/backend" +LABEL org.label-schema.vcs-ref="${BUILD_COMMIT}" +LABEL org.label-schema.vendor="ocelot.social Community" +LABEL org.label-schema.version="${BUILD_VERSION}" +LABEL org.label-schema.schema-version="1.0" +LABEL maintainer="devops@ocelot.social" + +# Install Additional Software +## install: git +RUN apk --no-cache add git + +# Settings +## Expose Container Port +EXPOSE ${PORT} + +## Workdir +RUN mkdir -p ${DOCKER_WORKDIR} +WORKDIR ${DOCKER_WORKDIR} -EXPOSE 3000 CMD ["yarn", "run", "start"] -# Expose the app port -ARG BUILD_COMMIT -ENV BUILD_COMMIT=$BUILD_COMMIT -ARG WORKDIR=/develop-webapp -RUN mkdir -p $WORKDIR -WORKDIR $WORKDIR - -# See: https://github.com/nodejs/docker-node/pull/367#issuecomment-430807898 -RUN apk --no-cache add git +################################################################################## +# BUILD ### TODO # TODO # TODO # TODO # TODO # TODO # TODO # TODO # TODO # TODO ## +################################################################################## +FROM base as build COPY package.json yarn.lock ./ RUN yarn install --production=false --frozen-lockfile --non-interactive @@ -35,8 +73,11 @@ COPY maintenance/source ./ RUN yarn run generate +################################################################################## +# PRODUCTION ### TODO # TODO # TODO # TODO # TODO # TODO # TODO # TODO # TODO #### +################################################################################## +FROM nginx:alpine as production -FROM nginx:alpine -COPY --from=build ./develop-webapp/dist/ /usr/share/nginx/html/ +COPY --from=build ./app/dist/ /usr/share/nginx/html/ RUN rm /etc/nginx/conf.d/default.conf COPY maintenance/nginx/custom.conf /etc/nginx/conf.d/ diff --git a/webapp/components/ComponentSlider/ComponentSlider.spec.js b/webapp/components/ComponentSlider/ComponentSlider.spec.js new file mode 100644 index 000000000..25bf3e7f4 --- /dev/null +++ b/webapp/components/ComponentSlider/ComponentSlider.spec.js @@ -0,0 +1,64 @@ +import { mount } from '@vue/test-utils' +import ComponentSlider from './ComponentSlider.vue' + +const localVue = global.localVue + +describe('ComponentSlider.vue', () => { + let wrapper + let mocks + let propsData + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + propsData = { + sliderData: { + sliderIndex: 0, + sliderSelectorCallback: jest.fn().mockResolvedValue(true), + sliders: [ + { + validated: true, + button: { + icon: 'smile', + callback: jest.fn().mockResolvedValue(true), + sliderCallback: jest.fn().mockResolvedValue(true), + }, + }, + { + validated: true, + button: { + icon: 'smile', + callback: jest.fn().mockResolvedValue(true), + sliderCallback: jest.fn().mockResolvedValue(true), + }, + }, + ], + }, + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(ComponentSlider, { + mocks, + localVue, + propsData, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('div')).toBe(true) + }) + + it('click on next Button', async () => { + await wrapper.find('.base-button[data-test="next-button"]').trigger('click') + await wrapper.vm.$nextTick() + expect(propsData.sliderData.sliderSelectorCallback).toHaveBeenCalled() + }) + }) +}) diff --git a/webapp/components/FollowButton.spec.js b/webapp/components/FollowButton.spec.js new file mode 100644 index 000000000..000745081 --- /dev/null +++ b/webapp/components/FollowButton.spec.js @@ -0,0 +1,47 @@ +import { mount } from '@vue/test-utils' +import FollowButton from './FollowButton.vue' + +const localVue = global.localVue + +describe('FollowButton.vue', () => { + let mocks + let propsData + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $apollo: { + mutate: jest.fn(), + }, + } + propsData = {} + }) + + describe('mount', () => { + let wrapper + const Wrapper = () => { + return mount(FollowButton, { mocks, propsData, localVue }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders button and text', () => { + expect(mocks.$t).toHaveBeenCalledWith('followButton.follow') + expect(wrapper.findAll('.base-button')).toHaveLength(1) + }) + + it('renders button and text when followed', () => { + propsData.isFollowed = true + wrapper = Wrapper() + expect(mocks.$t).toHaveBeenCalledWith('followButton.following') + expect(wrapper.findAll('.base-button')).toHaveLength(1) + }) + + it.skip('toggle the button', async () => { + wrapper.find('.base-button').trigger('click') // This does not work since @click.prevent is used + expect(wrapper.vm.isFollowed).toBe(true) + }) + }) +}) diff --git a/webapp/components/InviteButton/InviteButton.spec.js b/webapp/components/InviteButton/InviteButton.spec.js new file mode 100644 index 000000000..f28045612 --- /dev/null +++ b/webapp/components/InviteButton/InviteButton.spec.js @@ -0,0 +1,53 @@ +import { config, mount } from '@vue/test-utils' +import InviteButton from './InviteButton.vue' + +config.stubs['v-popover'] = '' + +describe('InviteButton.vue', () => { + let wrapper + let mocks + let propsData + + beforeEach(() => { + mocks = { + $t: jest.fn(), + navigator: { + clipboard: { + writeText: jest.fn(), + }, + }, + } + propsData = {} + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(InviteButton, { mocks, propsData }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.contains('.invite-button')).toBe(true) + }) + + it('open popup', () => { + wrapper.find('.base-button').trigger('click') + expect(wrapper.contains('.invite-button')).toBe(true) + }) + + it('invite codes not available', async () => { + wrapper.find('.base-button').trigger('click') // open popup + wrapper.find('.invite-button').trigger('click') // click copy button + expect(mocks.$t).toHaveBeenCalledWith('invite-codes.not-available') + }) + + it.skip('invite codes copied to clipboard', async () => { + wrapper.find('.base-button').trigger('click') // open popup + wrapper.find('.invite-button').trigger('click') // click copy button + expect(mocks.$t).toHaveBeenCalledWith('invite-codes.not-available') + }) + }) +}) diff --git a/webapp/components/Logo/Logo.spec.js b/webapp/components/Logo/Logo.spec.js new file mode 100644 index 000000000..a712a529b --- /dev/null +++ b/webapp/components/Logo/Logo.spec.js @@ -0,0 +1,29 @@ +import { mount } from '@vue/test-utils' +import Logo from './Logo.vue' + +const localVue = global.localVue + +describe('Logo.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Logo, { mocks, localVue }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.findAll('.ds-logo')).toHaveLength(1) + }) + }) +}) diff --git a/webapp/components/Modal.spec.js b/webapp/components/Modal.spec.js index 3ebff8771..c08c90f51 100644 --- a/webapp/components/Modal.spec.js +++ b/webapp/components/Modal.spec.js @@ -131,6 +131,42 @@ describe('Modal.vue', () => { }) }) }) + + describe('store/modal data contains an user', () => { + it('passes user name to report modal', () => { + state.data = { + type: 'user', + resource: { + id: 'u456', + name: 'Username', + }, + } + wrapper = Wrapper() + expect(wrapper.find(DisableModal).props()).toEqual({ + type: 'user', + name: 'Username', + id: 'u456', + }) + }) + }) + + describe('store/modal data contains no valid datatype', () => { + it('passes something as datatype to modal', () => { + state.data = { + type: 'something', + resource: { + id: 's456', + name: 'Username', + }, + } + wrapper = Wrapper() + expect(wrapper.find(DisableModal).props()).toEqual({ + type: 'something', + name: null, + id: 's456', + }) + }) + }) }) }) }) diff --git a/webapp/components/NotificationList/NotificationList.spec.js b/webapp/components/NotificationList/NotificationList.spec.js index ce20a2765..219c1fdbb 100644 --- a/webapp/components/NotificationList/NotificationList.spec.js +++ b/webapp/components/NotificationList/NotificationList.spec.js @@ -81,4 +81,23 @@ describe('NotificationList.vue', () => { }) }) }) + + describe('shallowMount with no notifications', () => { + const Wrapper = () => { + return shallowMount(NotificationList, { + propsData: {}, + mocks, + store, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders Notification.vue zero times', () => { + expect(wrapper.findAll(Notification)).toHaveLength(0) + }) + }) }) diff --git a/webapp/components/PageFooter/PageFooter.spec.js b/webapp/components/PageFooter/PageFooter.spec.js new file mode 100644 index 000000000..0edc0fed2 --- /dev/null +++ b/webapp/components/PageFooter/PageFooter.spec.js @@ -0,0 +1,44 @@ +import { config, mount } from '@vue/test-utils' +import PageFooter from './PageFooter.vue' +import links from '~/constants/links.js' + +const localVue = global.localVue + +config.stubs['nuxt-link'] = '' + +describe('PageFooter.vue', () => { + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $env: { + VERSION: 'v1.0.0', + }, + links, + } + }) + + describe('mount', () => { + let wrapper + const Wrapper = () => { + return mount(PageFooter, { mocks, localVue }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders three links', () => { + expect(wrapper.findAll('a')).toHaveLength(3) + }) + + it('renders four nuxt-links', () => { + expect(wrapper.findAll('.nuxt-link')).toHaveLength(4) + }) + + it('renders version', () => { + expect(wrapper.find('.ds-footer').text()).toContain('v1.0.0') + }) + }) +}) diff --git a/webapp/components/Password/Change.spec.js b/webapp/components/Password/Change.spec.js index 8416a0fce..95b7c1a3a 100644 --- a/webapp/components/Password/Change.spec.js +++ b/webapp/components/Password/Change.spec.js @@ -126,21 +126,18 @@ describe('ChangePassword.vue', () => { }) }) - // TODO This is not a valid testcase - we have to decide if we catch the same password on clientside - /* describe('mutation rejects', () => { + describe('mutation rejects', () => { beforeEach(async () => { await wrapper.find('input#oldPassword').setValue('supersecret') await wrapper.find('input#password').setValue('supersecret') await wrapper.find('input#passwordConfirmation').setValue('supersecret') + await wrapper.find('form').trigger('submit') }) it('displays error message', async () => { - await wrapper.find('form').trigger('submit') - await mocks.$apollo.mutate - expect(mocks.$toast.error).toHaveBeenCalledWith('Ouch!') }) - }) */ + }) }) }) }) diff --git a/webapp/components/PasswordReset/ChangePassword.spec.js b/webapp/components/PasswordReset/ChangePassword.spec.js index b1b55cb06..d6f451604 100644 --- a/webapp/components/PasswordReset/ChangePassword.spec.js +++ b/webapp/components/PasswordReset/ChangePassword.spec.js @@ -76,6 +76,22 @@ describe('ChangePassword ', () => { }) }) }) + + describe('password reset not successful', () => { + beforeEach(() => { + mocks.$apollo.mutate = jest.fn().mockRejectedValue({ + message: 'Ouch!', + }) + wrapper = Wrapper() + wrapper.find('input#password').setValue('supersecret') + wrapper.find('input#passwordConfirmation').setValue('supersecret') + wrapper.find('form').trigger('submit') + }) + + it('display a toast error', () => { + expect(mocks.$toast.error).toHaveBeenCalled() + }) + }) }) }) }) diff --git a/webapp/components/PasswordReset/Request.spec.js b/webapp/components/PasswordReset/Request.spec.js index 83459814e..e601030c6 100644 --- a/webapp/components/PasswordReset/Request.spec.js +++ b/webapp/components/PasswordReset/Request.spec.js @@ -95,5 +95,20 @@ describe('Request', () => { expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) }) }) + + describe('backend throws an error', () => { + beforeEach(() => { + mocks.$apollo.mutate = jest.fn().mockRejectedValue({ + message: 'Ouch!', + }) + wrapper = Wrapper() + wrapper.find('input#email').setValue('mail@gmail.com') + wrapper.find('form').trigger('submit') + }) + + it('display a toast error', () => { + expect(mocks.$toast.error).toHaveBeenCalled() + }) + }) }) }) diff --git a/webapp/components/Registration/Signup.spec.js b/webapp/components/Registration/Signup.spec.js index 36b16903c..dda0cbb9d 100644 --- a/webapp/components/Registration/Signup.spec.js +++ b/webapp/components/Registration/Signup.spec.js @@ -76,6 +76,21 @@ describe('Signup', () => { expect(mocks.$t).toHaveBeenCalledWith(...expected) }) + describe('mutation is rejected', () => { + beforeEach(async () => { + mocks.$apollo.mutate = jest.fn().mockRejectedValue({ + message: 'Ouch!', + }) + wrapper = Wrapper() + wrapper.find('input#email').setValue('mail@example.org') + await wrapper.find('form').trigger('submit') + }) + + it('displays error message', async () => { + expect(mocks.$toast.error).toHaveBeenCalledWith('Ouch!') + }) + }) + describe('after animation', () => { beforeEach(jest.runAllTimers) diff --git a/webapp/components/ShoutButton.spec.js b/webapp/components/ShoutButton.spec.js new file mode 100644 index 000000000..c3af134c1 --- /dev/null +++ b/webapp/components/ShoutButton.spec.js @@ -0,0 +1,57 @@ +import { mount } from '@vue/test-utils' +import ShoutButton from './ShoutButton.vue' +import Vue from 'vue' + +const localVue = global.localVue + +describe('ShoutButton.vue', () => { + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $apollo: { + mutate: jest.fn(), + }, + } + }) + + describe('mount', () => { + let wrapper + const Wrapper = () => { + return mount(ShoutButton, { mocks, localVue }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders button and text', () => { + expect(mocks.$t).toHaveBeenCalledWith('shoutButton.shouted') + expect(wrapper.findAll('.base-button')).toHaveLength(1) + expect(wrapper.findAll('.shout-button-text')).toHaveLength(1) + expect(wrapper.vm.shouted).toBe(false) + expect(wrapper.vm.shoutedCount).toBe(0) + }) + + it('toggle the button', async () => { + mocks.$apollo.mutate = jest.fn().mockResolvedValue({ data: { shout: 'WeDoShout' } }) + wrapper.find('.base-button').trigger('click') + expect(wrapper.vm.shouted).toBe(true) + expect(wrapper.vm.shoutedCount).toBe(1) + await Vue.nextTick() + expect(wrapper.vm.shouted).toBe(true) + expect(wrapper.vm.shoutedCount).toBe(1) + }) + + it('toggle the button, but backend fails', async () => { + mocks.$apollo.mutate = jest.fn().mockRejectedValue({ message: 'Ouch!' }) + await wrapper.find('.base-button').trigger('click') + expect(wrapper.vm.shouted).toBe(true) + expect(wrapper.vm.shoutedCount).toBe(1) + await Vue.nextTick() + expect(wrapper.vm.shouted).toBe(false) + expect(wrapper.vm.shoutedCount).toBe(0) + }) + }) +}) diff --git a/webapp/components/features/ReportsTable/ReportsTable.spec.js b/webapp/components/features/ReportsTable/ReportsTable.spec.js index a9baeea4f..c80e4fea5 100644 --- a/webapp/components/features/ReportsTable/ReportsTable.spec.js +++ b/webapp/components/features/ReportsTable/ReportsTable.spec.js @@ -34,7 +34,7 @@ describe('ReportsTable', () => { describe('given no reports', () => { beforeEach(() => { - propsData = { ...propsData, reports: [] } + propsData = { ...propsData } wrapper = Wrapper() }) diff --git a/webapp/components/generic/SearchableInput/SearchableInput.spec.js b/webapp/components/generic/SearchableInput/SearchableInput.spec.js index 53c361997..e0e9f9831 100644 --- a/webapp/components/generic/SearchableInput/SearchableInput.spec.js +++ b/webapp/components/generic/SearchableInput/SearchableInput.spec.js @@ -120,5 +120,15 @@ describe('SearchableInput.vue', () => { query: { search: 'ab' }, }) }) + + it('replaces irregular whitespace with a single space', async () => { + select.element.value = 'peter lustig' + select.trigger('input') + select.trigger('keyup.enter') + expect(mocks.$router.push).toHaveBeenCalledWith({ + path: '/search/search-results', + query: { search: 'peter lustig' }, + }) + }) }) }) diff --git a/webapp/config/index.js b/webapp/config/index.js index dd5a5e04d..00df85bac 100644 --- a/webapp/config/index.js +++ b/webapp/config/index.js @@ -17,7 +17,7 @@ const environment = { const server = { GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000', BACKEND_TOKEN: process.env.BACKEND_TOKEN || 'NULL', - WEBSOCKETS_URI: process.env.WEBSOCKETS_URI || 'ws://localhost:4000/graphql', + WEBSOCKETS_URI: process.env.WEBSOCKETS_URI || 'ws://localhost:3000/api/graphql', } const sentry = { diff --git a/webapp/graphql/Fragments.js b/webapp/graphql/Fragments.js index b67851873..7b05e2369 100644 --- a/webapp/graphql/Fragments.js +++ b/webapp/graphql/Fragments.js @@ -78,12 +78,6 @@ export const tagsCategoriesAndPinnedFragment = gql` tags { id } - categories { - id - slug - name - icon - } pinnedBy { id name diff --git a/webapp/layouts/basic.spec.js b/webapp/layouts/basic.spec.js new file mode 100644 index 000000000..5094a970b --- /dev/null +++ b/webapp/layouts/basic.spec.js @@ -0,0 +1,34 @@ +import { config, shallowMount } from '@vue/test-utils' +import Basic from './basic.vue' + +const localVue = global.localVue + +config.stubs.nuxt = '' + +describe('basic.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('shallow mount', () => { + const Wrapper = () => { + return shallowMount(Basic, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.layout-blank')).toBe(true) + }) + }) +}) diff --git a/webapp/layouts/blank.spec.js b/webapp/layouts/blank.spec.js new file mode 100644 index 000000000..a3ea3120c --- /dev/null +++ b/webapp/layouts/blank.spec.js @@ -0,0 +1,34 @@ +import { config, shallowMount } from '@vue/test-utils' +import Blank from './blank.vue' + +const localVue = global.localVue + +config.stubs.nuxt = '' + +describe('blank.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('shallow mount', () => { + const Wrapper = () => { + return shallowMount(Blank, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.layout-blank')).toBe(true) + }) + }) +}) diff --git a/webapp/layouts/default.spec.js b/webapp/layouts/default.spec.js new file mode 100644 index 000000000..3d465ce76 --- /dev/null +++ b/webapp/layouts/default.spec.js @@ -0,0 +1,52 @@ +import Vuex from 'vuex' +import { config, shallowMount } from '@vue/test-utils' +import Default from './default.vue' + +const localVue = global.localVue +localVue.directive('scrollTo', jest.fn()) + +config.stubs.nuxt = '' +config.stubs['client-only'] = '' +config.stubs['nuxt-link'] = '' + +describe('default.vue', () => { + let wrapper + let mocks + let store + + beforeEach(() => { + mocks = { + $route: { + matched: [{ name: 'index' }], + }, + $scrollTo: jest.fn(), + $t: jest.fn(), + $env: { + INVITE_REGISTRATION: true, + }, + } + store = new Vuex.Store({ + getters: { + 'auth/isLoggedIn': () => true, + }, + }) + }) + + describe('shallow mount', () => { + const Wrapper = () => { + return shallowMount(Default, { + store, + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.layout-default')).toBe(true) + }) + }) +}) diff --git a/webapp/package.json b/webapp/package.json index 84cdcbc7c..7abe34e4a 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -24,9 +24,17 @@ "verbose": true, "collectCoverageFrom": [ "**/*.{js,vue}", + "!**/?(*.)+(spec|test|story).js?(x)", "!**/node_modules/**", "!**/.nuxt/**", - "!**/?(*.)+(spec|test).js?(x)" + "!**/storybook/**", + "!**/coverage/**", + "!**/config/**", + "!**/maintenance/**", + "!**/plugins/**", + "!**/.eslintrc.js", + "!**/.prettierrc.js", + "!**/nuxt.config.js" ], "coverageReporters": [ "lcov" @@ -41,10 +49,10 @@ "vue" ], "moduleNameMapper": { - "^@/(.*)$": "/src/$1", - "^~/(.*)$": "/$1", + "\\.(svg)$": "/test/fileMock.js", "\\.(css|less)$": "identity-obj-proxy", - "\\.(svg)$": "/test/fileMock.js" + "^@/(.*)$": "/src/$1", + "^~/(.*)$": "/$1" }, "setupFiles": [ "/test/registerContext.js", diff --git a/webapp/pages/admin.spec.js b/webapp/pages/admin.spec.js new file mode 100644 index 000000000..fc3849fc4 --- /dev/null +++ b/webapp/pages/admin.spec.js @@ -0,0 +1,34 @@ +import { config, mount } from '@vue/test-utils' +import admin from './admin.vue' + +config.stubs['nuxt-child'] = '' + +const localVue = global.localVue + +describe('admin.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(admin, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('div')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/admin/categories.spec.js b/webapp/pages/admin/categories.spec.js new file mode 100644 index 000000000..55715e74b --- /dev/null +++ b/webapp/pages/admin/categories.spec.js @@ -0,0 +1,32 @@ +import { mount } from '@vue/test-utils' +import Categories from './categories.vue' + +const localVue = global.localVue + +describe('categories.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Categories, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.base-card')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/admin/donations.spec.js b/webapp/pages/admin/donations.spec.js new file mode 100644 index 000000000..2bc219dce --- /dev/null +++ b/webapp/pages/admin/donations.spec.js @@ -0,0 +1,32 @@ +import { mount } from '@vue/test-utils' +import Donations from './donations.vue' + +const localVue = global.localVue + +describe('donations.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Donations, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.base-card')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/admin/hashtags.spec.js b/webapp/pages/admin/hashtags.spec.js new file mode 100644 index 000000000..cd2d308d1 --- /dev/null +++ b/webapp/pages/admin/hashtags.spec.js @@ -0,0 +1,32 @@ +import { mount } from '@vue/test-utils' +import Hashtags from './hashtags.vue' + +const localVue = global.localVue + +describe('hashtags.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Hashtags, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.base-card')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/admin/invite.spec.js b/webapp/pages/admin/invite.spec.js new file mode 100644 index 000000000..e3e882119 --- /dev/null +++ b/webapp/pages/admin/invite.spec.js @@ -0,0 +1,35 @@ +import { mount } from '@vue/test-utils' +import Invite from './invite.vue' + +const localVue = global.localVue + +describe('invite.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $apollo: { + loading: false, + }, + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Invite, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.ds-section')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/admin/notifications.spec.js b/webapp/pages/admin/notifications.spec.js new file mode 100644 index 000000000..c9acf81a6 --- /dev/null +++ b/webapp/pages/admin/notifications.spec.js @@ -0,0 +1,35 @@ +import { mount } from '@vue/test-utils' +import Notifications from './notifications.vue' + +const localVue = global.localVue + +describe('notifications.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $apollo: { + loading: false, + }, + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Notifications, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.base-card')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/admin/organizations.spec.js b/webapp/pages/admin/organizations.spec.js new file mode 100644 index 000000000..d019d9485 --- /dev/null +++ b/webapp/pages/admin/organizations.spec.js @@ -0,0 +1,35 @@ +import { mount } from '@vue/test-utils' +import Organizations from './organizations.vue' + +const localVue = global.localVue + +describe('organizations.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $apollo: { + loading: false, + }, + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Organizations, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.base-card')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/admin/pages.spec.js b/webapp/pages/admin/pages.spec.js new file mode 100644 index 000000000..e0c3c9fb4 --- /dev/null +++ b/webapp/pages/admin/pages.spec.js @@ -0,0 +1,35 @@ +import { mount } from '@vue/test-utils' +import Pages from './pages.vue' + +const localVue = global.localVue + +describe('pages.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $apollo: { + loading: false, + }, + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Pages, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.base-card')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/admin/settings.spec.js b/webapp/pages/admin/settings.spec.js new file mode 100644 index 000000000..78a5beb94 --- /dev/null +++ b/webapp/pages/admin/settings.spec.js @@ -0,0 +1,35 @@ +import { mount } from '@vue/test-utils' +import Settings from './settings.vue' + +const localVue = global.localVue + +describe('settings.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $apollo: { + loading: false, + }, + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Settings, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.base-card')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/code-of-conduct.spec.js b/webapp/pages/code-of-conduct.spec.js new file mode 100644 index 000000000..75e244c79 --- /dev/null +++ b/webapp/pages/code-of-conduct.spec.js @@ -0,0 +1,38 @@ +import { mount } from '@vue/test-utils' +import CodeOfConduct from './code-of-conduct.vue' +import VueMeta from 'vue-meta' + +const localVue = global.localVue +localVue.use(VueMeta, { keyName: 'head' }) + +describe('code-of-conduct.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: (t) => t, + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(CodeOfConduct, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('div')).toBe(true) + }) + + it('has correct content', () => { + expect(wrapper.vm.$metaInfo.title).toBe('site.code-of-conduct') + }) + }) +}) diff --git a/webapp/pages/data-privacy.spec.js b/webapp/pages/data-privacy.spec.js new file mode 100644 index 000000000..a919bb742 --- /dev/null +++ b/webapp/pages/data-privacy.spec.js @@ -0,0 +1,38 @@ +import { mount } from '@vue/test-utils' +import DataPrivacy from './data-privacy.vue' +import VueMeta from 'vue-meta' + +const localVue = global.localVue +localVue.use(VueMeta, { keyName: 'head' }) + +describe('data-privacy.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: (t) => t, + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(DataPrivacy, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('div')).toBe(true) + }) + + it('has correct content', () => { + expect(wrapper.vm.$metaInfo.title).toBe('site.data-privacy') + }) + }) +}) diff --git a/webapp/pages/imprint.spec.js b/webapp/pages/imprint.spec.js new file mode 100644 index 000000000..1a84b5794 --- /dev/null +++ b/webapp/pages/imprint.spec.js @@ -0,0 +1,38 @@ +import { mount } from '@vue/test-utils' +import Imprint from './imprint.vue' +import VueMeta from 'vue-meta' + +const localVue = global.localVue +localVue.use(VueMeta, { keyName: 'head' }) + +describe('imprint.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: (t) => t, + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Imprint, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('div')).toBe(true) + }) + + it('has correct content', () => { + expect(wrapper.vm.$metaInfo.title).toBe('site.imprint') + }) + }) +}) diff --git a/webapp/pages/login.spec.js b/webapp/pages/login.spec.js new file mode 100644 index 000000000..09c1b066e --- /dev/null +++ b/webapp/pages/login.spec.js @@ -0,0 +1,74 @@ +import Vuex from 'vuex' +import { config, mount } from '@vue/test-utils' +import login from './login.vue' + +const localVue = global.localVue + +config.stubs['client-only'] = '' +config.stubs['nuxt-link'] = '' + +describe('Login.vue', () => { + let store + let mocks + let wrapper + let asyncData + let tosVersion + let redirect + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $i18n: { + locale: () => 'en', + }, + } + asyncData = false + tosVersion = '0.0.0' + redirect = jest.fn() + }) + + describe('mount', () => { + const Wrapper = async () => { + store = new Vuex.Store({ + getters: { + 'auth/user': () => { + return { termsAndConditionsAgreedVersion: tosVersion } + }, + }, + }) + if (asyncData) { + const data = login.data ? login.data() : {} + const aData = await login.asyncData({ + store, + redirect, + }) + login.data = function () { + return { ...data, ...aData } + } + } + return mount(login, { + store, + mocks, + localVue, + }) + } + + it('renders', async () => { + wrapper = await Wrapper() + expect(wrapper.findAll('.login-form')).toHaveLength(1) + }) + + it('renders with asyncData and wrong TOS Version', async () => { + asyncData = true + wrapper = await Wrapper() + expect(redirect).not.toHaveBeenCalled() + }) + + it('renders with asyncData and correct TOS Version', async () => { + asyncData = true + tosVersion = '0.0.4' + wrapper = await Wrapper() + expect(redirect).toBeCalledWith('/') + }) + }) +}) diff --git a/webapp/pages/login.vue b/webapp/pages/login.vue index d7548706c..c90b29146 100644 --- a/webapp/pages/login.vue +++ b/webapp/pages/login.vue @@ -25,9 +25,15 @@ export default { } }, methods: { - handleSuccess() { + async handleSuccess() { this.$i18n.set(this.user.locale || 'en') - this.$router.replace(this.$route.query.path || '/') + + try { + await this.$router.replace(this.$route.query.path || '/') + } catch (err) { + // throw new Error(`Problem handling something: ${err}.`); + // TODO this is causing trouble - most likely due to double redirect on terms&conditions + } }, }, } diff --git a/webapp/pages/logout.spec.js b/webapp/pages/logout.spec.js new file mode 100644 index 000000000..4ec777bf6 --- /dev/null +++ b/webapp/pages/logout.spec.js @@ -0,0 +1,43 @@ +import { mount } from '@vue/test-utils' +import Logout from './logout.vue' + +const localVue = global.localVue + +describe('logout.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $store: { + dispatch: jest.fn(), + }, + $router: { + replace: jest.fn(), + }, + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Logout, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('div')).toBe(true) + }) + + it('logs out and redirects to login', () => { + expect(mocks.$store.dispatch).toBeCalledWith('auth/logout') + expect(mocks.$router.replace).toBeCalledWith('/login') + }) + }) +}) diff --git a/webapp/pages/moderation.spec.js b/webapp/pages/moderation.spec.js new file mode 100644 index 000000000..2eeae9f7c --- /dev/null +++ b/webapp/pages/moderation.spec.js @@ -0,0 +1,34 @@ +import { config, mount } from '@vue/test-utils' +import moderation from './moderation.vue' + +config.stubs['nuxt-child'] = '' + +const localVue = global.localVue + +describe('moderation.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(moderation, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('div')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/moderation/index.spec.js b/webapp/pages/moderation/index.spec.js new file mode 100644 index 000000000..249752aa3 --- /dev/null +++ b/webapp/pages/moderation/index.spec.js @@ -0,0 +1,30 @@ +import { config, mount } from '@vue/test-utils' +import Moderation from './index.vue' + +const localVue = global.localVue +config.stubs['client-only'] = '' + +describe('moderation/index.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Moderation, { mocks, localVue }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.base-card')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/password-reset.spec.js b/webapp/pages/password-reset.spec.js new file mode 100644 index 000000000..01052e89c --- /dev/null +++ b/webapp/pages/password-reset.spec.js @@ -0,0 +1,71 @@ +import Vuex from 'vuex' +import { config, mount } from '@vue/test-utils' +import PasswordReset from './password-reset.vue' + +const localVue = global.localVue + +config.stubs['client-only'] = '' +config.stubs['nuxt-child'] = '' + +describe('password-reset.vue', () => { + let wrapper + let mocks + let asyncData + let store + let redirect + let isLoggedIn + + beforeEach(() => { + mocks = { + $t: (t) => t, + $i18n: { + locale: () => 'en', + }, + } + asyncData = false + isLoggedIn = false + redirect = jest.fn() + }) + + describe('mount', () => { + const Wrapper = async () => { + store = new Vuex.Store({ + getters: { + 'auth/isLoggedIn': () => isLoggedIn, + }, + }) + if (asyncData) { + const data = PasswordReset.data ? PasswordReset.data() : {} + const aData = await PasswordReset.asyncData({ + store, + redirect, + }) + PasswordReset.data = function () { + return { ...data, ...aData } + } + } + return mount(PasswordReset, { + mocks, + localVue, + }) + } + + it('renders', async () => { + wrapper = await Wrapper() + expect(wrapper.is('div')).toBe(true) + }) + + it('renders with asyncData and not loggedIn', async () => { + asyncData = true + wrapper = await Wrapper() + expect(redirect).not.toHaveBeenCalled() + }) + + it('renders with asyncData and loggedIn', async () => { + asyncData = true + isLoggedIn = true + wrapper = await Wrapper() + expect(redirect).toBeCalledWith('/') + }) + }) +}) diff --git a/webapp/pages/password-reset/change-password.spec.js b/webapp/pages/password-reset/change-password.spec.js new file mode 100644 index 000000000..cad031c95 --- /dev/null +++ b/webapp/pages/password-reset/change-password.spec.js @@ -0,0 +1,35 @@ +import { mount } from '@vue/test-utils' +import changePassword from './change-password.vue' + +const localVue = global.localVue + +describe('enter-nonce.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $route: { + query: jest.fn().mockResolvedValue({ email: 'peter@lustig.de', nonce: '12345' }), + }, + $apollo: { + loading: false, + }, + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(changePassword, { mocks, localVue }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.findAll('.ds-form')).toHaveLength(1) + }) + }) +}) diff --git a/webapp/pages/password-reset/enter-nonce.spec.js b/webapp/pages/password-reset/enter-nonce.spec.js new file mode 100644 index 000000000..664e1f7ca --- /dev/null +++ b/webapp/pages/password-reset/enter-nonce.spec.js @@ -0,0 +1,34 @@ +import { config, mount } from '@vue/test-utils' +import enterNonce from './enter-nonce.vue' + +const localVue = global.localVue + +config.stubs['nuxt-link'] = '' + +describe('enter-nonce.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $route: { + query: jest.fn().mockResolvedValue({ email: 'peter@lustig.de' }), + }, + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(enterNonce, { mocks, localVue }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.findAll('.ds-form')).toHaveLength(1) + }) + }) +}) diff --git a/webapp/pages/password-reset/request.spec.js b/webapp/pages/password-reset/request.spec.js new file mode 100644 index 000000000..f9bcefd79 --- /dev/null +++ b/webapp/pages/password-reset/request.spec.js @@ -0,0 +1,55 @@ +import { config, mount } from '@vue/test-utils' +import request from './request.vue' + +const localVue = global.localVue + +// config.stubs['sweetalert-icon'] = '' +config.stubs['nuxt-link'] = '' + +describe('request.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + /* $toast: { + success: jest.fn(), + error: jest.fn(), + }, */ + $t: jest.fn(), + $apollo: { + loading: false, + // mutate: jest.fn().mockResolvedValue({ data: { reqestPasswordReset: true } }), + }, + /* $router: { + push: jest.fn() + } */ + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(request, { mocks, localVue }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.findAll('.ds-form')).toHaveLength(1) + }) + + it.skip('calls "handlePasswordResetRequested" on submit', async () => { + await jest.useFakeTimers() + await wrapper.find('input#email').setValue('mail@example.org') + await wrapper.findAll('.ds-form').trigger('submit') + await jest.runAllTimers() + expect(wrapper.emitted('handleSubmitted')).toEqual([[{ email: 'mail@example.org' }]]) + expect(mocks.$router.push).toHaveBeenCalledWith({ + path: 'enter-nonce', + query: { email: 'mail@example.org' }, + }) + }) + }) +}) diff --git a/webapp/pages/post/_id.spec.js b/webapp/pages/post/_id.spec.js new file mode 100644 index 000000000..7e6812002 --- /dev/null +++ b/webapp/pages/post/_id.spec.js @@ -0,0 +1,37 @@ +import { config, mount } from '@vue/test-utils' +import _id from './_id.vue' + +const localVue = global.localVue + +config.stubs['nuxt-child'] = '' + +describe('post/_id.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $route: { + params: { + id: '1234', + slug: 'my-post', + }, + }, + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(_id, { mocks, localVue }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.findAll('.post-side-navigation')).toHaveLength(1) + }) + }) +}) diff --git a/webapp/pages/post/_id/_slug/index.spec.js b/webapp/pages/post/_id/_slug/index.spec.js index bc54edf53..4289bb53d 100644 --- a/webapp/pages/post/_id/_slug/index.spec.js +++ b/webapp/pages/post/_id/_slug/index.spec.js @@ -4,6 +4,7 @@ import Vue from 'vue' import PostSlug from './index.vue' import CommentList from '~/components/CommentList/CommentList' import HcHashtag from '~/components/Hashtag/Hashtag' +import VueMeta from 'vue-meta' config.stubs['client-only'] = '' config.stubs['nuxt-link'] = '' @@ -11,6 +12,7 @@ config.stubs['router-link'] = '' const localVue = global.localVue localVue.directive('scrollTo', jest.fn()) +localVue.use(VueMeta, { keyName: 'head' }) describe('PostSlug', () => { let wrapper, Wrapper, backendData, mocks, stubs @@ -91,6 +93,11 @@ describe('PostSlug', () => { return wrapper } + it('has correct content', async () => { + wrapper = await Wrapper() + expect(wrapper.vm.$metaInfo.title).toBe('loading') + }) + describe('given author is `null`', () => { it('does not crash', async () => { backendData = { diff --git a/webapp/pages/post/create.spec.js b/webapp/pages/post/create.spec.js new file mode 100644 index 000000000..951edba03 --- /dev/null +++ b/webapp/pages/post/create.spec.js @@ -0,0 +1,29 @@ +import { mount } from '@vue/test-utils' +import create from './create.vue' + +const localVue = global.localVue + +describe('create.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(create, { mocks, localVue }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.findAll('.contribution-form')).toHaveLength(1) + }) + }) +}) diff --git a/webapp/pages/post/edit/_id.spec.js b/webapp/pages/post/edit/_id.spec.js new file mode 100644 index 000000000..ea8ec61d8 --- /dev/null +++ b/webapp/pages/post/edit/_id.spec.js @@ -0,0 +1,82 @@ +import Vuex from 'vuex' +import { mount } from '@vue/test-utils' +import _id from './_id.vue' + +const localVue = global.localVue + +describe('post/_id.vue', () => { + let wrapper + let mocks + let store + let asyncData + let error + let userId + let authorId + + beforeEach(() => { + asyncData = false + error = jest.fn() + }) + + describe('mount', () => { + const Wrapper = async () => { + mocks = { + $t: jest.fn(), + $i18n: { + locale: () => 'en', + }, + apolloProvider: { + defaultClient: { + query: jest.fn().mockResolvedValue({ + data: { + Post: [{ author: { id: authorId } }], + }, + }), + }, + }, + } + store = new Vuex.Store({ + getters: { + 'auth/user': () => { + return { id: userId } + }, + }, + }) + if (asyncData) { + const data = _id.data ? _id.data() : {} + const aData = await _id.asyncData({ + app: mocks, + store, + error, + params: { id: '123' }, + }) + _id.data = function () { + return { ...data, ...aData } + } + } + return mount(_id, { store, mocks, localVue }) + } + + it('renders', async () => { + asyncData = false + wrapper = await Wrapper() + expect(wrapper.findAll('.contribution-form')).toHaveLength(1) + }) + + it('renders with asyncData of different users', async () => { + asyncData = true + authorId = 'some-author' + userId = 'some-user' + wrapper = await Wrapper() + expect(error).toBeCalledWith({ message: 'error-pages.cannot-edit-post', statusCode: 403 }) + }) + + it('renders with asyncData of same user', async () => { + asyncData = true + authorId = 'some-author' + userId = 'some-author' + wrapper = await Wrapper() + expect(error).not.toHaveBeenCalled() + }) + }) +}) diff --git a/webapp/pages/profile/_id.spec.js b/webapp/pages/profile/_id.spec.js new file mode 100644 index 000000000..aab216569 --- /dev/null +++ b/webapp/pages/profile/_id.spec.js @@ -0,0 +1,33 @@ +import { config, mount } from '@vue/test-utils' +import _id from './_id.vue' + +const localVue = global.localVue + +config.stubs['nuxt-child'] = '' + +describe('Profile _id.vue', () => { + let wrapper + let Wrapper + let mocks + + beforeEach(() => { + mocks = {} + }) + + describe('mount', () => { + Wrapper = () => { + return mount(_id, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.findAll('.nuxt-child')).toHaveLength(1) + }) + }) +}) diff --git a/webapp/pages/registration.spec.js b/webapp/pages/registration.spec.js index b83cb6ad4..74fb7d710 100644 --- a/webapp/pages/registration.spec.js +++ b/webapp/pages/registration.spec.js @@ -1,3 +1,4 @@ +import Vuex from 'vuex' import { config, mount } from '@vue/test-utils' import Registration from './registration.vue' import Vue from 'vue' @@ -13,6 +14,10 @@ describe('Registration', () => { let wrapper let Wrapper let mocks + let asyncData + let store + let redirect + let isLoggedIn beforeEach(() => { mocks = { @@ -25,10 +30,42 @@ describe('Registration', () => { }, $env: {}, } + asyncData = false + isLoggedIn = false + redirect = jest.fn() }) describe('mount', () => { - Wrapper = () => { + Wrapper = async () => { + if (asyncData) { + store = new Vuex.Store({ + getters: { + 'auth/isLoggedIn': () => isLoggedIn, + }, + }) + const data = { + method: mocks, + overwriteSliderData: { + collectedInputData: { + inviteCode: null, + email: null, + emailSend: !!null, + nonce: null, + }, + }, + publicRegistration: false, + inviteRegistration: false, + } + const aData = await Registration.asyncData({ + store, + redirect, + }) + Registration.data = function () { + return { ...data, ...aData } + } + } else { + Registration.data = Registration.backupData ? Registration.backupData : Registration.data + } return mount(Registration, { mocks, localVue, @@ -43,25 +80,25 @@ describe('Registration', () => { } }) - it('no "method" query in URI show "RegistrationSlideNoPublic"', () => { + it('no "method" query in URI show "RegistrationSlideNoPublic"', async () => { mocks.$route.query = {} - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.hc-empty').exists()).toBe(true) expect(wrapper.find('.enter-invite').exists()).toBe(false) expect(wrapper.find('.enter-email').exists()).toBe(false) }) describe('"method=invite-mail" in URI show "RegistrationSlideNonce"', () => { - it('no "email" query in URI', () => { + it('no "email" query in URI', async () => { mocks.$route.query = { method: 'invite-mail' } - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.enter-nonce').exists()).toBe(true) }) describe('"email=user%40example.org" query in URI', () => { - it('have email displayed', () => { + it('have email displayed', async () => { mocks.$route.query = { method: 'invite-mail', email: 'user@example.org' } - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.enter-nonce').text()).toContain('user@example.org') }) @@ -71,7 +108,7 @@ describe('Registration', () => { email: 'user@example.org', nonce: '64835', } - wrapper = Wrapper() + wrapper = await Wrapper() await Vue.nextTick() const form = wrapper.find('.enter-nonce') expect(form.vm.formData.nonce).toEqual('64835') @@ -80,15 +117,15 @@ describe('Registration', () => { }) describe('"method=invite-code" in URI show "RegistrationSlideNoPublic"', () => { - it('no "inviteCode" query in URI', () => { + it('no "inviteCode" query in URI', async () => { mocks.$route.query = { method: 'invite-code' } - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.hc-empty').exists()).toBe(true) }) - it('"inviteCode=AAAAAA" query in URI', () => { + it('"inviteCode=AAAAAA" query in URI', async () => { mocks.$route.query = { method: 'invite-code', inviteCode: 'AAAAAA' } - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.hc-empty').exists()).toBe(true) }) }) @@ -102,24 +139,24 @@ describe('Registration', () => { } }) - it('no "method" query in URI show "RegistrationSlideInvite"', () => { + it('no "method" query in URI show "RegistrationSlideInvite"', async () => { mocks.$route.query = {} - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.enter-invite').exists()).toBe(true) expect(wrapper.find('.enter-email').exists()).toBe(false) }) describe('"method=invite-mail" in URI show "RegistrationSlideNonce"', () => { - it('no "inviteCode" query in URI', () => { + it('no "inviteCode" query in URI', async () => { mocks.$route.query = { method: 'invite-mail' } - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.enter-nonce').exists()).toBe(true) }) describe('"email=user%40example.org" query in URI', () => { - it('have email displayed', () => { + it('have email displayed', async () => { mocks.$route.query = { method: 'invite-mail', email: 'user@example.org' } - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.enter-nonce').text()).toContain('user@example.org') }) @@ -129,7 +166,7 @@ describe('Registration', () => { email: 'user@example.org', nonce: '64835', } - wrapper = Wrapper() + wrapper = await Wrapper() await Vue.nextTick() const form = wrapper.find('.enter-nonce') expect(form.vm.formData.nonce).toEqual('64835') @@ -138,15 +175,15 @@ describe('Registration', () => { }) describe('"method=invite-code" in URI show "RegistrationSlideInvite"', () => { - it('no "inviteCode" query in URI', () => { + it('no "inviteCode" query in URI', async () => { mocks.$route.query = { method: 'invite-code' } - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.enter-invite').exists()).toBe(true) }) it('"inviteCode=AAAAAA" query in URI have invite code in input', async () => { mocks.$route.query = { method: 'invite-code', inviteCode: 'AAAAAA' } - wrapper = Wrapper() + wrapper = await Wrapper() await Vue.nextTick() const form = wrapper.find('.enter-invite') expect(form.vm.formData.inviteCode).toEqual('AAAAAA') @@ -162,24 +199,24 @@ describe('Registration', () => { } }) - it('no "method" query in URI show "RegistrationSlideEmail"', () => { + it('no "method" query in URI show "RegistrationSlideEmail"', async () => { mocks.$route.query = {} - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.enter-email').exists()).toBe(true) expect(wrapper.find('.enter-invite').exists()).toBe(false) }) describe('"method=invite-mail" in URI show "RegistrationSlideNonce"', () => { - it('no "inviteCode" query in URI', () => { + it('no "inviteCode" query in URI', async () => { mocks.$route.query = { method: 'invite-mail' } - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.enter-nonce').exists()).toBe(true) }) describe('"email=user%40example.org" query in URI', () => { - it('have email displayed', () => { + it('have email displayed', async () => { mocks.$route.query = { method: 'invite-mail', email: 'user@example.org' } - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.enter-nonce').text()).toContain('user@example.org') }) @@ -189,7 +226,7 @@ describe('Registration', () => { email: 'user@example.org', nonce: '64835', } - wrapper = Wrapper() + wrapper = await Wrapper() await Vue.nextTick() const form = wrapper.find('.enter-nonce') expect(form.vm.formData.nonce).toEqual('64835') @@ -198,9 +235,9 @@ describe('Registration', () => { }) describe('"method=invite-code" in URI show "RegistrationSlideEmail"', () => { - it('no "inviteCode" query in URI', () => { + it('no "inviteCode" query in URI', async () => { mocks.$route.query = { method: 'invite-code' } - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.enter-email').exists()).toBe(true) expect(wrapper.find('.enter-invite').exists()).toBe(false) }) @@ -215,24 +252,24 @@ describe('Registration', () => { } }) - it('no "method" query in URI show "RegistrationSlideEmail"', () => { + it('no "method" query in URI show "RegistrationSlideEmail"', async () => { mocks.$route.query = {} - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.enter-email').exists()).toBe(true) expect(wrapper.find('.enter-invite').exists()).toBe(false) }) describe('"method=invite-mail" in URI show "RegistrationSlideNonce"', () => { - it('no "inviteCode" query in URI', () => { + it('no "inviteCode" query in URI', async () => { mocks.$route.query = { method: 'invite-mail' } - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.enter-nonce').exists()).toBe(true) }) describe('"email=user%40example.org" query in URI', () => { - it('have email displayed', () => { + it('have email displayed', async () => { mocks.$route.query = { method: 'invite-mail', email: 'user@example.org' } - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.enter-nonce').text()).toContain('user@example.org') }) @@ -242,7 +279,7 @@ describe('Registration', () => { email: 'user@example.org', nonce: '64835', } - wrapper = Wrapper() + wrapper = await Wrapper() await Vue.nextTick() const form = wrapper.find('.enter-nonce') expect(form.vm.formData.nonce).toEqual('64835') @@ -251,15 +288,15 @@ describe('Registration', () => { }) describe('"method=invite-code" in URI show "RegistrationSlideInvite"', () => { - it('no "inviteCode" query in URI', () => { + it('no "inviteCode" query in URI', async () => { mocks.$route.query = { method: 'invite-code' } - wrapper = Wrapper() + wrapper = await Wrapper() expect(wrapper.find('.enter-invite').exists()).toBe(true) }) it('"inviteCode=AAAAAA" query in URI have invite code in input', async () => { mocks.$route.query = { method: 'invite-code', inviteCode: 'AAAAAA' } - wrapper = Wrapper() + wrapper = await Wrapper() await Vue.nextTick() const form = wrapper.find('.enter-invite') expect(form.vm.formData.inviteCode).toEqual('AAAAAA') @@ -267,6 +304,25 @@ describe('Registration', () => { }) }) + it('renders', async () => { + wrapper = await Wrapper() + expect(wrapper.is('.login-form')).toBe(true) + }) + + // The asyncTests must go last + it('renders with asyncData and not loggedIn', async () => { + asyncData = true + wrapper = await Wrapper() + expect(redirect).not.toHaveBeenCalled() + }) + + it('renders with asyncData and loggedIn', async () => { + asyncData = true + isLoggedIn = true + wrapper = await Wrapper() + expect(redirect).toBeCalledWith('/') + }) + // copied from webapp/components/Registration/Signup.spec.js as testing template // describe('with invitation code', () => { // let action diff --git a/webapp/pages/search/search-results.spec.js b/webapp/pages/search/search-results.spec.js new file mode 100644 index 000000000..c594f3e56 --- /dev/null +++ b/webapp/pages/search/search-results.spec.js @@ -0,0 +1,61 @@ +import { config, mount } from '@vue/test-utils' +import searchResults from './search-results.vue' +import VueMeta from 'vue-meta' + +const localVue = global.localVue +localVue.use(VueMeta, { keyName: 'head' }) + +config.stubs['client-only'] = '' + +describe('search-results.vue', () => { + let wrapper + let mocks + let asyncData + let query + + beforeEach(() => { + mocks = { + $t: (t) => t, + } + asyncData = false + query = {} + }) + + describe('mount', () => { + const Wrapper = async () => { + if (asyncData) { + const data = searchResults.data ? searchResults.data() : {} + const aData = await searchResults.asyncData({ + query, + }) + searchResults.data = function () { + return { ...data, ...aData } + } + } + return mount(searchResults, { mocks, localVue }) + } + + it('renders', async () => { + wrapper = await Wrapper() + expect(wrapper.findAll('.search-results')).toHaveLength(1) + }) + + it('has correct content', async () => { + wrapper = await Wrapper() + expect(wrapper.vm.$metaInfo.title).toBe('search.title') + }) + + it('renders with asyncData and no query', async () => { + asyncData = true + wrapper = await Wrapper() + expect(wrapper.vm.search).toBe(null) + }) + + it('renders with asyncData and query', async () => { + asyncData = true + query = { search: 'hello' } + wrapper = await Wrapper() + expect(wrapper.vm.search).toBe('hello') + }) + }) +}) diff --git a/webapp/pages/settings.spec.js b/webapp/pages/settings.spec.js new file mode 100644 index 000000000..353f1e6b8 --- /dev/null +++ b/webapp/pages/settings.spec.js @@ -0,0 +1,34 @@ +import { config, mount } from '@vue/test-utils' +import settings from './settings.vue' + +const localVue = global.localVue + +config.stubs['nuxt-child'] = '' + +describe('settings.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(settings, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('div')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/settings/data-download.spec.js b/webapp/pages/settings/data-download.spec.js new file mode 100644 index 000000000..b50c8d046 --- /dev/null +++ b/webapp/pages/settings/data-download.spec.js @@ -0,0 +1,32 @@ +import { mount } from '@vue/test-utils' +import DataDownload from './data-download.vue' + +const localVue = global.localVue + +describe('data-download.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(DataDownload, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.base-card')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/settings/delete-account.spec.js b/webapp/pages/settings/delete-account.spec.js new file mode 100644 index 000000000..aa8ffd954 --- /dev/null +++ b/webapp/pages/settings/delete-account.spec.js @@ -0,0 +1,42 @@ +import Vuex from 'vuex' +import { mount } from '@vue/test-utils' +import DeleteAccount from './delete-account.vue' + +const localVue = global.localVue + +describe('delete-account.vue', () => { + let wrapper + let mocks + let store + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + store = new Vuex.Store({ + getters: { + 'auth/user': () => { + return { id: 'u343', name: 'Delete MyAccount' } + }, + }, + }) + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(DeleteAccount, { + store, + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.delete-data')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/settings/embeds.spec.js b/webapp/pages/settings/embeds.spec.js new file mode 100644 index 000000000..75247ddf0 --- /dev/null +++ b/webapp/pages/settings/embeds.spec.js @@ -0,0 +1,42 @@ +import Vuex from 'vuex' +import { mount } from '@vue/test-utils' +import Embeds from './embeds.vue' + +const localVue = global.localVue + +describe('embeds.vue', () => { + let wrapper + let mocks + let store + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + store = new Vuex.Store({ + getters: { + 'auth/user': () => { + return { id: 'u343', name: 'Delete MyAccount', allowEmbedIframes: true } + }, + }, + }) + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Embeds, { + store, + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.base-card')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/settings/invites.spec.js b/webapp/pages/settings/invites.spec.js new file mode 100644 index 000000000..cbc8d1765 --- /dev/null +++ b/webapp/pages/settings/invites.spec.js @@ -0,0 +1,32 @@ +import { mount } from '@vue/test-utils' +import Invites from './invites.vue' + +const localVue = global.localVue + +describe('invites.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Invites, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.base-card')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/settings/languages.spec.js b/webapp/pages/settings/languages.spec.js new file mode 100644 index 000000000..0e3665739 --- /dev/null +++ b/webapp/pages/settings/languages.spec.js @@ -0,0 +1,32 @@ +import { mount } from '@vue/test-utils' +import Languages from './languages.vue' + +const localVue = global.localVue + +describe('languages.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Languages, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.base-card')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/settings/my-email-address/index.spec.js b/webapp/pages/settings/my-email-address/index.spec.js index 22654afd0..808aee3be 100644 --- a/webapp/pages/settings/my-email-address/index.spec.js +++ b/webapp/pages/settings/my-email-address/index.spec.js @@ -111,6 +111,21 @@ describe('EmailSettingsIndexPage', () => { expect(wrapper.text()).toContain('registration.signup.form.errors.email-exists') }) }) + + describe('if backend sends any other error', () => { + beforeEach(() => { + mocks.$apollo.mutate = jest.fn().mockRejectedValue({ + message: 'Ouch!', + }) + wrapper = Wrapper() + wrapper.find('#email').setValue('already-taken@example.org') + wrapper.find('form').trigger('submit') + }) + + it('display a toast error', () => { + expect(mocks.$toast.error).toHaveBeenCalled() + }) + }) }) }) }) diff --git a/webapp/pages/settings/my-organizations.spec.js b/webapp/pages/settings/my-organizations.spec.js new file mode 100644 index 000000000..7f11b9871 --- /dev/null +++ b/webapp/pages/settings/my-organizations.spec.js @@ -0,0 +1,32 @@ +import { mount } from '@vue/test-utils' +import MyOrganizations from './my-organizations.vue' + +const localVue = global.localVue + +describe('my-organizations.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(MyOrganizations, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.base-card')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/settings/privacy.spec.js b/webapp/pages/settings/privacy.spec.js new file mode 100644 index 000000000..eb9cb90b3 --- /dev/null +++ b/webapp/pages/settings/privacy.spec.js @@ -0,0 +1,70 @@ +import Vuex from 'vuex' +import { mount } from '@vue/test-utils' +import Privacy from './privacy.vue' + +const localVue = global.localVue + +describe('privacy.vue', () => { + let wrapper + let mocks + let store + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $apollo: { + mutate: jest.fn(), + }, + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + } + store = new Vuex.Store({ + getters: { + 'auth/user': () => { + return { + id: 'u343', + name: 'MyAccount', + showShoutsPublicly: true, + } + }, + }, + }) + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Privacy, { + store, + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.base-card')).toBe(true) + }) + + it('clicking on submit changes shoutsAllowed to false', async () => { + wrapper.find('#allow-shouts').trigger('click') + await wrapper.vm.$nextTick() + wrapper.find('.base-button').trigger('click') + expect(wrapper.vm.shoutsAllowed).toBe(false) + }) + + it('clicking on submit with a server error shows a toast and shoutsAllowed is still true', async () => { + mocks.$apollo.mutate = jest.fn().mockRejectedValue({ message: 'Ouch!' }) + wrapper.find('#allow-shouts').trigger('click') + await wrapper.vm.$nextTick() + await wrapper.find('.base-button').trigger('click') + await wrapper.vm.$nextTick() + expect(mocks.$toast.error).toHaveBeenCalledWith('Ouch!') + expect(wrapper.vm.shoutsAllowed).toBe(true) + }) + }) +}) diff --git a/webapp/pages/settings/privacy.vue b/webapp/pages/settings/privacy.vue index 14f27bf8f..71fd31946 100644 --- a/webapp/pages/settings/privacy.vue +++ b/webapp/pages/settings/privacy.vue @@ -52,6 +52,7 @@ export default { }, }) } catch (error) { + this.shoutsAllowed = !this.shoutsAllowed this.$toast.error(error.message) } }, diff --git a/webapp/pages/settings/security.spec.js b/webapp/pages/settings/security.spec.js new file mode 100644 index 000000000..dee9e640a --- /dev/null +++ b/webapp/pages/settings/security.spec.js @@ -0,0 +1,32 @@ +import { mount } from '@vue/test-utils' +import Security from './security.vue' + +const localVue = global.localVue + +describe('security.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Security, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.base-card')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/terms-and-conditions-confirm.spec.js b/webapp/pages/terms-and-conditions-confirm.spec.js new file mode 100644 index 000000000..098e73a92 --- /dev/null +++ b/webapp/pages/terms-and-conditions-confirm.spec.js @@ -0,0 +1,74 @@ +import Vuex from 'vuex' +import { config, mount } from '@vue/test-utils' +import TermsAndConditionsConfirm from './terms-and-conditions-confirm.vue' +import VueMeta from 'vue-meta' + +const localVue = global.localVue +localVue.use(VueMeta, { keyName: 'head' }) + +config.stubs['nuxt-link'] = '' + +describe('terms-and-conditions-confirm.vue', () => { + let wrapper + let mocks + let store + let asyncData + let tosAgree + let redirect + + beforeEach(() => { + mocks = { + $t: (t) => t, + } + asyncData = false + tosAgree = false + redirect = jest.fn() + }) + + describe('mount', () => { + const Wrapper = async () => { + store = new Vuex.Store({ + getters: { + 'auth/termsAndConditionsAgreed': () => tosAgree, + }, + }) + if (asyncData) { + const data = TermsAndConditionsConfirm.data ? TermsAndConditionsConfirm.data() : {} + const aData = await TermsAndConditionsConfirm.asyncData({ + store, + redirect, + }) + TermsAndConditionsConfirm.data = function () { + return { ...data, ...aData } + } + } + return mount(TermsAndConditionsConfirm, { + mocks, + localVue, + }) + } + + it('renders', async () => { + wrapper = await Wrapper() + expect(wrapper.is('div')).toBe(true) + }) + + it('has correct content', async () => { + wrapper = await Wrapper() + expect(wrapper.vm.$metaInfo.title).toBe('termsAndConditions.newTermsAndConditions') + }) + + it('renders with asyncData and did not agree to TOS', async () => { + asyncData = true + wrapper = await Wrapper() + expect(redirect).not.toHaveBeenCalled() + }) + + it('renders with asyncData and did agree to TOS', async () => { + asyncData = true + tosAgree = true + wrapper = await Wrapper() + expect(redirect).toBeCalledWith('/') + }) + }) +}) diff --git a/webapp/pages/terms-and-conditions.spec.js b/webapp/pages/terms-and-conditions.spec.js new file mode 100644 index 000000000..d6ae6dce7 --- /dev/null +++ b/webapp/pages/terms-and-conditions.spec.js @@ -0,0 +1,38 @@ +import { mount } from '@vue/test-utils' +import TermsAndConditions from './terms-and-conditions.vue' +import VueMeta from 'vue-meta' + +const localVue = global.localVue +localVue.use(VueMeta, { keyName: 'head' }) + +describe('terms-and-conditions.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: (t) => t, + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(TermsAndConditions, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('div')).toBe(true) + }) + + it('has correct content', () => { + expect(wrapper.vm.$metaInfo.title).toBe('site.termsAndConditions') + }) + }) +}) diff --git a/webapp/test/fileMock.js b/webapp/test/fileMock.js index 0e56c5b5f..c77f5e0de 100644 --- a/webapp/test/fileMock.js +++ b/webapp/test/fileMock.js @@ -1 +1,3 @@ -module.exports = 'test-file-stub' +module.exports = { + render: () => 'test-file-stub', +} diff --git a/yarn.lock b/yarn.lock index 06e855034..15394a334 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1204,7 +1204,7 @@ lodash.clonedeep "4.5.0" watchify "3.11.1" -"@cypress/listr-verbose-renderer@0.4.1": +"@cypress/listr-verbose-renderer@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a" integrity sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo= @@ -1214,7 +1214,33 @@ date-fns "^1.27.2" figures "^1.7.0" -"@cypress/xvfb@1.2.4": +"@cypress/request@^2.88.5": + version "2.88.5" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7" + integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +"@cypress/xvfb@^1.2.4": version "1.2.4" resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a" integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== @@ -1301,7 +1327,17 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/sizzle@2.3.2": +"@types/node@^14.14.31": + version "14.14.37" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e" + integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== + +"@types/sinonjs__fake-timers@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz#3a84cf5ec3249439015e14049bd3161419bf9eae" + integrity sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg== + +"@types/sizzle@^2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== @@ -1446,10 +1482,10 @@ aproba@^1.0.3: resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -arch@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.1.tgz#8f5c2731aa35a30929221bb0640eed65175ec84e" - integrity sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg== +arch@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" + integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== are-we-there-yet@~1.1.2: version "1.1.5" @@ -1567,16 +1603,21 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.2.tgz#8b8a7ca2a658f927e9f307d6d1a42f4199f0f735" integrity sha512-6xrbvN0MOBKSJDdonmSSz2OwFSgxRaVtBDes26mj9KIGtDo+g9xosFRSC+i1gQh2oAN/tQ62AI/pGZGQjVOiRg== -async@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/async/-/async-3.1.1.tgz#dd3542db03de837979c9ebbca64ca01b06dc98df" - integrity sha512-X5Dj8hK1pJNC2Wzo2Rcp9FBVdJMGRR/S7V+lH46s8GVFhtbo5O4Le5GECCF/8PISVdkUA6mMPvgz7qTTD1rf1g== +async@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" + integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + atob@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -1676,12 +1717,17 @@ binary-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.0.tgz#9523e001306a32444b907423f1de2164222f6ab1" integrity sha512-EgmjVLMn22z7eGGv3kcnHwSnJXmFHjISTY9E/S5lIcTD3Oxw05QTcBLNkJFzcb3cNueUdF/IN4U+d78V0zO8Hw== +blob-util@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" + integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== + bluebird@3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== -bluebird@3.7.2, bluebird@^3.4.1: +bluebird@^3.4.1, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -1923,7 +1969,7 @@ cached-path-relative@^1.0.0: resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.2.tgz#a13df4196d26776220cc3356eb147a52dba2c6db" integrity sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg== -cachedir@2.3.0: +cachedir@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== @@ -1950,15 +1996,6 @@ chai@^4.1.2: pathval "^1.1.0" type-detect "^4.0.5" -chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - chalk@^1.0.0, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -1970,6 +2007,15 @@ chalk@^1.0.0, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@^2.0.0, chalk@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -1978,12 +2024,20 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + check-error@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= -check-more-types@2.24.0: +check-more-types@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA= @@ -2012,10 +2066,10 @@ chownr@^1.1.1: resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +ci-info@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.1.1.tgz#9a32fcefdf7bcdb6f0a7e1c0f8098ec57897b80a" + integrity sha512-kdRWLBIJwdsYJWYJFtAFFYxybguqeF91qpZaggjG5Nf8QKdizFG2hjqvaTXbxFIcYbSaD74KpAXv6BSm17DHEQ== cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" @@ -2049,7 +2103,7 @@ cli-cursor@^2.0.0, cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" -cli-table3@0.5.1, cli-table3@^0.5.1: +cli-table3@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== @@ -2059,6 +2113,16 @@ cli-table3@0.5.1, cli-table3@^0.5.1: optionalDependencies: colors "^1.1.2" +cli-table3@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" + integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ== + dependencies: + object-assign "^4.1.0" + string-width "^4.2.0" + optionalDependencies: + colors "^1.1.2" + cli-table@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" @@ -2167,11 +2231,6 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83" - integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw== - commander@^2.9.0, commander@~2.20.3: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -2187,7 +2246,12 @@ commander@^5.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.0.0.tgz#dbf1909b49e5044f8fdaf0adc809f0c0722bdfd0" integrity sha512-JrDGPAKjMGSP1G0DUoaceEJ3DZgAfr/q6X7FVk4+U5KxUSKviYGM2k6zWkfyyBHy5rAtzgYJFa1ro2O9PtoxwQ== -common-tags@1.8.0: +commander@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + +common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== @@ -2207,7 +2271,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@1.6.2, concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@~1.6.0: +concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@^1.6.2, concat-stream@~1.6.0: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -2325,7 +2389,7 @@ cross-fetch@2.2.2: node-fetch "2.1.2" whatwg-fetch "2.0.4" -cross-spawn@^6.0.0, cross-spawn@^6.0.5: +cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -2336,6 +2400,15 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^7.0.0: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + cross-spawn@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14" @@ -2485,54 +2558,50 @@ cypress-file-upload@^3.5.3: resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-3.5.3.tgz#cd706485de3fb2cbd4a8c2dd90fe96d537bb4311" integrity sha512-S/czzqAj1BYz6Xxnfpx2aSc6hXsj76fd8/iuycJ2RxoxCcQMliw8eQV0ugzVlkzr1GD5dKGviNFGYqv3nRJ+Tg== -cypress-plugin-retries@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/cypress-plugin-retries/-/cypress-plugin-retries-1.5.2.tgz#21d5247cd77013b95bbfdd914f2de66f91f76a2e" - integrity sha512-o1xVIGtv4WvNVxoVJ2X08eAuvditPHrePRzHqhwwHbMKu3C2rtxCdanRCZdO5fjh8ww+q4v4V0e9GmysbOvu3A== +cypress@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-7.0.1.tgz#8603f84d828fd4c5462a856f55cef5642e4ce573" + integrity sha512-dMZmZDo+x3jslEQiXRGQlMmMVMhe4JpMHQ6g1unMGXTUsapU1EXlnubevmKmqZ1IQpntAlDKmx8dupOTd3oW+g== dependencies: - chalk "^3.0.0" - -cypress@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.2.0.tgz#45673fb648b1a77b9a78d73e58b89ed05212d243" - integrity sha512-8LdreL91S/QiTCLYLNbIjLL8Ht4fJmu/4HGLxUI20Tc7JSfqEfCmXELrRfuPT0kjosJwJJZacdSji9XSRkPKUw== - dependencies: - "@cypress/listr-verbose-renderer" "0.4.1" - "@cypress/xvfb" "1.2.4" - "@types/sizzle" "2.3.2" - arch "2.1.1" - bluebird "3.7.2" - cachedir "2.3.0" - chalk "2.4.2" - check-more-types "2.24.0" - cli-table3 "0.5.1" - commander "4.1.0" - common-tags "1.8.0" - debug "4.1.1" - eventemitter2 "4.1.2" - execa "1.0.0" - executable "4.1.1" - extract-zip "1.6.7" - fs-extra "8.1.0" - getos "3.1.4" - is-ci "2.0.0" - is-installed-globally "0.1.0" - lazy-ass "1.6.0" - listr "0.14.3" - lodash "4.17.15" - log-symbols "3.0.0" - minimist "1.2.2" - moment "2.24.0" - ospath "1.2.2" - pretty-bytes "5.3.0" - ramda "0.26.1" - request cypress-io/request#b5af0d1fa47eec97ba980cde90a13e69a2afcd16 - request-progress "3.0.0" - supports-color "7.1.0" - tmp "0.1.0" - untildify "4.0.0" - url "0.11.0" - yauzl "2.10.0" + "@cypress/listr-verbose-renderer" "^0.4.1" + "@cypress/request" "^2.88.5" + "@cypress/xvfb" "^1.2.4" + "@types/node" "^14.14.31" + "@types/sinonjs__fake-timers" "^6.0.2" + "@types/sizzle" "^2.3.2" + arch "^2.2.0" + blob-util "^2.0.2" + bluebird "^3.7.2" + cachedir "^2.3.0" + chalk "^4.1.0" + check-more-types "^2.24.0" + cli-table3 "~0.6.0" + commander "^5.1.0" + common-tags "^1.8.0" + dayjs "^1.10.4" + debug "4.3.2" + eventemitter2 "^6.4.3" + execa "4.1.0" + executable "^4.1.1" + extract-zip "^1.7.0" + fs-extra "^9.1.0" + getos "^3.2.1" + is-ci "^3.0.0" + is-installed-globally "~0.4.0" + lazy-ass "^1.6.0" + listr "^0.14.3" + lodash "^4.17.21" + log-symbols "^4.0.0" + minimist "^1.2.5" + ospath "^1.2.2" + pretty-bytes "^5.6.0" + ramda "~0.27.1" + request-progress "^3.0.0" + supports-color "^8.1.1" + tmp "~0.2.1" + untildify "^4.0.0" + url "^0.11.0" + yauzl "^2.10.0" d@1: version "1.0.0" @@ -2568,12 +2637,10 @@ date-now@^0.1.4: resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= -debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" +dayjs@^1.10.4: + version "1.10.4" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2" + integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw== debug@4, debug@4.1.1, debug@^4.1.0: version "4.1.1" @@ -2582,6 +2649,20 @@ debug@4, debug@4.1.1, debug@^4.1.0: dependencies: ms "^2.1.1" +debug@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== + dependencies: + ms "2.1.2" + +debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + debug@^3.0.1, debug@^3.1.0: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" @@ -2774,6 +2855,11 @@ elliptic@^6.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -2867,10 +2953,10 @@ esutils@^2.0.0, esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= -eventemitter2@4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-4.1.2.tgz#0e1a8477af821a6ef3995b311bf74c23a5247f15" - integrity sha1-DhqEd6+CGm7zmVsxG/dMI6UkfxU= +eventemitter2@^6.4.3: + version "6.4.4" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.4.tgz#aa96e8275c4dbeb017a5d0e03780c65612a1202b" + integrity sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw== events@^2.0.0: version "2.1.0" @@ -2885,20 +2971,22 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" -execa@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== +execa@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" -executable@4.1.1: +executable@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== @@ -2969,15 +3057,15 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-zip@1.6.7: - version "1.6.7" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" - integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k= +extract-zip@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" + integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== dependencies: - concat-stream "1.6.2" - debug "2.6.9" - mkdirp "0.5.1" - yauzl "2.4.1" + concat-stream "^1.6.2" + debug "^2.6.9" + mkdirp "^0.5.4" + yauzl "^2.10.0" extsprintf@1.3.0: version "1.3.0" @@ -3003,13 +3091,6 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= -fd-slicer@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" - integrity sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU= - dependencies: - pend "~1.2.0" - fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -3107,14 +3188,15 @@ fs-extra@7.0.1: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== dependencies: + at-least-node "^1.0.0" graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" + jsonfile "^6.0.1" + universalify "^2.0.0" fs-minipass@^1.2.5: version "1.2.5" @@ -3170,10 +3252,10 @@ get-func-name@^2.0.0: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= -get-stream@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== +get-stream@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== dependencies: pump "^3.0.0" @@ -3182,12 +3264,12 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= -getos@3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/getos/-/getos-3.1.4.tgz#29cdf240ed10a70c049add7b6f8cb08c81876faf" - integrity sha512-UORPzguEB/7UG5hqiZai8f0vQ7hzynMQyJLxStoQ8dPGAcmgsfXOPA4iE/fGtweHYkK+z4zc9V0g+CIFRf5HYw== +getos@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" + integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== dependencies: - async "^3.1.0" + async "^3.2.0" getpass@^0.1.1: version "0.1.7" @@ -3226,12 +3308,12 @@ glob@^7.0.0, glob@^7.1.0, glob@^7.1.2, glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -global-dirs@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" - integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU= +global-dirs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686" + integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA== dependencies: - ini "^1.3.4" + ini "2.0.0" globals@^11.1.0: version "11.11.0" @@ -3409,6 +3491,11 @@ https-proxy-agent@^4.0.0: agent-base "5" debug "4" +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -3468,7 +3555,12 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -ini@^1.3.4, ini@~1.3.0: +ini@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + +ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== @@ -3539,12 +3631,12 @@ is-callable@^1.1.4: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== -is-ci@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== +is-ci@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.0.tgz#c7e7be3c9d8eef7d0fa144390bd1e4b88dc4c994" + integrity sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ== dependencies: - ci-info "^2.0.0" + ci-info "^3.1.1" is-data-descriptor@^0.1.4: version "0.1.4" @@ -3617,6 +3709,11 @@ is-fullwidth-code-point@^2.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-generator@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/is-generator/-/is-generator-1.0.3.tgz#c14c21057ed36e328db80347966c693f886389f3" @@ -3636,13 +3733,13 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" -is-installed-globally@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" - integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA= +is-installed-globally@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== dependencies: - global-dirs "^0.1.0" - is-path-inside "^1.0.0" + global-dirs "^3.0.0" + is-path-inside "^3.0.2" is-number@^3.0.0: version "3.0.0" @@ -3663,12 +3760,10 @@ is-observable@^1.1.0: dependencies: symbol-observable "^1.1.0" -is-path-inside@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" - integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= - dependencies: - path-is-inside "^1.0.1" +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" @@ -3711,6 +3806,11 @@ is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -3870,6 +3970,15 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -3963,7 +4072,7 @@ labeled-stream-splicer@^2.0.0: isarray "^2.0.4" stream-splicer "^2.0.0" -lazy-ass@1.6.0: +lazy-ass@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM= @@ -3997,7 +4106,7 @@ listr-verbose-renderer@^0.5.0: date-fns "^1.27.2" figures "^2.0.0" -listr@0.14.3: +listr@^0.14.3: version "0.14.3" resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== @@ -4080,22 +4189,15 @@ lodash.uniqby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI= -lodash@4.17.15: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== - lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.4: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== -log-symbols@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" - integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== - dependencies: - chalk "^2.4.2" +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== log-symbols@^1.0.2: version "1.0.2" @@ -4104,6 +4206,14 @@ log-symbols@^1.0.2: dependencies: chalk "^1.0.0" +log-symbols@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + log-update@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708" @@ -4159,6 +4269,11 @@ memorystream@^0.3.1: resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI= +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -4211,6 +4326,11 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -4233,11 +4353,6 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.2.tgz#b00a00230a1108c48c169e69a291aafda3aacd63" - integrity sha512-rIqbOrKb8GJmx/5bc2M0QchhUouMXSpd1RTclXsB41JdL+VtnojfaJR+h7F9k18/4kHUsBFgk80Uk+q569vjPA== - minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" @@ -4271,13 +4386,20 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1: +mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= dependencies: minimist "0.0.8" +mkdirp@^0.5.4: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + mock-socket@^9.0.3: version "9.0.3" resolved "https://registry.yarnpkg.com/mock-socket/-/mock-socket-9.0.3.tgz#4bc6d2aea33191e4fed5ec71f039e2bbeb95e414" @@ -4306,17 +4428,12 @@ module-deps@^6.0.0: through2 "^2.0.0" xtend "^4.0.0" -moment@2.24.0: - version "2.24.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" - integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@^2.1.1: +ms@2.1.2, ms@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== @@ -4506,12 +4623,12 @@ npm-run-all@^4.1.5: shell-quote "^1.6.1" string.prototype.padend "^3.0.0" -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= +npm-run-path@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== dependencies: - path-key "^2.0.0" + path-key "^3.0.0" npmlog@^4.0.2: version "4.1.2" @@ -4600,6 +4717,13 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + optimist@0.3.x: version "0.3.7" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9" @@ -4638,7 +4762,7 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -ospath@1.2.2: +ospath@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs= @@ -4650,11 +4774,6 @@ outpipe@^1.1.0: dependencies: shell-quote "^1.4.2" -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - p-limit@^2.0.0: version "2.2.1" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537" @@ -4748,16 +4867,16 @@ path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -path-is-inside@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= - -path-key@^2.0.0, path-key@^2.0.1: +path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-key@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + path-key@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.0.tgz#99a10d870a803bdd5ee6f0470e58dfcd2f9a54d3" @@ -4850,10 +4969,10 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= -pretty-bytes@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" - integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg== +pretty-bytes@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== pretty-format@^25.3.0: version "25.3.0" @@ -4945,10 +5064,10 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== -ramda@0.26.1: - version "0.26.1" - resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" - integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== +ramda@~0.27.1: + version "0.27.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9" + integrity sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw== randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.1.0" @@ -5100,38 +5219,13 @@ repeat-string@^1.5.2, repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -request-progress@3.0.0: +request-progress@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4= dependencies: throttleit "^1.0.0" -request@cypress-io/request#b5af0d1fa47eec97ba980cde90a13e69a2afcd16: - version "2.88.1" - resolved "https://codeload.github.com/cypress-io/request/tar.gz/b5af0d1fa47eec97ba980cde90a13e69a2afcd16" - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - require-from-string@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" @@ -5180,13 +5274,20 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -rimraf@^2.6.1, rimraf@^2.6.3: +rimraf@^2.6.1: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== dependencies: glob "^7.1.3" +rimraf@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -5591,6 +5692,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^4.2.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + string.prototype.padend@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0" @@ -5628,15 +5738,22 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== strip-json-comments@~2.0.1: version "2.0.1" @@ -5655,13 +5772,6 @@ subarg@^1.0.0: dependencies: minimist "^1.1.0" -supports-color@7.1.0, supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== - dependencies: - has-flag "^4.0.0" - supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -5674,6 +5784,20 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + symbol-observable@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" @@ -5762,12 +5886,12 @@ title-case@^2.1.1: no-case "^2.2.0" upper-case "^1.0.3" -tmp@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877" - integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw== +tmp@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== dependencies: - rimraf "^2.6.3" + rimraf "^3.0.0" to-arraybuffer@^1.0.0: version "1.0.1" @@ -5923,6 +6047,11 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" @@ -5931,7 +6060,7 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" -untildify@4.0.0: +untildify@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== @@ -5966,7 +6095,7 @@ url-parse@^1.4.4: querystringify "^2.1.1" requires-port "^1.0.0" -url@0.11.0, url@~0.11.0: +url@^0.11.0, url@~0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= @@ -6109,17 +6238,10 @@ yallist@^3.0.0, yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== -yauzl@2.10.0: +yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" - -yauzl@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" - integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU= - dependencies: - fd-slicer "~1.0.1"