diff --git a/.circleci/config.yml b/.circleci/config.yml index 538b4b8d92..a945d51a1a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -280,10 +280,12 @@ jobs: name: Set branch environment command: | echo 'export VITE_PROJECT_VERSION=${CIRCLE_SHA1}' >> $BASH_ENV + echo 'export VITE_SITE_VARIANT=test-ci' >> $BASH_ENV - run: name: Check environment variables command: | echo $VITE_PROJECT_VERSION + echo $VITE_SITE_VARIANT - run: command: yarn build - persist_to_workspace: @@ -356,7 +358,7 @@ jobs: --build-secret VITE_SENTRY_DSN="$VITE_SENTRY_DSN" \ --build-secret VITE_GA_TRACKING_ID="$VITE_GA_TRACKING_ID" \ --build-secret VITE_PATREON_CLIENT_ID="$VITE_PATREON_CLIENT_ID" \ - --build-secret VITE_PLATFORM_THEME="$VITE_PLATFORM_THEME" \ + --build-secret VITE_PLATFORM_PROFILES="$VITE_PLATFORM_PROFILES" \ --build-secret VITE_PROJECT_VERSION="$VITE_PROJECT_VERSION" \ --build-secret VITE_SUPPORTED_MODULES="$VITE_SUPPORTED_MODULES" \ --build-secret VITE_ACADEMY_RESOURCE="$VITE_ACADEMY_RESOURCE" \ @@ -401,6 +403,7 @@ jobs: - run: command: npm run test ci prod environment: + VITE_SITE_VARIANT: test-ci CI_BROWSER: << parameters.CI_BROWSER >> CI_NODE: << parameters.CI_NODE >> CI_GROUP: ci-<< parameters.CI_BROWSER >> diff --git a/.env b/.env index 315ce51557..c24465c878 100644 --- a/.env +++ b/.env @@ -1,9 +1,10 @@ ### Prefix VITE_ to use client-side (only for non-sensitive data!) PORT=3000 WS_URLS=localhost:24678,ws://localhost:24678 + +# All valid options for VITE_THEME: project-kamp, fixing-fashion or precious-plastic VITE_THEME=precious-plastic -# VITE_THEME=fixing-fashion -# VITE_THEME=project-kamp + VITE_ACADEMY_RESOURCE=https://onearmy.github.io/academy/ # VITE_ACADEMY_RESOURCE=https://project-kamp-academy.netlify.app/ # VITE_ACADEMY_RESOURCE=https://fixing-fashion-academy.netlify.app/ @@ -17,4 +18,8 @@ VITE_COMMUNITY_PROGRAM_URL=https://community.preciousplastic.com/academy/guides/ VITE_PROFILE_GUIDELINES_URL=https://community.preciousplastic.com/academy/guides/platform # VITE_PROFILE_GUIDELINES_URL=https://drive.google.com/file/d/1fXTtBbzgCO0EL6G9__aixwqc-Euqgqnd/view # VITE_PROFILE_GUIDELINES_URL=https://community.fixing.fashion/academy/guides/profile -VITE_QUESTIONS_GUIDELINES_URL=https://community.preciousplastic.com/academy/guides/guidelines-questions \ No newline at end of file +VITE_QUESTIONS_GUIDELINES_URL=https://community.preciousplastic.com/academy/guides/guidelines-questions + +# For testing, VITE_PLATFORM_PROFILES in localStorage is prioritised over this value +# All valid options for VITE_PLATFORM_PROFILES: "member,workspace,community-builder,space,collection-point,machine-builder" +VITE_PLATFORM_PROFILES="member,workspace,community-builder,collection-point,machine-builder" diff --git a/.github/actions/destroy-fly-preview-app/Dockerfile b/.github/actions/destroy-fly-preview-app/Dockerfile new file mode 100644 index 0000000000..1c8e77415b --- /dev/null +++ b/.github/actions/destroy-fly-preview-app/Dockerfile @@ -0,0 +1,11 @@ +FROM alpine + +RUN apk add --no-cache curl jq + +RUN curl -L https://fly.io/install.sh | FLYCTL_INSTALL=/usr/local sh + +COPY entrypoint.sh /entrypoint.sh + +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/.github/actions/destroy-fly-preview-app/action.yml b/.github/actions/destroy-fly-preview-app/action.yml new file mode 100644 index 0000000000..b833f222c4 --- /dev/null +++ b/.github/actions/destroy-fly-preview-app/action.yml @@ -0,0 +1,12 @@ +name: "Destroy fly.io app" +description: "Destroy fly.io app if matching app name found" +author: iSCJT +branding: + icon: "delete" + color: "red" +runs: + using: "docker" + image: "Dockerfile" +inputs: + name: + description: Fly app name \ No newline at end of file diff --git a/.github/actions/destroy-fly-preview-app/entrypoint.sh b/.github/actions/destroy-fly-preview-app/entrypoint.sh new file mode 100644 index 0000000000..aa4b5696ee --- /dev/null +++ b/.github/actions/destroy-fly-preview-app/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh -l + +set -ex + +# Change underscores to hyphens. +app="${INPUT_NAME//_/-}" + + +if ! flyctl status --app "$app"; then + echo "App name not found" + exit 1 +fi + +flyctl apps destroy "$app" -y +echo "App $app successfully destroyed" +exit 0 \ No newline at end of file diff --git a/.github/workflows/fly-pr-preview.yml b/.github/workflows/pr-preview-fly-deploy.yml similarity index 90% rename from .github/workflows/fly-pr-preview.yml rename to .github/workflows/pr-preview-fly-deploy.yml index 780c4a678b..f5eb55fe49 100644 --- a/.github/workflows/fly-pr-preview.yml +++ b/.github/workflows/pr-preview-fly-deploy.yml @@ -1,4 +1,4 @@ -name: Fly PR Preview +name: Deploy Fly PR Preview on: # Run this workflow on every PR event. Existing review apps will be updated when the PR is updated. pull_request_target: @@ -16,7 +16,7 @@ on: env: FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} # Set these to your Fly.io organization and preferred region. - FLY_REGION: cdg + FLY_REGION: ams FLY_ORG: one-army jobs: @@ -45,8 +45,7 @@ jobs: - name: Deploy PR app to Fly.io id: deploy - uses: superfly/fly-pr-review-apps@1.2.1 + uses: superfly/fly-pr-review-apps@1.3.0 with: config: fly-preview.toml - name: community-platform-pr-${{ github.event.number }} - secrets: VITE_SITE_VARIANT=preview VITE_PROJECT_VERSION=${GITHUB_SHA} + name: community-platform-pr-${{ github.event.number }} \ No newline at end of file diff --git a/.github/workflows/pr-preview-fly-destroy.yml b/.github/workflows/pr-preview-fly-destroy.yml new file mode 100644 index 0000000000..01820999bb --- /dev/null +++ b/.github/workflows/pr-preview-fly-destroy.yml @@ -0,0 +1,29 @@ +name: Destroy Fly PR Preview + +on: + pull_request_target: + types: + - unlabeled + +env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + +jobs: + label_removed: + if: github.event.label.name == 'Review allow-preview ✅' + runs-on: ubuntu-latest + continue-on-error: true + concurrency: + group: pr-${{ github.event.number }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # pull the repo from the pull request source, not the default local repo + ref: ${{ github.event.pull_request.head.sha }} + + - name: Destroy fly.io preview app + id: destroy + uses: ./.github/actions/destroy-fly-preview-app + with: + name: community-platform-pr-${{ github.event.number }} \ No newline at end of file diff --git a/.github/workflows/pr-preview-remove-label.yml b/.github/workflows/pr-preview-remove-label.yml new file mode 100644 index 0000000000..ff399a5c48 --- /dev/null +++ b/.github/workflows/pr-preview-remove-label.yml @@ -0,0 +1,35 @@ +name: Remove PR Preview Label +on: + # Run this workflow on every PR event. Existing review apps will be updated when the PR is updated. + pull_request_target: + # Trigger when labels are changed or more commits added to a PR that contains labels + types: [closed] + +jobs: + preview_app: + if: contains(github.event.pull_request.labels.*.name, 'Review allow-preview ✅') + runs-on: ubuntu-latest + continue-on-error: true + # Only run one deployment at a time per PR. + concurrency: + group: pr-${{ github.event.number }} + + steps: + - name: Get code + uses: actions/checkout@v4 + with: + # pull the repo from the pull request source, not the default local repo + ref: ${{ github.event.pull_request.head.sha }} + + - name: Remove preview label + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: github.event.number, + name: 'Review allow-preview ✅', + }); + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml deleted file mode 100644 index 7b081f5db2..0000000000 --- a/.github/workflows/pr-preview.yml +++ /dev/null @@ -1,70 +0,0 @@ -# Create and deploy a build on all pull requests -name: PR Preview -on: - # use pull_request_target so that secrets accessible from fork - pull_request_target: - # Trigger when labels are changed or more commits added to a PR that contains labels - types: [labeled, synchronize] - # Only create a preview if changes have been made to the main src code or backend functions - paths: - - 'src/**' - - 'functions/**' - - 'packages/components/**' - - '.github/workflows/pr-preview.yml' - - 'package.json' - - 'yarn.lock' - -jobs: - build_and_preview: - # NOTE - as we are going to check out and build from forks we also need to add manual - # check that malicious code has not been inserted. This is handled by manually labelling the PR - # https://securitylab.github.com/research/github-actions-preventing-pwn-requests - if: contains(github.event.pull_request.labels.*.name, 'Review allow-preview ✅') - runs-on: ubuntu-latest - continue-on-error: true - steps: - - uses: actions/checkout@v2 - with: - # pull the repo from the pull request source, not the default local repo - ref: ${{ github.event.pull_request.head.sha }} - - uses: actions/setup-node@v3 - with: - node-version: '18' - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn config get cacheFolder)" - # Setup yarn 2 cache: https://github.com/actions/cache/blob/main/examples.md#node---yarn - - name: Setup Cache - uses: actions/cache@v2 - id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - name: Install npm dependencies - run: yarn install --immutable - - name: Set environment variables - run: export VITE_PROJECT_VERSION=${GITHUB_SHA} - - name: Check environment variables - run: echo $VITE_PROJECT_VERSION - - name: Build for Preview - run: npm run build - env: - # currently some linting fails when CI mode enabled (warnings become errors) - # disable until fully resolved - CI: false - # specify the 'preview' site variant to populate the relevant firebase config - VITE_SITE_VARIANT: preview - # The hosting-deploy action calls firebase tools via npx, however installing globally - # gives us control over what version will be made available - - name: Install firebase-tools globally - run: npm i -g firebase-tools - - uses: FirebaseExtended/action-hosting-deploy@v0 - with: - repoToken: '${{ secrets.GITHUB_TOKEN }}' - # the details of the service account need to be populated to github secrets - # these must match the target projectId account - firebaseServiceAccount: '${{ secrets.ONEARMY_NEXT_FIREBASE_SERVICE_ACCOUNT }}' - expires: 30d - projectId: onearmy-next diff --git a/Dockerfile b/Dockerfile index 726b69d797..81f4dfff6e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ ARG VITE_FIREBASE_STORAGE_BUCKET ARG VITE_SENTRY_DSN ARG VITE_GA_TRACKING_ID ARG VITE_PATREON_CLIENT_ID -ARG VITE_PLATFORM_THEME +ARG VITE_PLATFORM_PROFILES ARG VITE_PROJECT_VERSION ARG VITE_SUPPORTED_MODULES ARG VITE_ACADEMY_RESOURCE @@ -66,12 +66,12 @@ RUN --mount=type=secret,id=VITE_BRANCH \ --mount=type=secret,id=VITE_SENTRY_DSN \ --mount=type=secret,id=VITE_GA_TRACKING_ID \ --mount=type=secret,id=VITE_PATREON_CLIENT_ID \ - --mount=type=secret,id=VITE_PLATFORM_THEME \ --mount=type=secret,id=VITE_PROJECT_VERSION \ --mount=type=secret,id=VITE_SUPPORTED_MODULES \ --mount=type=secret,id=VITE_ACADEMY_RESOURCE \ --mount=type=secret,id=VITE_API_URL \ --mount=type=secret,id=VITE_PROFILE_GUIDELINES_URL \ + --mount=type=secret,id=VITE_PLATFORM_PROFILES \ --mount=type=secret,id=VITE_SITE_NAME \ --mount=type=secret,id=VITE_THEME \ --mount=type=secret,id=VITE_DONATIONS_BODY \ @@ -91,7 +91,7 @@ RUN --mount=type=secret,id=VITE_BRANCH \ VITE_SENTRY_DSN="$(cat /run/secrets/VITE_SENTRY_DSN)" && \ VITE_GA_TRACKING_ID="$(cat /run/secrets/VITE_GA_TRACKING_ID)" && \ VITE_PATREON_CLIENT_ID="$(cat /run/secrets/VITE_PATREON_CLIENT_ID)" && \ - VITE_PLATFORM_THEME="$(cat /run/secrets/VITE_PLATFORM_THEME)" && \ + VITE_PLATFORM_PROFILES="$(cat /run/secrets/VITE_PLATFORM_PROFILES)" && \ VITE_PROJECT_VERSION="$(cat /run/secrets/VITE_PROJECT_VERSION)" && \ VITE_SUPPORTED_MODULES="$(cat /run/secrets/VITE_SUPPORTED_MODULES)" && \ VITE_ACADEMY_RESOURCE="$(cat /run/secrets/VITE_ACADEMY_RESOURCE)" && \ @@ -116,7 +116,7 @@ RUN --mount=type=secret,id=VITE_BRANCH \ echo "VITE_SENTRY_DSN=\"${VITE_SENTRY_DSN}\"" >> .env && \ echo "VITE_GA_TRACKING_ID=\"${VITE_GA_TRACKING_ID}\"" >> .env && \ echo "VITE_PATREON_CLIENT_ID=\"${VITE_PATREON_CLIENT_ID}\"" >> .env && \ - echo "VITE_PLATFORM_THEME=\"${VITE_PLATFORM_THEME}\"" >> .env && \ + echo "VITE_PLATFORM_PROFILES=\"${VITE_PLATFORM_PROFILES}\"" >> .env && \ echo "VITE_PROJECT_VERSION=\"${VITE_PROJECT_VERSION}\"" >> .env && \ echo "VITE_SUPPORTED_MODULES=\"${VITE_SUPPORTED_MODULES}\"" >> .env && \ echo "VITE_ACADEMY_RESOURCE=\"${VITE_ACADEMY_RESOURCE}\"" >> .env && \ diff --git a/Dockerfile.preview b/Dockerfile.preview new file mode 100644 index 0000000000..b22547dc0e --- /dev/null +++ b/Dockerfile.preview @@ -0,0 +1,49 @@ +# syntax = docker/dockerfile:1 + +FROM node:20-slim AS base + +LABEL fly_launch_runtime="Remix" + +# Remix app lives here +WORKDIR /app + +# Set production environment +ENV NODE_ENV="production" +ARG YARN_VERSION=3.6.4 + +# Install Yarn 3 +RUN corepack enable && \ + yarn set version ${YARN_VERSION} + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build node modules +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3 + +# Copy source code +COPY . . + +# Install packages +RUN yarn install + +RUN echo "VITE_SITE_VARIANT=\"preview\"" >> .env && \ + echo "VITE_GA_TRACKING_ID=\"G-G4QQ9NSQPC\"" >> .env && \ + echo "VITE_PATREON_CLIENT_ID=\"RLgZo7SKNVn8cOyFlYeGZsNnoCOjvzQiNJFskDfoxltZltjPPGNMPokwJckHNphU\"" >> .env && \ + echo "VITE_THEME=\"precious-plastic\"" >> .env && \ + echo "VITE_PLATFORM_PROFILES=\"member,workspace,community-builder,collection-point,machine-builder\"" >> .env + +# Build application +RUN yarn run build + +# Final stage for app image +FROM base + +# Copy built application +COPY --from=build /app /app + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD [ "yarn", "run", "start" ] + diff --git a/fly-ff.toml b/fly-ff.toml index 0d74e09125..03bd8c5916 100644 --- a/fly-ff.toml +++ b/fly-ff.toml @@ -1,18 +1,19 @@ app = 'community-platform-ff' -primary_region = 'cdg' +primary_region = 'ams' [build] [http_service] - internal_port = 3000 - force_https = true - auto_stop_machines = 'off' - processes = ['app'] +internal_port = 3000 +force_https = true +auto_stop_machines = 'suspend' +min_machines_running = 1 +processes = ['app'] [env] - VITE_BRANCH = "production" +VITE_BRANCH = "production" [[vm]] - memory = '4gb' - cpu_kind = 'shared' - cpus = 4 +memory = '4gb' +cpu_kind = 'shared' +cpus = 4 diff --git a/fly-pk.toml b/fly-pk.toml index e54fc228ce..851e0ed699 100644 --- a/fly-pk.toml +++ b/fly-pk.toml @@ -1,18 +1,18 @@ app = 'community-platform-pk' -primary_region = 'cdg' +primary_region = 'ams' [build] [http_service] - internal_port = 3000 - force_https = true - auto_stop_machines = 'off' - processes = ['app'] +internal_port = 3000 +force_https = true +auto_stop_machines = 'off' +processes = ['app'] [env] - VITE_BRANCH = "production" +VITE_BRANCH = "production" [[vm]] - memory = '4gb' - cpu_kind = 'shared' - cpus = 4 +memory = '4gb' +cpu_kind = 'shared' +cpus = 4 diff --git a/fly-pp.toml b/fly-pp.toml index cdd0b52960..dc73c6e054 100644 --- a/fly-pp.toml +++ b/fly-pp.toml @@ -1,18 +1,18 @@ app = 'community-platform-pp' -primary_region = 'cdg' +primary_region = 'ams' [build] [http_service] - internal_port = 3000 - force_https = true - auto_stop_machines = 'off' - processes = ['app'] +internal_port = 3000 +force_https = true +auto_stop_machines = 'off' +processes = ['app'] [env] - VITE_BRANCH = "production" +VITE_BRANCH = "production" [[vm]] - memory = '4gb' - cpu_kind = 'shared' - cpus = 4 +memory = '4gb' +cpu_kind = 'shared' +cpus = 4 diff --git a/fly-preview.toml b/fly-preview.toml index 45717ddb53..07ae141679 100644 --- a/fly-preview.toml +++ b/fly-preview.toml @@ -1,10 +1,18 @@ +primary_region = 'ams' + [build] +dockerfile = "Dockerfile.preview" [http_service] internal_port = 3000 force_https = true -auto_stop_machines = 'off' +auto_stop_machines = 'stop' processes = ['app'] [env] VITE_BRANCH = "preview" + +[[vm]] +memory = '4gb' +cpu_kind = 'shared' +cpus = 2 diff --git a/functions/src/userUpdates/index.ts b/functions/src/userUpdates/index.ts index d69b819e85..6ff3ce6e11 100644 --- a/functions/src/userUpdates/index.ts +++ b/functions/src/userUpdates/index.ts @@ -6,8 +6,8 @@ import { backupUser } from './backupUser' import { updateDiscussionComments } from './updateDiscussionComments' import { updateMapPins } from './updateMapPins' +import type { IUserDB } from 'oa-shared/models/user' import type { IDBDocChange } from '../models' -import { IUserDB } from 'oa-shared/models/user' /********************************************************************* * Side-effects to be carried out on various user updates, namely: diff --git a/functions/src/userUpdates/updateDiscussionComments.test.ts b/functions/src/userUpdates/updateDiscussionComments.test.ts index f676726fb9..0c5c5b8e4e 100644 --- a/functions/src/userUpdates/updateDiscussionComments.test.ts +++ b/functions/src/userUpdates/updateDiscussionComments.test.ts @@ -1,6 +1,7 @@ -import { IUserDB } from 'oa-shared/models/user' import { updateDiscussionComments } from './updateDiscussionComments' +import type { IUserDB } from 'oa-shared/models/user' + const prevUser = { _id: 'hjg235z', location: { countryCode: 'UK' }, diff --git a/functions/src/userUpdates/updateMapPins.ts b/functions/src/userUpdates/updateMapPins.ts index a8c85d18e5..7d769f26af 100644 --- a/functions/src/userUpdates/updateMapPins.ts +++ b/functions/src/userUpdates/updateMapPins.ts @@ -4,6 +4,7 @@ import { db } from '../Firebase/firestoreDB' import { getCreatorImage, getFirstCoverImage, + getValidTags, hasDetailsForMapPinChanged, } from './utils' @@ -32,14 +33,15 @@ export const updateMapPins = async (prevUser: IUserDB, user: IUserDB) => { coverImages, displayName, isContactableByPublic, + location, profileType, - workspaceType, userImage, - location, + workspaceType, } = user const creatorImage = getCreatorImage(userImage) const coverImage = getFirstCoverImage(coverImages) const countryCode = location?.countryCode || country || '' + const tags = user.tags ? getValidTags(user.tags) : [] const creator = { _lastActive, @@ -50,8 +52,9 @@ export const updateMapPins = async (prevUser: IUserDB, user: IUserDB) => { displayName, isContactableByPublic, profileType, - workspaceType, + tags, userImage: creatorImage, + workspaceType, } // Only one expected diff --git a/functions/src/userUpdates/utils.test.ts b/functions/src/userUpdates/utils.test.ts index 963c5d33a6..7d44f6597c 100644 --- a/functions/src/userUpdates/utils.test.ts +++ b/functions/src/userUpdates/utils.test.ts @@ -4,21 +4,43 @@ import { hasDetailsForMapPinChanged, hasLocationDetailsChanged, hasUserImageChanged, + hasUserTagsChanged, } from './utils' +import type { IUploadedFileMeta } from 'oa-shared' import type { IUserDB } from 'oa-shared/models/user' +const unimportantUserDetails = { + _authID: '00', + _id: 'unchangeable', + _created: '', + _deleted: false, + userName: 'unchangeable', + verified: false, + coverImages: [], + links: [], +} + describe('hasDetailsChanged', () => { it("returns false for every field that's the same", () => { const user = { + _lastActive: 'same', + about: 'about', displayName: 'same', + isContactableByPublic: true, + profileType: 'member', userImage: { downloadUrl: 'https://more.same/image.jpg', - }, + } as IUploadedFileMeta, badges: { verified: true, supporter: false, }, + tags: { + hguowewer: true, + '76khbrw': false, + }, + ...unimportantUserDetails, } as IUserDB expect(hasDetailsChanged(user, user)).toEqual([false, false, false, false]) @@ -26,25 +48,42 @@ describe('hasDetailsChanged', () => { it("returns true for every field that's different", () => { const prevUser = { + _lastActive: 'yesterday', + about: 'Old about', displayName: 'old', + profileType: 'member', + workspaceType: null, userImage: { downloadUrl: 'https://more.old/image.jpg', - }, + } as IUploadedFileMeta, badges: { verified: true, supporter: true, }, + tags: { + hguowewer: true, + }, + ...unimportantUserDetails, } as IUserDB const user = { + _lastActive: 'today', + about: 'New about description.', displayName: 'new', + profileType: 'space', + workspaceType: 'extrusion', userImage: { downloadUrl: 'https://more.new/image.jpg', - }, + } as IUploadedFileMeta, badges: { verified: false, supporter: false, }, + tags: { + hguowewer: true, + '76khbrw': true, + }, + ...unimportantUserDetails, } as IUserDB expect(hasDetailsChanged(prevUser, user)).toEqual([true, true, true, true]) @@ -99,7 +138,6 @@ describe('hasDetailsForCommentsChanged', () => { userImage: { downloadUrl: 'http://etc.', }, - badges: { verified: true, supporter: false, @@ -196,3 +234,61 @@ describe('hasUserImageChanged', () => { expect(hasUserImageChanged(prevUser, user)).toEqual(true) }) }) + +describe('hasUserTagsChanged', () => { + it('returns false when nothing has changed', () => { + const user = { + displayName: 'displayName', + profileType: 'member', + tags: { + gyi: false, + bnhjo: true, + }, + ...unimportantUserDetails, + } as IUserDB + expect(hasUserTagsChanged(user, user)).toEqual(false) + }) + + it('returns true when a tag is added', () => { + const prevUser = { + displayName: 'displayName', + profileType: 'member', + tags: { + gyi: false, + }, + ...unimportantUserDetails, + } as IUserDB + + const user = { + displayName: 'displayName', + profileType: 'member', + tags: { + gyi: false, + bnhjo: true, + }, + ...unimportantUserDetails, + } as IUserDB + expect(hasUserTagsChanged(prevUser, user)).toEqual(true) + }) + + it('returns true when a tag is changed', () => { + const prevUser = { + displayName: 'displayName', + profileType: 'member', + tags: { + gyi: false, + }, + ...unimportantUserDetails, + } as IUserDB + + const user = { + displayName: 'displayName', + profileType: 'member', + tags: { + gyi: true, + }, + ...unimportantUserDetails, + } as IUserDB + expect(hasUserTagsChanged(prevUser, user)).toEqual(true) + }) +}) diff --git a/functions/src/userUpdates/utils.ts b/functions/src/userUpdates/utils.ts index 69415eff65..5515ca7fbb 100644 --- a/functions/src/userUpdates/utils.ts +++ b/functions/src/userUpdates/utils.ts @@ -1,6 +1,8 @@ +import { profileTags } from 'oa-shared' + import { valuesAreDeepEqual } from '../Utils' -import type { IUserDB } from 'oa-shared/models/user' +import type { ISelectedTags, ITag, IUserDB } from 'oa-shared' export const hasDetailsChanged = ( prevUser: IUserDB, @@ -46,6 +48,7 @@ export const hasDetailsForMapPinChanged = ( prevUser.isContactableByPublic !== user.isContactableByPublic, prevUser.profileType !== user.profileType, prevUser.workspaceType !== user.workspaceType, + hasUserTagsChanged(prevUser, user), ...hasDetailsChanged(prevUser, user), ...hasLocationDetailsChanged(prevUser, user), ] @@ -66,6 +69,13 @@ export const hasUserImageChanged = ( if (!prevUser.userImage && user.userImage) return true } +export const hasUserTagsChanged = ( + prevUser: IUserDB, + user: IUserDB, +): boolean => { + return !valuesAreDeepEqual(prevUser.tags, user.tags) +} + export const getCreatorImage = (userImage: IUserDB['userImage']) => { return userImage?.downloadUrl || null } @@ -73,3 +83,14 @@ export const getCreatorImage = (userImage: IUserDB['userImage']) => { export const getFirstCoverImage = (coverImages: IUserDB['coverImages']) => { return coverImages?.[0]?.downloadUrl || null } + +// For ease, duplicated from src/utils/getValidTags.ts +export const getValidTags = (tagIds: ISelectedTags) => { + const selectedTagIds = Object.keys(tagIds).filter((id) => tagIds[id] === true) + const tags: ITag[] = selectedTagIds + .map((id) => profileTags.find(({ _id }) => id === _id)) + .filter((tag): tag is ITag => !!tag) + .filter(({ _deleted }) => _deleted !== true) + + return tags +} diff --git a/package.json b/package.json index f93855db08..6c7041f04e 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "main": "lib/index.js", "type": "module", "scripts": { - "start": "concurrently --kill-others --names themes,components,platform --prefix-colors cyan,blue,magenta \"yarn start:shared\" \"yarn start:themes\" \"yarn start:components\" \"yarn start:platform\"", - "start-ci": "concurrently --kill-others --names themes,components,platform --prefix-colors cyan,blue,magenta \"yarn start:themes\" \"yarn start:components\" \"yarn start:platform-ci\"", + "start": "concurrently --kill-others --names shared,themes,components,platform --prefix-colors cyan,blue,magenta \"yarn start:shared\" \"yarn start:themes\" \"yarn start:components\" \"yarn start:platform\"", + "start-ci": "concurrently --kill-others --names shared,themes,components,platform --prefix-colors cyan,blue,magenta \"yarn start:themes\" \"yarn start:components\" \"yarn start:platform-ci\"", "start:themes": "yarn workspace oa-themes dev", "start:components": "yarn workspace oa-components dev", "start:shared": "yarn workspace oa-shared dev", @@ -38,7 +38,7 @@ "format:style": "prettier --write '**/*.{json,js,tsx,ts}'", "serve": "npx serve -s build", "test": "yarn workspace oa-cypress start", - "test:components": "yarn workspace oa-components test", + "test:components": "yarn build:themes && yarn build:components && yarn workspace oa-components test", "test:unit": "yarn build:themes && yarn build:components && vitest", "test:madge": "npx madge --circular --extensions ts,tsx ./ --exclude src/stores", "storybook": "yarn workspace oa-components start", @@ -71,9 +71,9 @@ "@emotion/react": "^11.11.4", "@emotion/server": "^11.11.0", "@emotion/styled": "^11.11.5", - "@remix-run/express": "^2.11.1", - "@remix-run/node": "^2.11.1", - "@remix-run/react": "^2.11.1", + "@remix-run/express": "^2.12.1", + "@remix-run/node": "^2.12.1", + "@remix-run/react": "^2.12.1", "@sentry/react": "7.56.0", "@sentry/remix": "^8.26.0", "@uppy/compressor": "^1.1.4", @@ -99,6 +99,7 @@ "fuse.js": "^6.4.6", "helmet": "^7.1.0", "isbot": "^5.1.13", + "keyv": "^5.1.2", "leaflet": "^1.5.1", "leaflet.markercluster": "^1.4.1", "mobx": "6.9.0", @@ -109,7 +110,7 @@ "react": "18.3.1", "react-country-flag": "^3.1.0", "react-dom": "18.3.1", - "react-dropzone": "^14.2.3", + "react-dropzone-esm": "^15.0.1", "react-final-form": "6.5.3", "react-final-form-arrays": "^3.1.3", "react-foco": "^1.3.1", @@ -129,8 +130,8 @@ "@emotion/babel-plugin": "^11.11.0", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@faker-js/faker": "^8.4.1", - "@remix-run/dev": "^2.10.3", - "@remix-run/testing": "^2.11.0", + "@remix-run/dev": "^2.12.1", + "@remix-run/testing": "^2.12.1", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@testing-library/jest-dom": "^6.4.6", diff --git a/packages/components/.storybook/preview.tsx b/packages/components/.storybook/preview.tsx index e079bac9db..5160cc72d0 100644 --- a/packages/components/.storybook/preview.tsx +++ b/packages/components/.storybook/preview.tsx @@ -1,4 +1,4 @@ -import { ThemeProvider } from '@emotion/react' +import { ThemeProvider } from '@theme-ui/core' import type { Preview } from '@storybook/react' import React from 'react' diff --git a/packages/components/assets/images/plastic-types/hdpe.svg b/packages/components/assets/images/plastic-types/hdpe.svg deleted file mode 100755 index 4d5a5fefb6..0000000000 --- a/packages/components/assets/images/plastic-types/hdpe.svg +++ /dev/null @@ -1 +0,0 @@ -icon plastic type hdpe \ No newline at end of file diff --git a/packages/components/assets/images/plastic-types/ldpe.svg b/packages/components/assets/images/plastic-types/ldpe.svg deleted file mode 100755 index 2538181824..0000000000 --- a/packages/components/assets/images/plastic-types/ldpe.svg +++ /dev/null @@ -1,8 +0,0 @@ -icon plastic type ldpe - - - - - - - \ No newline at end of file diff --git a/packages/components/assets/images/plastic-types/other.svg b/packages/components/assets/images/plastic-types/other.svg deleted file mode 100755 index a62c4cd4f7..0000000000 --- a/packages/components/assets/images/plastic-types/other.svg +++ /dev/null @@ -1 +0,0 @@ -icon plastic type other \ No newline at end of file diff --git a/packages/components/assets/images/plastic-types/pet.svg b/packages/components/assets/images/plastic-types/pet.svg deleted file mode 100755 index f99758e7c9..0000000000 --- a/packages/components/assets/images/plastic-types/pet.svg +++ /dev/null @@ -1 +0,0 @@ -icon plastic type pet \ No newline at end of file diff --git a/packages/components/assets/images/plastic-types/pp.svg b/packages/components/assets/images/plastic-types/pp.svg deleted file mode 100755 index 46dd3aaa01..0000000000 --- a/packages/components/assets/images/plastic-types/pp.svg +++ /dev/null @@ -1 +0,0 @@ -icon plastic type pp \ No newline at end of file diff --git a/packages/components/assets/images/plastic-types/ps.svg b/packages/components/assets/images/plastic-types/ps.svg deleted file mode 100755 index 64fad655ab..0000000000 --- a/packages/components/assets/images/plastic-types/ps.svg +++ /dev/null @@ -1 +0,0 @@ -icon plastic type ps \ No newline at end of file diff --git a/packages/components/assets/images/plastic-types/pvc.svg b/packages/components/assets/images/plastic-types/pvc.svg deleted file mode 100755 index a8e2c74701..0000000000 --- a/packages/components/assets/images/plastic-types/pvc.svg +++ /dev/null @@ -1 +0,0 @@ -icon plastic type pvc \ No newline at end of file diff --git a/packages/components/package.json b/packages/components/package.json index ef97f8565c..f002f6d46b 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -22,10 +22,9 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@faker-js/faker": "^7.6.0", - "@mui/base": "^5.0.0-beta.18", - "@react-icons/all-files": "^4.1.0", - "@remix-run/node": "^2.11.1", - "@remix-run/react": "^2.11.1", + "@mui/base": "next", + "@remix-run/node": "^2.12.1", + "@remix-run/react": "^2.12.1", "chromatic": "^11.4.0", "date-fns": "^2.29.3", "linkify-plugin-mention": "^4.0.2", @@ -34,15 +33,15 @@ "oa-themes": "workspace:^", "photoswipe": "^5.4.1", "react-country-flag": "^3.1.0", - "react-icons": "^4.3.1", + "react-icons": "^5.3.0", "react-image-crop": "^11.0.5", "react-player": "^2.16.0", "react-portal": "^4.2.2", "react-responsive-masonry": "2.1.7", "react-router": "6.24.1", "react-router-dom": "^6.26.0", - "react-select": "^5.4.0", - "react-tooltip": "^4.2.21", + "react-select": "^5.8.1", + "react-tooltip": "^5.28.0", "storybook": "^7.6.0", "styled-system": "^5.1.5", "theme-ui": "^0.16.2", @@ -55,8 +54,8 @@ }, "devDependencies": { "@babel/core": "^7.14.3", - "@remix-run/dev": "^2.11.1", - "@remix-run/testing": "^2.11.1", + "@remix-run/dev": "^2.12.1", + "@remix-run/testing": "^2.12.1", "@storybook/addon-actions": "^7.4.1", "@storybook/addon-essentials": "^7.4.1", "@storybook/addon-links": "^7.4.1", diff --git a/packages/components/scripts/templates/{componentName}.test.tsx.mst b/packages/components/scripts/templates/{componentName}.test.tsx.mst index d78b9239f6..c029ba57ea 100644 --- a/packages/components/scripts/templates/{componentName}.test.tsx.mst +++ b/packages/components/scripts/templates/{componentName}.test.tsx.mst @@ -1,3 +1,7 @@ +import '@testing-library/jest-dom/vitest' + +import { describe, expect, it } from 'vitest' + import { render } from '../test/utils' import { Default } from './{{ComponentName}}.stories' import type { IProps } from './{{ComponentName}}' diff --git a/packages/components/src/ArticleCallToAction/ArticleCallToAction.tsx b/packages/components/src/ArticleCallToAction/ArticleCallToAction.tsx index c61371d18b..1076db9ce7 100644 --- a/packages/components/src/ArticleCallToAction/ArticleCallToAction.tsx +++ b/packages/components/src/ArticleCallToAction/ArticleCallToAction.tsx @@ -1,5 +1,4 @@ -import { useTheme } from '@emotion/react' -import { Flex, Heading, Text } from 'theme-ui' +import { Flex, Heading, Text, useThemeUI } from 'theme-ui' import { Username } from '../Username/Username' @@ -13,7 +12,7 @@ export interface IProps { export const ArticleCallToAction = (props: IProps) => { const { author, children, contributors } = props - const theme = useTheme() as any + const { theme } = useThemeUI() as any return ( { countryCode, coverImage, profileType, + tags, workspaceType, } = creator @@ -82,7 +84,8 @@ export const CardDetailsSpaceProfile = ({ creator, isLink }: IProps) => { isLink={isLink} /> - {workspaceType && ( + + {workspaceType && profileType === 'workspace' && ( { }} /> )} + + {tags && } + {about && ( {aboutTextStart || about} diff --git a/packages/components/src/Category/Category.tsx b/packages/components/src/Category/Category.tsx index 68b8008331..7c1e31061d 100644 --- a/packages/components/src/Category/Category.tsx +++ b/packages/components/src/Category/Category.tsx @@ -14,6 +14,7 @@ export const Category = (props: Props) => { return ( { } return ( - + {_deleted && ( diff --git a/packages/components/src/CommentList/CommentList.tsx b/packages/components/src/CommentList/CommentList.tsx index cf8888224b..7cd3a4e349 100644 --- a/packages/components/src/CommentList/CommentList.tsx +++ b/packages/components/src/CommentList/CommentList.tsx @@ -231,6 +231,7 @@ export const CommentList = (props: IPropsCommentList) => { type="button" sx={{ margin: '0 auto' }} variant="outline" + data-cy="show-more-comments" onClick={handleMoreComments} > show more comments diff --git a/packages/components/src/DiscussionTitle/DiscussionTitle.tsx b/packages/components/src/DiscussionTitle/DiscussionTitle.tsx index 5d36b65059..c5f09886ab 100644 --- a/packages/components/src/DiscussionTitle/DiscussionTitle.tsx +++ b/packages/components/src/DiscussionTitle/DiscussionTitle.tsx @@ -30,5 +30,9 @@ export const DiscussionTitle = ({ comments }: IProps) => { const title = setTitle() - return {title} + return ( + + {title} + + ) } diff --git a/packages/components/src/DownloadButton/DownloadButton.tsx b/packages/components/src/DownloadButton/DownloadButton.tsx index b16c29e12f..37579c4321 100644 --- a/packages/components/src/DownloadButton/DownloadButton.tsx +++ b/packages/components/src/DownloadButton/DownloadButton.tsx @@ -36,14 +36,15 @@ export const DownloadButton = ({ onClick={onClick} data-cy="downloadButton" data-testid="downloadButton" - data-tip={!isLoggedIn ? 'Login to download' : ''} + data-tooltip-id="download-files" + data-tooltip-content={!isLoggedIn ? 'Login to download' : ''} > {label ? label : 'Download files'} - + ) } diff --git a/packages/components/src/DownloadStaticFile/DownloadStaticFile.tsx b/packages/components/src/DownloadStaticFile/DownloadStaticFile.tsx index 25f7411f05..3584d153ec 100644 --- a/packages/components/src/DownloadStaticFile/DownloadStaticFile.tsx +++ b/packages/components/src/DownloadStaticFile/DownloadStaticFile.tsx @@ -48,7 +48,8 @@ const FileDetails = (props: { cursor: 'pointer', }} onClick={() => redirectToSignIn && redirectToSignIn()} - data-tip={redirectToSignIn ? 'Login to download' : ''} + data-tooltip-id="login-download" + data-tooltip-content={redirectToSignIn ? 'Login to download' : ''} > {size} - + ) } diff --git a/packages/components/src/FollowButton/FollowButton.tsx b/packages/components/src/FollowButton/FollowButton.tsx index 5540e755f3..dfa3291415 100644 --- a/packages/components/src/FollowButton/FollowButton.tsx +++ b/packages/components/src/FollowButton/FollowButton.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react' import { useNavigate } from '@remix-run/react' import { Button } from '../Button/Button' @@ -15,6 +16,7 @@ export interface IProps { export const FollowButton = (props: IProps) => { const { hasUserSubscribed, isLoggedIn, onFollowClick, sx } = props const navigate = useNavigate() + const uuid = useMemo(() => (Math.random() * 16).toString(), []) return ( <> @@ -22,7 +24,8 @@ export const FollowButton = (props: IProps) => { type="button" data-testid={isLoggedIn ? 'follow-button' : 'follow-redirect'} data-cy={isLoggedIn ? 'follow-button' : 'follow-redirect'} - data-tip={isLoggedIn ? '' : 'Login to follow'} + data-tooltip-id={uuid} + data-tooltip-content={isLoggedIn ? '' : 'Login to follow'} icon="thunderbolt" variant="outline" iconColor={hasUserSubscribed ? 'subscribed' : 'notSubscribed'} @@ -35,7 +38,7 @@ export const FollowButton = (props: IProps) => { > {hasUserSubscribed ? 'Following' : 'Follow'} - + ) } diff --git a/packages/components/src/GlobalStyles/GlobalStyles.tsx b/packages/components/src/GlobalStyles/GlobalStyles.tsx index 699fd29179..b457624ed4 100644 --- a/packages/components/src/GlobalStyles/GlobalStyles.tsx +++ b/packages/components/src/GlobalStyles/GlobalStyles.tsx @@ -1,13 +1,11 @@ import { css } from '@emotion/react' -import { GlobalFonts, preciousPlasticTheme } from 'oa-themes' - -const theme = preciousPlasticTheme.styles +import { commonStyles, GlobalFonts } from 'oa-themes' export const GlobalStyles = css` ${GlobalFonts} body { font-family: 'Varela Round', Arial, sans-serif; - background-color: ${theme.colors.background}; + background-color: ${commonStyles.colors.background}; margin: 0; padding: 0; min-height: 100vh; @@ -22,11 +20,11 @@ export const GlobalStyles = css` } .beta-tester-feature { - border: 4px dashed ${theme.colors.betaGreen}; + border: 4px dashed ${commonStyles.colors.betaGreen}; } body:has(.beta-tester-feature) .user-beta-icon > span { - background-color: ${theme.colors.betaGreen}; + background-color: ${commonStyles.colors.betaGreen}; } /***** Fix for Algolia search Icon *******/ diff --git a/packages/components/src/Icon/Icon.tsx b/packages/components/src/Icon/Icon.tsx index 5fe9846fa8..0730934efe 100644 --- a/packages/components/src/Icon/Icon.tsx +++ b/packages/components/src/Icon/Icon.tsx @@ -1,32 +1,36 @@ /** @jsxImportSource theme-ui */ +import { IconContext } from 'react-icons' +import { + FaCloudUploadAlt, + FaFacebookF, + FaFilePdf, + FaFilter, + FaInstagram, + FaSignal, + FaSlack, +} from 'react-icons/fa' +import { GoLinkExternal } from 'react-icons/go' +import { + MdAccessTime, + MdAccountCircle, + MdAdd, + MdArrowBack, + MdArrowForward, + MdCheck, + MdEdit, + MdFileDownload, + MdImage, + MdKeyboardArrowDown, + MdLocationOn, + MdLock, + MdMail, + MdMailOutline, + MdMenu, + MdMoreVert, + MdNotifications, + MdTurnedIn, +} from 'react-icons/md' import styled from '@emotion/styled' -import { IconContext } from '@react-icons/all-files' -import { FaFacebookF } from '@react-icons/all-files/fa/FaFacebookF' -import { FaInstagram } from '@react-icons/all-files/fa/FaInstagram' -import { FaSignal } from '@react-icons/all-files/fa/FaSignal' -import { FaSlack } from '@react-icons/all-files/fa/FaSlack' -import { GoCloudUpload } from '@react-icons/all-files/go/GoCloudUpload' -import { GoFilePdf } from '@react-icons/all-files/go/GoFilePdf' -import { GoLinkExternal } from '@react-icons/all-files/go/GoLinkExternal' -import { MdAccessTime } from '@react-icons/all-files/md/MdAccessTime' -import { MdAccountCircle } from '@react-icons/all-files/md/MdAccountCircle' -import { MdAdd } from '@react-icons/all-files/md/MdAdd' -import { MdArrowBack } from '@react-icons/all-files/md/MdArrowBack' -import { MdArrowForward } from '@react-icons/all-files/md/MdArrowForward' -import { MdCheck } from '@react-icons/all-files/md/MdCheck' -import { MdEdit } from '@react-icons/all-files/md/MdEdit' -import { MdFileDownload } from '@react-icons/all-files/md/MdFileDownload' -import { MdImage } from '@react-icons/all-files/md/MdImage' -import { MdKeyboardArrowDown } from '@react-icons/all-files/md/MdKeyboardArrowDown' -import { MdLocationOn } from '@react-icons/all-files/md/MdLocationOn' -import { MdLock } from '@react-icons/all-files/md/MdLock' -import { MdMail } from '@react-icons/all-files/md/MdMail' -import { MdMailOutline } from '@react-icons/all-files/md/MdMailOutline' -import { MdMenu } from '@react-icons/all-files/md/MdMenu' -import { MdMoreVert } from '@react-icons/all-files/md/MdMoreVert' -import { MdNotifications } from '@react-icons/all-files/md/MdNotifications' -import { MdTurnedIn } from '@react-icons/all-files/md/MdTurnedIn' -import { RiFilter2Fill } from '@react-icons/all-files/ri/RiFilter2Fill' import { space, verticalAlign } from 'styled-system' import { DownloadIcon } from './DownloadIcon' @@ -82,7 +86,7 @@ export const glyphs: IGlyphs = { 'external-link': , 'external-url': , facebook: , - filter: , + filter: , 'flag-unknown': iconMap.flagUnknown, hide: iconMap.hide, hyperlink: iconMap.hyperlink, @@ -99,7 +103,7 @@ export const glyphs: IGlyphs = { 'more-vert': , notifications: , patreon: iconMap.patreon, - pdf: , + pdf: , plastic: iconMap.plastic, profile: iconMap.profile, revenue: iconMap.revenue, @@ -116,7 +120,7 @@ export const glyphs: IGlyphs = { supporter: iconMap.supporter, show: iconMap.show, update: iconMap.update, - upload: , + upload: , useful: iconMap.useful, verified: iconMap.verified, view: iconMap.view, @@ -158,7 +162,7 @@ const Glyph = ({ glyph }: IGlyphProps) => { } export const Icon = (props: Props) => { - const { glyph, size, marginRight, sx } = props + const { glyph, size, sx } = props const isSizeNumeric = !isNaN(size as any) @@ -186,7 +190,6 @@ export const Icon = (props: Props) => { ...sx, }} size={definedSize} - style={{ marginRight }} > { const { count, dataCy, icon, text } = props + const id = useMemo(() => (Math.random() * 16).toString(), []) + return ( <> { {count} - + ) } diff --git a/packages/components/src/LinkifyText/LinkifyText.tsx b/packages/components/src/LinkifyText/LinkifyText.tsx index 0e24ae5d45..2e9ad63801 100644 --- a/packages/components/src/LinkifyText/LinkifyText.tsx +++ b/packages/components/src/LinkifyText/LinkifyText.tsx @@ -1,8 +1,8 @@ import 'linkify-plugin-mention' -import { useTheme } from '@emotion/react' import styled from '@emotion/styled' import Linkify from 'linkify-react' +import { useThemeUI } from 'theme-ui' import { ExternalLink } from '../ExternalLink/ExternalLink' import { InternalLink } from '../InternalLink/InternalLink' @@ -12,8 +12,7 @@ export interface Props { } export const LinkifyText = (props: Props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const theme = useTheme() as any + const { theme } = useThemeUI() as any const StyledExternalLink = styled(ExternalLink)` color: ${theme.colors.grey}!important; text-decoration: underline; diff --git a/packages/components/src/Loader/Loader.tsx b/packages/components/src/Loader/Loader.tsx index 3e9f724bd7..92f5fcaf05 100644 --- a/packages/components/src/Loader/Loader.tsx +++ b/packages/components/src/Loader/Loader.tsx @@ -1,7 +1,8 @@ -import { keyframes, useTheme } from '@emotion/react' +import { keyframes } from '@emotion/react' import styled from '@emotion/styled' -import { Flex, Image, Text } from 'theme-ui' +import { Flex, Image, Text, useThemeUI } from 'theme-ui' +import type { ThemeWithName } from 'oa-themes' import type { ThemeUIStyleObject } from 'theme-ui' const rotate = keyframes` @@ -25,7 +26,8 @@ export interface Props { } export const Loader = ({ label, sx }: Props) => { - const theme = useTheme() as any + const themeUi = useThemeUI() + const theme = themeUi.theme as ThemeWithName const logo = theme.logo || null return ( diff --git a/packages/components/src/MapFilterProfileTypeCardList/MapFilterProfileTypeCardList.tsx b/packages/components/src/MapFilterProfileTypeCardList/MapFilterProfileTypeCardList.tsx index fae8cdb582..b9af97b424 100644 --- a/packages/components/src/MapFilterProfileTypeCardList/MapFilterProfileTypeCardList.tsx +++ b/packages/components/src/MapFilterProfileTypeCardList/MapFilterProfileTypeCardList.tsx @@ -21,17 +21,12 @@ export const MapFilterProfileTypeCardList = (props: IProps) => { const elementRef = useRef(null) const [disableLeftArrow, setDisableLeftArrow] = useState(true) const [disableRightArrow, setDisableRightArrow] = useState(false) - const { activeFilters, availableFilters, onFilterChange } = props const typeFilters = availableFilters.filter( ({ filterType }) => filterType === 'profileType', ) - if (!typeFilters || typeFilters.length < 2) { - return null - } - const handleHorizantalScroll = (step: number) => { const distance = 121 const element = elementRef.current @@ -67,6 +62,10 @@ export const MapFilterProfileTypeCardList = (props: IProps) => { handleHorizantalScroll(0) }, []) + if (!availableFilters || availableFilters.length < 2) { + return null + } + return ( { - const theme: any = useTheme() + const { theme } = useThemeUI() as any const { size, useLowDetailVersion, sx } = props const profileType = props.profileType || 'member' const badgeSize = size ? size : MINIMUM_SIZE diff --git a/packages/components/src/MoreContainer/MoreContainer.tsx b/packages/components/src/MoreContainer/MoreContainer.tsx index e773a5c5f5..1935ccb0b1 100644 --- a/packages/components/src/MoreContainer/MoreContainer.tsx +++ b/packages/components/src/MoreContainer/MoreContainer.tsx @@ -1,6 +1,5 @@ -import { useTheme } from '@emotion/react' import styled from '@emotion/styled' -import { Box } from 'theme-ui' +import { Box, useThemeUI } from 'theme-ui' import WhiteBubble0 from '../../assets/images/white-bubble_0.svg' import WhiteBubble1 from '../../assets/images/white-bubble_1.svg' @@ -10,7 +9,7 @@ import WhiteBubble3 from '../../assets/images/white-bubble_3.svg' import type { BoxProps } from 'theme-ui' export const MoreContainer = (props: BoxProps) => { - const theme = useTheme() as any + const { theme } = useThemeUI() as any const MoreModalContainer = styled(Box)` position: relative; max-width: 780px; diff --git a/packages/components/src/NotificationItem/NotificationItem.tsx b/packages/components/src/NotificationItem/NotificationItem.tsx index 42f8a5ff4e..ab5788fcb2 100644 --- a/packages/components/src/NotificationItem/NotificationItem.tsx +++ b/packages/components/src/NotificationItem/NotificationItem.tsx @@ -1,4 +1,4 @@ -import { ThemeProvider } from '@emotion/react' +import { ThemeProvider } from '@theme-ui/core' import { Box, Flex } from 'theme-ui' import { Icon } from '../Icon/Icon' diff --git a/packages/components/src/ProfileTagsList/ProfileTagsList.stories.tsx b/packages/components/src/ProfileTagsList/ProfileTagsList.stories.tsx new file mode 100644 index 0000000000..5df447e884 --- /dev/null +++ b/packages/components/src/ProfileTagsList/ProfileTagsList.stories.tsx @@ -0,0 +1,27 @@ +import { ProfileTagsList } from './ProfileTagsList' + +import type { Meta, StoryFn } from '@storybook/react' + +export default { + title: 'Components/ProfileTagsList', + component: ProfileTagsList, +} as Meta + +export const Default: StoryFn = () => ( + +) diff --git a/packages/components/src/ProfileTagsList/ProfileTagsList.test.tsx b/packages/components/src/ProfileTagsList/ProfileTagsList.test.tsx new file mode 100644 index 0000000000..0d6b9564ff --- /dev/null +++ b/packages/components/src/ProfileTagsList/ProfileTagsList.test.tsx @@ -0,0 +1,16 @@ +import '@testing-library/jest-dom/vitest' + +import { describe, expect, it } from 'vitest' + +import { render } from '../test/utils' +import { Default } from './ProfileTagsList.stories' + +import type { IProps } from './ProfileTagsList' + +describe('ProfileTagsList', () => { + it('validates the component behaviour', () => { + const { getByText } = render() + + expect(getByText('Electronics')).toBeInTheDocument() + }) +}) diff --git a/packages/components/src/ProfileTagsList/ProfileTagsList.tsx b/packages/components/src/ProfileTagsList/ProfileTagsList.tsx new file mode 100644 index 0000000000..7ed4c4b1e1 --- /dev/null +++ b/packages/components/src/ProfileTagsList/ProfileTagsList.tsx @@ -0,0 +1,22 @@ +import { Flex } from 'theme-ui' + +import { Category } from '../Category/Category' + +import type { ITag } from 'oa-shared' + +export interface IProps { + tags: ITag[] +} + +export const ProfileTagsList = ({ tags }: IProps) => { + return ( + + {tags.map( + (tag, index) => + tag?.label && ( + + ), + )} + + ) +} diff --git a/packages/components/src/ResearchEditorOverview/__snapshots__/ResearchEditorOverview.test.tsx.snap b/packages/components/src/ResearchEditorOverview/__snapshots__/ResearchEditorOverview.test.tsx.snap index 1f77d5d53b..68cfa243f7 100644 --- a/packages/components/src/ResearchEditorOverview/__snapshots__/ResearchEditorOverview.test.tsx.snap +++ b/packages/components/src/ResearchEditorOverview/__snapshots__/ResearchEditorOverview.test.tsx.snap @@ -3,23 +3,23 @@ exports[`ResearchEditorOverview > handles empty updates 1`] = `

Research overview

- + + ) diff --git a/packages/components/src/Tooltip/Tooltip.tsx b/packages/components/src/Tooltip/Tooltip.tsx index 1c162606f2..004347789b 100644 --- a/packages/components/src/Tooltip/Tooltip.tsx +++ b/packages/components/src/Tooltip/Tooltip.tsx @@ -1,4 +1,4 @@ -import ReactTooltip from 'react-tooltip' +import { Tooltip as ReactTooltip } from 'react-tooltip' import styled from '@emotion/styled' const StyledTooltip = styled(ReactTooltip)` @@ -8,16 +8,16 @@ const StyledTooltip = styled(ReactTooltip)` ` type TooltipProps = { + id: string children?: React.ReactNode } -export const Tooltip = ({ children, ...props }: TooltipProps) => { +export const Tooltip = ({ children, id }: TooltipProps) => { return ( {children} diff --git a/packages/components/src/UsefulStatsButton/UsefulStatsButton.tsx b/packages/components/src/UsefulStatsButton/UsefulStatsButton.tsx index c1ee686ac2..f03659919e 100644 --- a/packages/components/src/UsefulStatsButton/UsefulStatsButton.tsx +++ b/packages/components/src/UsefulStatsButton/UsefulStatsButton.tsx @@ -1,7 +1,6 @@ -import { useState } from 'react' -import { useTheme } from '@emotion/react' +import { useMemo, useState } from 'react' import { useNavigate } from '@remix-run/react' -import { Text } from 'theme-ui' +import { Text, useThemeUI } from 'theme-ui' import { Button } from '../Button/Button' import { Tooltip } from '../Tooltip/Tooltip' @@ -17,8 +16,9 @@ export interface IProps { } export const UsefulStatsButton = (props: IProps) => { - const theme: any = useTheme() + const { theme } = useThemeUI() as any const navigate = useNavigate() + const uuid = useMemo(() => (Math.random() * 16).toString(), []) const [disabled, setDisabled] = useState() @@ -36,7 +36,8 @@ export const UsefulStatsButton = (props: IProps) => { <> - + ) } diff --git a/packages/components/src/UserStatistics/UserStatistics.tsx b/packages/components/src/UserStatistics/UserStatistics.tsx index 69c501f0cb..fa133a0668 100644 --- a/packages/components/src/UserStatistics/UserStatistics.tsx +++ b/packages/components/src/UserStatistics/UserStatistics.tsx @@ -18,7 +18,7 @@ export interface UserStatisticsProps { howtoCount: number usefulCount: number researchCount: number - totalViews?: number + totalViews: number sx?: ThemeUIStyleObject | undefined } @@ -38,12 +38,7 @@ export const UserStatistics = (props: UserStatisticsProps) => { ...props.sx, }} > - + {props.isVerified && ( @@ -51,9 +46,9 @@ export const UserStatistics = (props: UserStatisticsProps) => { )} - {props?.isSupporter ? ( + {props?.isSupporter && ( - + { - ) : null} + )} - {hasLocation ? ( + {hasLocation && ( { {props.country || 'View on Map'} - ) : null} + )} - {props.usefulCount ? ( + {props.usefulCount > 0 && ( - Useful: {props.usefulCount} + {`Useful: ${props.usefulCount}`} - ) : null} + )} - {props.howtoCount ? ( + {props.howtoCount > 0 && ( { > - How‑to: {props.howtoCount} + {`How-to: ${props.howtoCount}`} - ) : null} + )} - {props.researchCount ? ( + {props.researchCount > 0 && ( { > - Research: {props.researchCount} + {`Research: ${props.researchCount}`} - ) : null} + )} - {props.totalViews ? ( + {props.totalViews > 0 && ( - - Views: {props.totalViews} + + {`Views: ${props.totalViews}`} - ) : null} + )} ) diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 4d75d201d2..6c72f698c9 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -56,6 +56,7 @@ export { NotificationList } from './NotificationList/NotificationList' export { OsmGeocoding } from './OsmGeocoding/OsmGeocoding' export { PinProfile } from './PinProfile/PinProfile' export { ProfileLink } from './ProfileLink/ProfileLink' +export { ProfileTagsList } from './ProfileTagsList/ProfileTagsList' export { ResearchEditorOverview } from './ResearchEditorOverview/ResearchEditorOverview' export { SearchField } from './SearchField/SearchField' export { Select } from './Select/Select' diff --git a/packages/components/src/test/utils.tsx b/packages/components/src/test/utils.tsx index 7de0820ff3..d2d9b4357b 100644 --- a/packages/components/src/test/utils.tsx +++ b/packages/components/src/test/utils.tsx @@ -1,6 +1,6 @@ -import { ThemeProvider } from '@emotion/react' import { createRemixStub } from '@remix-run/testing' import { render as testLibReact } from '@testing-library/react' +import { ThemeProvider } from '@theme-ui/core' import { preciousPlasticTheme } from 'oa-themes' import type { RenderOptions } from '@testing-library/react' diff --git a/packages/cypress/.env b/packages/cypress/.env index 0cc056de89..ef09288fb3 100644 --- a/packages/cypress/.env +++ b/packages/cypress/.env @@ -1 +1,4 @@ -CYPRESS_KEY=62585f33-d688-47b7-acb3-6d1dca832065 \ No newline at end of file +CYPRESS_KEY=62585f33-d688-47b7-acb3-6d1dca832065 +VITE_SITE_NAME=Precious Plastic +VITE_SITE_VARIANT=test-ci +VITE_PLATFORM_THEME=precious-plastic \ No newline at end of file diff --git a/packages/cypress/.eslintrc.json b/packages/cypress/.eslintrc.json index b0a7cd9839..69721d3f75 100644 --- a/packages/cypress/.eslintrc.json +++ b/packages/cypress/.eslintrc.json @@ -7,6 +7,7 @@ "cypress/no-force": "warn", "cypress/no-async-tests": "warn", "cypress/no-pause": "warn", - "mocha/no-skipped-tests": "error" + "mocha/no-skipped-tests": "error", + "mocha/no-exclusive-tests": "error" } } diff --git a/packages/cypress/cypress.config.ts b/packages/cypress/cypress.config.ts index 91bcff032b..f0d185165d 100644 --- a/packages/cypress/cypress.config.ts +++ b/packages/cypress/cypress.config.ts @@ -28,6 +28,7 @@ export default defineConfig({ baseUrl: 'http://localhost:3456', specPattern: 'src/integration/**/*.{js,jsx,ts,tsx}', supportFile: 'src/support/index.ts', + experimentalStudio: true, }, component: { devServer: { diff --git a/packages/cypress/package.json b/packages/cypress/package.json index 808356fdc9..8c7e9f771c 100644 --- a/packages/cypress/package.json +++ b/packages/cypress/package.json @@ -10,7 +10,7 @@ "@types/fs-extra": "^11.0.4", "@types/wait-on": "^5.3.4", "cross-env": "^7.0.3", - "cypress": "13.6.6", + "cypress": "13.15.1", "cypress-file-upload": "5.0.8", "dotenv": "^10.0.0", "eslint-plugin-mocha": "^10.4.3", @@ -24,5 +24,8 @@ }, "installConfig": { "hoistingLimits": "workspaces" + }, + "dependencies": { + "dateformat": "^5.0.3" } } diff --git a/packages/cypress/scripts/start.mts b/packages/cypress/scripts/start.mts index d9c7f23d87..d534ecde2b 100644 --- a/packages/cypress/scripts/start.mts +++ b/packages/cypress/scripts/start.mts @@ -90,14 +90,14 @@ async function main() { async function startAppServer() { const { CROSSENV_BIN } = PATHS // by default spawns will not respect colours used in stdio, so try to force - const crossEnvArgs = `FORCE_COLOR=1 VITE_SITE_VARIANT=test-ci` + const crossEnvArgs = `VITE_SITE_VARIANT=test-ci` // run local debug server for testing unless production build specified let serverCmd = `${CROSSENV_BIN} ${crossEnvArgs} BROWSER=none yarn start` // create local build if not running on ci (which will have build already generated) if (isCi) { - serverCmd = 'yarn start-ci' + serverCmd = `${CROSSENV_BIN} ${crossEnvArgs} yarn start-ci` } /******************* Run the main commands ******************* */ @@ -107,6 +107,10 @@ async function startAppServer() { shell: true, stdio: ['pipe', 'pipe', 'inherit'], cwd: PATHS.PLATFORM_ROOT_DIR, + env: { + ...process.env, + VITE_SITE_VARIANT: 'test-ci', + }, }) child.stdout.on('data', (d) => { @@ -134,7 +138,7 @@ function runTests() { const CI_BROWSER = e.CI_BROWSER || 'chrome' const CI_GROUP = e.CI_GROUP || '1x-chrome' // not currently used, but can pass variables accessed by Cypress.env() - const CYPRESS_ENV = `DUMMY_VAR=1` + const CYPRESS_ENV = `VITE_SITE_VARIANT=test-ci` // use workflow ID so that jobs running in parallel can be assigned to same cypress build // cypress will use this to split tests between parallel runs const buildId = e.CIRCLE_WORKFLOW_ID || generateAlphaNumeric(8) @@ -149,11 +153,14 @@ function runTests() { console.log(`Running cypress with cmd: ${testCMD}`) - const spawn = spawnSync(`${CROSSENV_BIN} FORCE_COLOR=1 ${testCMD}`, { - shell: true, - stdio: ['inherit', 'inherit', 'pipe'], - cwd: PATHS.WORKSPACE_DIR, - }) + const spawn = spawnSync( + `${CROSSENV_BIN} VITE_SITE_VARIANT=test-ci ${testCMD}`, + { + shell: true, + stdio: ['inherit', 'inherit', 'pipe'], + cwd: PATHS.WORKSPACE_DIR, + }, + ) console.log('testing complete with exit code', spawn.status) if (spawn.status === 1) { console.error('error', spawn.stderr.toString()) diff --git a/packages/cypress/src/fixtures/howto.ts b/packages/cypress/src/fixtures/howto.ts new file mode 100644 index 0000000000..f223c22764 --- /dev/null +++ b/packages/cypress/src/fixtures/howto.ts @@ -0,0 +1,55 @@ +import dateformat from 'dateformat' +import { DifficultyLevel, IModerationStatus } from 'oa-shared' + +import { generateAlphaNumeric } from '../utils/TestUtils' + +import type { IHowtoDB } from 'oa-shared' + +const _id = generateAlphaNumeric(20) +const _created = dateformat(Date.now(), 'yyyy-mm-dd') + +export const howto: IHowtoDB = { + _id, + _deleted: false, + _createdBy: 'howto_super_user', + _created, + title: 'Howto for discussion', + slug: 'howto-for-discussion', + previousSlugs: [], + mentions: [], + totalComments: 0, + time: '< 1 week', + description: 'Hi! Super quick how-to for commenting', + difficulty_level: DifficultyLevel.EASY, + files: [], + fileLink: 'http://google.com/', + total_downloads: 10, + steps: [ + { + title: 'Get the code', + images: [ + { + timeCreated: '2019-07-08T07:24:16.883Z', + name: '1.jpg', + fullPath: 'uploads/howtosV1/DBgbCKle7h4CcNxeUP2V/1.jpg', + type: 'image/jpeg', + updated: '2019-07-08T07:24:16.883Z', + size: 92388, + downloadUrl: + 'https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2FhowtosV1%2FDBgbCKle7h4CcNxeUP2V%2F1.jpg?alt=media&token=439a1dea-2dfc-4514-b725-d38aad85fe88', + contentType: 'image/jpeg', + }, + ], + text: 'First step go to Github, and download or clone our code. I’d recommend to install the Github app to add pull request in a later stage.\n', + _animationKey: 'unique1', + }, + ], + moderation: IModerationStatus.ACCEPTED, + tags: {}, + category: { + _id: '000', + _created, + _deleted: false, + label: 'product', + }, +} diff --git a/packages/cypress/src/fixtures/question.ts b/packages/cypress/src/fixtures/question.ts new file mode 100644 index 0000000000..90b4044f34 --- /dev/null +++ b/packages/cypress/src/fixtures/question.ts @@ -0,0 +1,33 @@ +import dateformat from 'dateformat' +import { IModerationStatus } from 'oa-shared' + +import { generateAlphaNumeric } from '../utils/TestUtils' + +import type { IQuestionDB } from 'oa-shared' + +const _id = generateAlphaNumeric(20) +const _created = dateformat(Date.now(), 'yyyy-mm-dd') + +export const question: IQuestionDB = { + _id, + _created, + _deleted: false, + description: 'Quick question for discussion testing.', + title: 'Quick question', + slug: 'quick-question', + previousSlugs: [], + tags: { + dibcwRYbQVzfQfmSkg5x: true, + }, + moderation: IModerationStatus.ACCEPTED, + _createdBy: 'super-question-creator', + creatorCountry: 'fr', + keywords: [], + questionCategory: { + _created: '2017-11-20T05:58:20.458Z', + _deleted: false, + _id: 'categoryLmj5B5UJh0M8BxSTP3uI', + _modified: '2018-07-29T04:34:49.982Z', + label: 'exhibition', + }, +} diff --git a/packages/cypress/src/fixtures/research.ts b/packages/cypress/src/fixtures/research.ts new file mode 100644 index 0000000000..b3f3712151 --- /dev/null +++ b/packages/cypress/src/fixtures/research.ts @@ -0,0 +1,51 @@ +import dateformat from 'dateformat' +import { IModerationStatus } from 'oa-shared' + +import { generateAlphaNumeric } from '../utils/TestUtils' + +import type { IResearchDB } from 'oa-shared' + +const research_id = generateAlphaNumeric(20) +const update_id = generateAlphaNumeric(20) +const _created = dateformat(Date.now(), 'yyyy-mm-dd') +const _createdBy = 'best-researcher' + +export const research: IResearchDB = { + _created, + _createdBy, + _deleted: false, + _id: research_id, + creatorCountry: 'ge', + collaborators: [], + description: 'All of this for the discussion tests.', + moderation: IModerationStatus.ACCEPTED, + title: 'Discussion research', + slug: 'discussion-research', + previousSlugs: ['discussion-research'], + tags: { + h1wCs0o9j60lkw3AYPB1: true, + }, + totalCommentCount: 0, + researchCategory: { + _modified: '2012-10-27T01:47:57.948Z', + _created: '2012-08-02T07:27:04.609Z', + _id: 'ehdI345E36hWyk3Ockr', + label: 'Best', + _deleted: false, + }, + updates: [ + { + _created, + _deleted: false, + _id: update_id, + description: 'qwerty', + collaborators: [_createdBy], + commentCount: 0, + images: [], + title: 'Dis update!', + files: [], + downloadCount: 0, + fileLink: '', + }, + ], +} diff --git a/packages/cypress/src/integration/SignUp.spec.ts b/packages/cypress/src/integration/SignUp.spec.ts index 945303d0d5..a103e39fe4 100644 --- a/packages/cypress/src/integration/SignUp.spec.ts +++ b/packages/cypress/src/integration/SignUp.spec.ts @@ -43,7 +43,7 @@ describe('[User sign-up]', () => { const { email, username, password } = user cy.signUpNewUser(user) - cy.logout() + cy.logout(false) cy.fillSignupForm(username, email, password) cy.contains(FRIENDLY_MESSAGES['sign-up/username-taken']).should( 'be.visible', @@ -55,7 +55,7 @@ describe('[User sign-up]', () => { const { email, username, password } = user cy.signUpNewUser(user) - cy.logout() + cy.logout(false) cy.fillSignupForm(`${username}-new`, email, password) cy.get('[data-cy=submit]').click() cy.get('[data-cy=error-msg]') diff --git a/packages/cypress/src/integration/common.spec.ts b/packages/cypress/src/integration/common.spec.ts index 41614b91e2..ea4cc0f5f3 100644 --- a/packages/cypress/src/integration/common.spec.ts +++ b/packages/cypress/src/integration/common.spec.ts @@ -18,29 +18,35 @@ describe('[Common]', () => { it('[Page Navigation]', () => { cy.visit('/how-to') + cy.wait(2000) cy.step('Go to Academy page') cy.get('[data-cy=page-link]').contains('Academy').click() + cy.wait(2000) cy.url().should('include', '/academy') cy.step('Go to How-to page') cy.get('[data-cy=page-link]').contains('How-to').click() + cy.wait(2000) cy.url().should('include', '/how-to') cy.step('Go to Map page') cy.get('[data-cy=page-link]').contains('Map').click() + cy.wait(2000) cy.url().should('include', '/map') }) describe('[User feeback button]', () => { it('[Desktop]', () => { cy.visit('/how-to') + cy.wait(2000) cy.get('[data-cy=feedback]').should('contain', 'Report a Problem') cy.get('[data-cy=feedback]') .should('have.attr', 'href') .and('contain', '/how-to?sort=Newest') cy.visit('/map') + cy.wait(2000) cy.get('[data-cy=feedback]') .should('have.attr', 'href') .and('contain', '/map') @@ -50,12 +56,14 @@ describe('[Common]', () => { cy.viewport('iphone-6') cy.visit('/how-to') + cy.wait(2000) cy.get('[data-cy=feedback]').should('contain', 'Problem?') cy.get('[data-cy=feedback]') .should('have.attr', 'href') .and('contain', '/how-to?sort=Newest') cy.visit('/map') + cy.wait(2000) cy.get('[data-cy=feedback]') .should('have.attr', 'href') .and('contain', '/map') @@ -66,6 +74,7 @@ describe('[Common]', () => { it('[By Anonymous]', () => { cy.step('Login and Join buttons are available') cy.visit('/how-to') + cy.wait(2000) cy.get('[data-cy=login]').should('be.visible') cy.get('[data-cy=join]').should('be.visible') cy.get('[data-cy=user-menu]').should('not.exist') @@ -77,6 +86,7 @@ describe('[Common]', () => { cy.step('Login and Join buttons are unavailable to logged-in users') const user = generateNewUserDetails() cy.signUpNewUser(user) + cy.wait(2000) cy.get('[data-cy=login]', { timeout: 20000 }).should('not.exist') cy.get('[data-cy=join]').should('not.exist') @@ -98,6 +108,7 @@ describe('[Common]', () => { cy.step('Logout the session') cy.toggleUserMenuOn() cy.clickMenuItem(UserMenuItem.LogOut) + cy.wait(2000) cy.get('[data-cy=login]', { timeout: 20000 }).should('be.visible') cy.get('[data-cy=join]').should('be.visible') }) diff --git a/packages/cypress/src/integration/howto/discussions.spec.ts b/packages/cypress/src/integration/howto/discussions.spec.ts index b94e6659f4..e819d287d5 100644 --- a/packages/cypress/src/integration/howto/discussions.spec.ts +++ b/packages/cypress/src/integration/howto/discussions.spec.ts @@ -1,7 +1,10 @@ // This is basically an identical set of steps to the discussion tests for // questions and research. Any changes here should be replicated there. +import { ExternalLinkLabel } from 'oa-shared' + import { MOCK_DATA } from '../../data' +import { howto } from '../../fixtures/howto' import { generateNewUserDetails } from '../../utils/TestUtils' const howtos = Object.values(MOCK_DATA.howtos) @@ -14,125 +17,79 @@ const howtoDiscussion = Object.values(MOCK_DATA.discussions).find( describe('[Howto.Discussions]', () => { it('can open using deep links', () => { const firstComment = howtoDiscussion.comments[0] - - cy.signUpNewUser() cy.visit(`/how-to/${item.slug}#comment:${firstComment._id}`) + cy.wait(2000) cy.checkCommentItem(firstComment.text, 2) }) it('allows authenticated users to contribute to discussions', () => { - const newComment = 'An interesting howto. The answer must be...' - const updatedNewComment = - 'An interesting howto. The answer must be that when the sky is red, the apocalypse _might_ be on the way.' - const newReply = 'Thanks Dave and Ben. What does everyone else think?' - const updatedNewReply = 'Anyone else?' - const visitor = generateNewUserDetails() + cy.addHowto(howto, visitor) cy.signUpNewUser(visitor) - cy.visit(`/how-to/${item.slug}`) + + const newComment = `An interesting howto. ${visitor.username}` + const updatedNewComment = `An interesting howto. The answer must be that when the sky is red, the apocalypse _might_ be on the way. Yours, ${visitor.username}` + const newReply = `Thanks Dave and Ben. What does everyone else think? - ${visitor.username}` + const updatedNewReply = `Anyone else? All the best, ${visitor.username}` + + const howtoPath = `/how-to/howto-for-discussion-${visitor.username}` cy.step('Can add comment') + cy.visit(howtoPath) + cy.contains('Start the discussion') + cy.contains('0 comments') cy.addComment(newComment) - cy.contains(`${howtoDiscussion.comments.length + 1} comments`) - cy.contains(newComment) - - // The following step isn't possible to test atm due to the relationship between - // the functions listen for changes and how cypress creates new document collections - // for each test run. - // cy.step('Updating user settings shows on comments') - // cy.visit('/settings') - // cy.setSettingBasicUserInfo({ - // country: 'Saint Lucia', - // description: "I'm a commenter", - // displayName: visitor.username, - // }) - // cy.setSettingImage('avatar', 'userImage') - // cy.setSettingAddContactLink({ - // index: 0, - // label: ExternalLinkLabel.SOCIAL_MEDIA, - // url: 'http://something.to.delete/', - // }) - // cy.saveSettingsForm() - - // cy.visit(`/how-to/${item.slug}`) - // cy.get('[data-cy="country:lc"]') - // cy.get('[data-cy="commentAvatarImage"]') - // .should('have.attr', 'src') - // .and('include', 'avatar') + cy.contains('1 comment') cy.step('Can edit their comment') - cy.editDiscussionItem('CommentItem', updatedNewComment) - cy.contains(updatedNewComment) - cy.contains(newComment).should('not.exist') + cy.editDiscussionItem('CommentItem', newComment, updatedNewComment) - cy.step('Can delete their comment') - cy.deleteDiscussionItem('CommentItem') - cy.contains(updatedNewComment).should('not.exist') - cy.contains(`${howtoDiscussion.comments.length} comments`) - - cy.step('Can add reply') + cy.step('Another user can add reply') + const secondCommentor = generateNewUserDetails() + cy.logout() + cy.signUpNewUser(secondCommentor) + cy.visit(howtoPath) cy.addReply(newReply) - cy.contains(`${howtoDiscussion.comments.length + 1} comments`) - cy.contains(newReply) - cy.queryDocuments('howtos', '_id', '==', item._id).then((docs) => { - const [howto] = docs - expect(howto.totalComments).to.eq(howtoDiscussion.comments.length + 1) - // Updated to the just added comment iso datetime - expect(howto.latestCommentDate).to.not.eq(item.latestCommentDate) - }) + cy.wait(1000) + cy.contains('2 comments') cy.step('Can edit their reply') - cy.editDiscussionItem('ReplyItem', updatedNewReply) - cy.contains(updatedNewReply) - cy.contains(newReply).should('not.exist') - - cy.step('Can delete their reply') - cy.deleteDiscussionItem('ReplyItem') - cy.contains(updatedNewReply).should('not.exist') - cy.contains(`${howtoDiscussion.comments.length} comments`) - cy.queryDocuments('howtos', '_id', '==', item._id).then((docs) => { - const [howto] = docs - expect(howto.totalComments).to.eq(howtoDiscussion.comments.length) - expect(howto.latestCommentDate).to.eq(item.latestCommentDate) + cy.editDiscussionItem('ReplyItem', newReply, updatedNewReply) + + cy.step('Updating user settings shows on comments') + cy.visit('/settings') + cy.get('[data-cy=loader]').should('not.exist') + cy.setSettingBasicUserInfo({ + country: 'Saint Lucia', + description: "I'm a commenter", + displayName: secondCommentor.username, + }) + cy.setSettingImage('avatar', 'userImage') + cy.setSettingAddContactLink({ + index: 0, + label: ExternalLinkLabel.SOCIAL_MEDIA, + url: 'http://something.to.delete/', }) + cy.saveSettingsForm() + + cy.step('Another user can leave a reply') + const secondReply = `Quick reply. ${visitor.username}` + + cy.step('First commentor can respond') + cy.logout() + cy.login(visitor.email, visitor.password) + cy.visit(howtoPath) + + cy.addReply(secondReply) - // Putting these at the end to avoid having to put a wait in the test - cy.step('Comment generated notification for question author') - cy.queryDocuments('users', 'userName', '==', item._createdBy).then( - (docs) => { - const [user] = docs - const discussionNotification = user.notifications.find( - ({ type, triggeredBy }) => - type === 'new_comment_discussion' && - triggeredBy.userId === visitor.username, - ) - expect(discussionNotification.relevantUrl).to.include( - `/how-to/${item.slug}#comment:`, - ), - expect(discussionNotification.title).to.eq(item.title), - expect(discussionNotification.triggeredBy.userId).to.eq( - visitor.username, - ) - }, - ) - - cy.step('Reply generates notification for comment author') - cy.queryDocuments('users', 'userName', '==', 'howto_creator').then( - (docs) => { - const [user] = docs - const discussionNotification = user.notifications.find( - ({ type, triggeredBy }) => - type === 'new_comment_discussion' && - triggeredBy.userId === visitor.username, - ) - expect(discussionNotification.relevantUrl).to.include( - `/how-to/${item.slug}#comment:`, - ), - expect(discussionNotification.title).to.eq(item.title), - expect(discussionNotification.triggeredBy.userId).to.eq( - visitor.username, - ) - }, - ) + cy.step('Can delete their comment') + cy.deleteDiscussionItem('CommentItem', updatedNewComment) + + cy.step('Replies still show for deleted comments') + cy.get('[data-cy="deletedComment"]').should('be.visible') + cy.get('[data-cy=OwnReplyItem]').contains(secondReply) + + cy.step('Can delete their reply') + cy.deleteDiscussionItem('ReplyItem', secondReply) }) }) diff --git a/packages/cypress/src/integration/howto/read.spec.ts b/packages/cypress/src/integration/howto/read.spec.ts index 44a744594f..4f81e42631 100644 --- a/packages/cypress/src/integration/howto/read.spec.ts +++ b/packages/cypress/src/integration/howto/read.spec.ts @@ -74,15 +74,19 @@ describe('[How To]', () => { ) expect($summary).to.contain('3-4 weeks', 'Duration') expect($summary).to.contain(DifficultyLevel.HARD, 'Difficulty') - expect($summary).to.contain('product', 'Tag') - expect($summary).to.contain('injection', 'Tag') expect($summary.find('img[alt="how-to cover"]')) .to.have.attr('src') .match(coverFileRegex) - expect($summary.find('[data-cy=file-download-counter]')).to.contain( - '1,234 downloads', - ) }) + cy.wait(2000) + cy.get('[data-cy=tag-list]').then(($tagList) => { + expect($tagList).to.contain('product') + expect($tagList).to.contain('injection') + }) + cy.get('[data-cy=file-download-counter]').should( + 'contain', + '1,234 downloads', + ) cy.step('Breadcrumbs work') cy.get('[data-cy=breadcrumbsItem]').first().should('contain', 'How To') @@ -104,7 +108,7 @@ describe('[How To]', () => { cy.get('[data-cy=breadcrumbsItem]').eq(2).should('contain', howto.title) cy.step('Download file button should redirect to sign in') - cy.get('div[data-tip="Login to download"]') + cy.get('div[data-tooltip-content="Login to download"]') .first() .click() .url() @@ -115,7 +119,7 @@ describe('[How To]', () => { cy.get('[data-cy^=step_]').should('have.length', 12) cy.step('All step info is shown') - cy.get('[data-cy=step_11]').within(($step) => { + cy.get('[data-cy=step_12]').within(($step) => { // const pic1Regex = /brick-12-1.jpg/ // const pic3Regex = /brick-12.jpg/ expect($step).to.contain('12', 'Step #') @@ -158,9 +162,9 @@ describe('[How To]', () => { it('[Allows opening of attachments]', () => { cy.signUpNewUser() cy.visit(specificHowtoUrl) - cy.step('[Presents the donation request before opening of attachments]') cy.step('Shows modal') + cy.wait(2000) cy.get('[data-cy=downloadButton]').first().click() cy.get('[data-cy=DonationRequest]').should('be.visible') cy.get('[data-cy=DonationRequest]').contains('Support our work') @@ -257,11 +261,7 @@ describe('[How To]', () => { it('[Redirects to search]', () => { cy.visit(howToNotFoundUrl) - cy.location('pathname').should('eq', '/how-to/') - cy.location('search').should( - 'eq', - `?search=this+how+to+does+not+exist&source=how-to-not-found&sort=Newest`, - ) + cy.get('[data-test="NotFound: Heading"').should('be.visible') }) }) }) diff --git a/packages/cypress/src/integration/howto/write.spec.ts b/packages/cypress/src/integration/howto/write.spec.ts index e3476f8bc0..06aa3b2413 100644 --- a/packages/cypress/src/integration/howto/write.spec.ts +++ b/packages/cypress/src/integration/howto/write.spec.ts @@ -6,9 +6,7 @@ import { HOWTO_TITLE_MIN_LENGTH, } from '../../../../../src/pages/Howto/constants' import { guidance, headings } from '../../../../../src/pages/Howto/labels' - -const creatorEmail = 'howto_creator@test.com' -const creatorPassword = 'test1234' +import { generateNewUserDetails } from '../../utils/TestUtils' describe('[How To]', () => { beforeEach(() => { @@ -104,18 +102,19 @@ describe('[How To]', () => { } describe('[Create a how-to]', () => { - const alpha = faker.random.alphaNumeric(5) + const randomId = faker.random.alphaNumeric(8) + const creator = generateNewUserDetails() const expected = { - _createdBy: 'howto_creator', + _createdBy: creator.username, _deleted: false, category: 'Moulds', description: 'After creating, the how-to will be deleted', moderation: IModerationStatus.AWAITING_MODERATION, difficulty_level: DifficultyLevel.MEDIUM, time: '1-2 weeks', - title: `Create a how-to test ${alpha}`, - slug: `create-a-how-to-test-${alpha}`, - previousSlugs: ['qwerty', `create-a-how-to-test-${alpha}`], + title: `Create a how-to test ${randomId}`, + slug: `create-a-how-to-test-${randomId}`, + previousSlugs: ['qwerty', `create-a-how-to-test-${randomId}`], fileLink: 'http://google.com/', files: [], total_downloads: 0, @@ -150,25 +149,12 @@ describe('[How To]', () => { }, { _animationKey: 'unique3', - images: [ - { - contentType: 'image/jpeg', - name: 'howto-step-pic1.jpg', - size: 19410, - type: 'image/jpeg', - }, - { - contentType: 'image/jpeg', - name: 'howto-step-pic2.jpg', - size: 20009, - type: 'image/jpeg', - }, - ], text: faker.lorem .sentences(50) .slice(0, HOWTO_STEP_DESCRIPTION_MAX_LENGTH) .trim(), title: 'A long title that is the total characters limit of', + videoURL: 'https://www.youtube.com/watch?v=Os7dREQ00l4', }, { _animationKey: 'unique2', @@ -199,10 +185,12 @@ describe('[How To]', () => { const categoryGuidanceMain = guidance.moulds.main.slice(0, 40) const categoryGuidanceFiles = guidance.moulds.files - cy.login(creatorEmail, creatorPassword) + cy.signUpNewUser(creator) cy.get('[data-cy=loader]').should('not.exist') cy.get('[data-cy="MemberBadge-member"]').should('be.visible') - cy.step('Access the create-how-to') + cy.visit('/how-to') + + cy.step('Access the create how-to page') cy.get('a[href="/how-to/create"]').should('be.visible') cy.get('[data-cy=create]').click() cy.contains('Create a How-To').should('be.visible') @@ -234,12 +222,13 @@ describe('[How To]', () => { ) cy.step('A basic draft was created') - cy.fillIntroTitle('qwerty') + cy.fillIntroTitle(`qwerty ${randomId}`) cy.get('[data-cy=draft]').click() + const firstSlug = `/how-to/qwerty-${randomId}` cy.get('[data-cy=view-howto]:enabled', { timeout: 20000 }) .click() .url() - .should('include', `/how-to/qwerty`) + .should('include', firstSlug) cy.get('[data-cy=moderationstatus-draft]').should('be.visible') cy.step('Back to completing the how-to') @@ -271,15 +260,13 @@ describe('[How To]', () => { .attachFile('images/howto-intro.jpg') fillStep(1, steps[0].title, steps[0].text, imagePaths) - fillStep(2, steps[2].title, steps[2].text, [], steps[2].videoURL) cy.step('Move step two down to step three') cy.get(`[data-cy=step_${1}]:visible`) .find('[data-cy=move-step-down]') .click() - - fillStep(2, steps[1].title, steps[1].text, imagePaths) + fillStep(2, steps[1].title, steps[1].text, [], steps[1].videoURL) cy.step('Add extra step') cy.get('[data-cy=add-step]').click() @@ -293,6 +280,7 @@ describe('[How To]', () => { } }) + cy.step('Can remove extra steps') deleteStep(4) cy.screenClick() @@ -313,11 +301,24 @@ describe('[How To]', () => { cy.get('[data-cy=file-download-counter]') .contains(total_downloads) .should('be.visible') - cy.queryDocuments('howtos', 'title', '==', title).then((docs) => { - cy.log('queryDocs', docs) - cy.wrap(null) - .then(() => docs[0]) - .should('eqHowto', expected) + // Check against UI + cy.get('[data-cy=how-to-title]').should('contain', title) + cy.get('[data-cy=how-to-description]').should('contain', description) + + // Check category + cy.get('[data-cy=category]').should('contain', category) + + // Check difficulty level + cy.get('[data-cy=difficulty-level]').should('contain', difficulty_level) + + // Check steps + steps.forEach((step, index) => { + cy.get(`[data-cy=step_${index + 1}]`) + .find('[data-cy=step-title]') + .should('contain', step.title) + cy.get(`[data-cy=step_${index + 1}]`) + .find('[data-cy=step-text]') + .should('contain', step.text) }) }) @@ -328,7 +329,7 @@ describe('[How To]', () => { }) it('[Warning on leaving page]', () => { - cy.login(creatorEmail, creatorPassword) + cy.login(creator.email, creator.password) cy.get('[data-cy=loader]').should('not.exist') cy.step('Access the create-how-to') cy.get('a[href="/how-to/create"]').should('be.visible') @@ -344,221 +345,4 @@ describe('[How To]', () => { cy.url().should('match', /\/how-to?/) }) }) - - describe('[Edit a how-to]', () => { - const howtoUrl = '/how-to/set-up-devsite-to-help-coding' - const editHowtoUrl = '/how-to/set-up-devsite-to-help-coding/edit' - const editTitle = faker.random.alphaNumeric(5) - const expected = { - _createdBy: 'howto_editor', - _deleted: false, - category: 'exhibition', - moderation: IModerationStatus.ACCEPTED, - description: 'After editing, all changes are reverted', - difficulty_level: DifficultyLevel.HARD, - files: [], - fileLink: 'http://google.com/', - total_downloads: 10, - slug: `this-is-an-edit-test-${editTitle}`, - previousSlugs: [ - 'set-up-devsite-to-help-coding', - `this-is-an-edit-test-${editTitle}`, - ], - tags: { EOVeOZaKKw1UJkDIf3c3: true }, - time: '3-4 weeks', - title: `This is an edit test ${editTitle}`, - cover_image: { - contentType: 'image/jpeg', - name: 'howto-intro.jpg', - size: 19897, - type: 'image/jpeg', - }, - steps: [ - { - _animationKey: 'unique1', - images: [ - { - contentType: 'image/jpeg', - name: 'howto-step-pic1.jpg', - size: 19410, - type: 'image/jpeg', - }, - { - contentType: 'image/jpeg', - name: 'howto-step-pic2.jpg', - size: 20009, - type: 'image/jpeg', - }, - ], - text: 'Description for step 1. This description should be between the minimum and maximum description length', - title: 'Step 1 is easy', - }, - { - _animationKey: 'unique2', - images: [ - { - contentType: 'image/jpeg', - name: 'howto-step-pic1.jpg', - size: 19410, - type: 'image/jpeg', - }, - { - contentType: 'image/jpeg', - name: 'howto-step-pic2.jpg', - size: 20009, - type: 'image/jpeg', - }, - ], - text: 'Description for step 2. This description should be between the minimum and maximum description length', - title: 'Step 2 is easy', - }, - { - _animationKey: 'unique3', - images: [ - { - contentType: 'image/jpeg', - name: '3.1.jpg', - size: 141803, - type: 'image/jpeg', - }, - { - contentType: 'image/jpeg', - name: '3.2.jpg', - size: 211619, - type: 'image/jpeg', - }, - { - contentType: 'image/jpeg', - name: '3.4.jpg', - size: 71309, - type: 'image/jpeg', - }, - ], - text: 'Description for step 3. This description should be between the minimum and maximum description length', - title: 'Step 3 is easy', - }, - ], - } - - it('[By Anonymous]', () => { - cy.step('Prevent anonymous access to edit howto') - cy.visit(editHowtoUrl) - cy.get('[data-cy=BlockedRoute]').should('be.visible') - }) - - it('[By Authenticated]', () => { - cy.step('Prevent non-owner access to edit howto') - cy.visit('/how-to') - cy.login(creatorEmail, creatorPassword) - cy.visit(editHowtoUrl) - // user should be redirect to how-to page - cy.location('pathname').should('eq', howtoUrl) - }) - - it('[By Owner]', () => { - cy.login('howto_editor@test.com', 'test1234') - - cy.step('Go to Edit mode') - cy.visit(howtoUrl) - cy.get('[data-cy=edit]').click() - - cy.step('Warn if title is identical with the existing ones') - cy.get('[data-cy=intro-title]').focus().blur({ force: true }) - cy.contains( - 'Did you know there is an existing how-to with the title', - ).should('not.exist') - - cy.step('Warn if title has less than minimum required characters') - cy.fillIntroTitle('qwer') - cy.contains( - `Should be more than ${HOWTO_TITLE_MIN_LENGTH} characters`, - ).should('be.visible') - - cy.fillIntroTitle('Make glass-like beams') - cy.contains( - "Did you know there is an existing how-to with the title 'Make glass-like beams'? Using a unique title helps readers decide which how-to better meet their needs.", - ).should('be.visible') - - cy.step('Update the intro') - cy.fillIntroTitle(expected.title) - cy.selectTag('howto_testing') - selectCategory(expected.category as Category) - selectTimeDuration(expected.time as Duration) - selectDifficultLevel(expected.difficulty_level) - cy.get('[data-cy=intro-description]').clear().type(expected.description) - - cy.step('Update a new cover for the intro') - - cy.get('[data-cy="intro-cover"]') - .find('[data-cy="delete-image"]') - .click({ force: true }) - - cy.get('[data-cy="intro-cover"]') - .find(':file') - .attachFile('images/howto-intro.jpg') - - cy.step('Upload a new file') - - cy.get('[data-cy="files"]').click({ force: true }) - - cy.fixture('files/Example.pdf').then((fileContent) => { - cy.get('[data-cy="uppy-dashboard"] .uppy-Dashboard-input').attachFile({ - fileContent: fileContent, - fileName: 'example.pdf', - mimeType: 'application/pdf', - }) - }) - - cy.contains('Upload 1 file').click() - - cy.step('Steps beyond the minimum can be deleted') - deleteStep(5) - deleteStep(4) - - expected.steps.forEach((step, index) => { - fillStep(index + 1, step.title, step.text, [ - 'images/howto-step-pic1.jpg', - 'images/howto-step-pic2.jpg', - ]) - }) - - cy.step('Submit updated Howto') - - cy.get('[data-cy=submit]').click() - cy.get('[data-cy=invalid-file-warning]').should('be.visible') - - cy.get('[data-cy=fileLink]').clear() - cy.get('[data-cy=submit]').click() - cy.get('[data-cy=invalid-file-warning]').should('not.exist') - - cy.step('Open the updated how-to') - - cy.get('[data-cy=view-howto]:enabled', { timeout: 20000 }) - .click() - .url() - .should('include', '/how-to/this-is-an-edit-test') - cy.get('[data-cy=how-to-basis]').contains( - `This is an edit test ${editTitle}`, - ) - cy.get('[data-cy=file-download-counter]') - .contains(expected.total_downloads) - .should('be.visible') - cy.queryDocuments( - 'howtos', - 'title', - '==', - `This is an edit test ${editTitle}`, - ).then((docs) => { - cy.log('queryDocs', docs) - cy.wrap(null) - .then(() => docs[0]) - .should('eqHowto', expected) - }) - - cy.step('Open the old slug') - - cy.visit('/how-to/set-up-devsite-to-help-coding') - cy.get('[data-cy=how-to-basis]').contains('This is an edit test') - }) - }) }) diff --git a/packages/cypress/src/integration/map.spec.ts b/packages/cypress/src/integration/map.spec.ts index e9fca9aa31..526775b0b1 100644 --- a/packages/cypress/src/integration/map.spec.ts +++ b/packages/cypress/src/integration/map.spec.ts @@ -1,9 +1,13 @@ -const userId = 'davehakkens' +const userId = 'demo_user' const profileTypesCount = 5 const urlLondon = 'https://nominatim.openstreetmap.org/search?format=json&q=london&accept-language=en' describe('[Map]', () => { + beforeEach(() => { + localStorage.setItem('VITE_THEME', 'fixing-fashion') + }) + it('[Shows expected pins]', () => { cy.viewport('macbook-16') @@ -29,7 +33,7 @@ describe('[Map]', () => { cy.step('New map shows the cards') cy.get('[data-cy="welome-header"]').should('be.visible') cy.get('[data-cy="CardList-desktop"]').should('be.visible') - cy.get('[data-cy="list-results"]').contains('52 results in view') + cy.get('[data-cy="list-results"]').contains(/\d+ results in view/) cy.step('Map filters can be used') cy.get('[data-cy=MapFilterProfileTypeCardList]') @@ -40,16 +44,22 @@ describe('[Map]', () => { // Reduction in coverage until temp API removed // cy.get('[data-cy="list-results"]').contains('6 results in view') cy.get('[data-cy=MapListFilter-active]').first().click() - cy.get('[data-cy="list-results"]').contains('52 results in view') + cy.get('[data-cy="list-results"]').contains(/\d+ results in view/) + + cy.step('Clusters show up') + cy.get('.icon-cluster-many') + .first() + .within(() => { + cy.get('.icon-cluster-text').contains(/\d+/) + }) cy.step('Users can select filters') cy.get('[data-cy=MapFilterList]').should('not.exist') cy.get('[data-cy=MapFilterList-OpenButton]').first().click() cy.get('[data-cy=MapFilterList]').should('be.visible') cy.get('[data-cy=MapFilterList-CloseButton]').first().click() - cy.step('As the user moves in the list updates') - for (let i = 0; i < 9; i++) { + for (let i = 0; i < 6; i++) { cy.get('.leaflet-control-zoom-in').click() } cy.get('[data-cy="list-results"]').contains('1 result') @@ -76,10 +86,10 @@ describe('[Map]', () => { cy.get('[data-cy=PinProfileCloseButton]').click() cy.url().should('not.include', `#${userId}`) cy.get('[data-cy=PinProfile]').should('not.exist') - - cy.step('New map pins can be hidden by clicking the map') cy.get(`[data-cy=pin-${userId}]`).click() cy.url().should('include', `#${userId}`) + + cy.step('New map pins can be hidden by clicking the map') cy.get('[data-cy=PinProfile]').should('be.visible') cy.get('.markercluster-map').click(10, 10) cy.url().should('not.include', `#${userId}`) @@ -87,12 +97,16 @@ describe('[Map]', () => { cy.step('Mobile list view can be shown') cy.viewport('samsung-note9') + cy.get('.leaflet-control-zoom-out').click() + cy.get('.leaflet-control-zoom-out').click() + cy.get('.leaflet-control-zoom-out').click() cy.get('[data-cy="CardList-desktop"]').should('not.be.visible') cy.get('[data-cy="CardList-mobile"]').should('not.be.visible') cy.get('[data-cy="ShowMobileListButton"]').click() cy.get('[data-cy="CardList-mobile"]').within(() => { cy.get('[data-cy=CardListItem]') + .last() .within(() => { cy.contains(userId) cy.get('[data-cy="MemberBadge-member"]') @@ -117,12 +131,6 @@ describe('[Map]', () => { cy.wait('@londonSearch') cy.contains('London, Greater London, England, United Kingdom').click() - // cy.get('.icon-cluster-many') - // .should('be.visible') - // .within(() => { - // cy.get('.icon-cluster-text').should('have.text', '3').and('be.visible') - // }) - cy.step('Shows the zoom out and zoom in buttons') cy.get('[data-cy="WorldViewButton"]', { timeout: 10000 }) .should('exist') @@ -137,18 +145,22 @@ describe('[Map]', () => { // }) cy.step('Zoom in button prompts for user location and zooms') - cy.stub(window.navigator.geolocation, 'getCurrentPosition').callsFake( - (cb) => { - return cb({ coords: { latitude: 40.7128, longitude: -74.006 } }) - }, - ) - cy.get('[data-cy="LocationViewButton"]').click() - cy.window() - .its('map') - .invoke('getCenter') - .should((center) => { - expect(center).to.have.property('lat', 40.7128) - expect(center).to.have.property('lng', -74.006) - }) + cy.get('[data-cy="WorldViewButton"]', { timeout: 10000 }) + .should('exist') + .and('be.visible') + cy.get('[data-cy="LocationViewButton"]').should('exist').and('be.visible') + // cy.stub(window.navigator.geolocation, 'getCurrentPosition').callsFake( + // (cb) => { + // return cb({ coords: { latitude: 40.7128, longitude: -74.006 } }) + // }, + // ) + // cy.get('[data-cy="LocationViewButton"]').click() + // cy.window() + // .its('map') + // .invoke('getCenter') + // .should((center) => { + // expect(center).to.have.property('lat', 40.7128) + // expect(center).to.have.property('lng', -74.006) + // }) }) }) diff --git a/packages/cypress/src/integration/notifications.spec.ts b/packages/cypress/src/integration/notifications.spec.ts index ef3f35423a..d664eb751e 100644 --- a/packages/cypress/src/integration/notifications.spec.ts +++ b/packages/cypress/src/integration/notifications.spec.ts @@ -8,19 +8,21 @@ import { generateNewUserDetails } from '../utils/TestUtils' const DB_WAIT_TIME = 5000 describe('[Notifications]', () => { - it('[are not generated when the howTo author is triggering notification]', () => { - cy.visit('how-to') - cy.login('event_reader@test.com', 'test1234') - cy.visit('/how-to/testing-testing') - cy.get('[data-cy="vote-useful"]').contains('useful').click() - cy.step('Verify the notification has not been added') - cy.queryDocuments('users', 'userName', '==', 'event_reader').then( - (docs) => { - expect(docs.length).to.be.greaterThan(0) - expect(docs[0]['notifications']).to.be.undefined - }, - ) - }) + // Can't test like this now because we are now using the same users collection for all tests. + // it('[are not generated when the howTo author is triggering notification]', () => { + // cy.visit('how-to') + // cy.login('event_reader@test.com', 'test1234') + // cy.visit('/how-to/testing-testing') + // cy.wait(2000) + // cy.get('[data-cy="vote-useful"]').contains('useful').click() + // cy.step('Verify the notification has not been added') + // cy.queryDocuments('users', 'userName', '==', 'event_reader').then( + // (docs) => { + // expect(docs.length).to.be.greaterThan(0) + // expect(docs[0]['notifications']).to.be.undefined + // }, + // ) + // }) it('[are generated by clicking on useful for how-tos]', () => { const visitor = generateNewUserDetails() @@ -28,6 +30,7 @@ describe('[Notifications]', () => { cy.visit('how-to') cy.visit('/how-to/testing-testing') + cy.wait(DB_WAIT_TIME) cy.get('[data-cy="vote-useful"]').contains('useful').click() cy.wait(DB_WAIT_TIME) cy.step('Verify the notification has been added') @@ -57,6 +60,7 @@ describe('[Notifications]', () => { cy.signUpNewUser(visitor) cy.visit('/research/qwerty') + cy.wait(DB_WAIT_TIME) cy.get('[data-cy="vote-useful"]').contains('useful').click() cy.wait(DB_WAIT_TIME) cy.step('Verify the notification has been added') diff --git a/packages/cypress/src/integration/profile.spec.ts b/packages/cypress/src/integration/profile.spec.ts index 2a0f82017b..32e0c927bb 100644 --- a/packages/cypress/src/integration/profile.spec.ts +++ b/packages/cypress/src/integration/profile.spec.ts @@ -7,7 +7,7 @@ import { MOCK_DATA } from '../data' import { UserMenuItem } from '../support/commandsUi' import { setIsPreciousPlastic } from '../utils/TestUtils' -const { admin, profile_views, profile_no_views, subscriber } = MOCK_DATA.users +const { admin, profile_views, subscriber } = MOCK_DATA.users const eventReader = MOCK_DATA.users.event_reader const machine = MOCK_DATA.users.settings_machine_new const userProfiletype = MOCK_DATA.users.settings_workplace_new @@ -91,7 +91,7 @@ describe('[Profile]', () => { cy.step('Submit form') cy.get('[data-cy=contact-submit]').click() - cy.contains(contact.successMessage).should('be.visible') + cy.contains(contact.successMessage) cy.step("Can't contact pages who opt-out") cy.visit(`/u/${userProfiletype.userName}`) @@ -159,15 +159,6 @@ describe('[By Beta Tester]', () => { it('[Displays view count for profile with views]', () => { cy.login(betaTester.email, betaTester.password) cy.visit(`/u/${profile_views.userName}`) - cy.get('[data-testid=profile-views-stat]').should( - 'contain.text', - profile_views.total_views, - ) - }) - - it('[Displays view count for profile with first view]', () => { - cy.login(betaTester.email, betaTester.password) - cy.visit(`/u/${profile_no_views.userName}`) - cy.get('[data-testid=profile-views-stat]').contains('1') + cy.get('[data-testid=profile-views-stat]').contains(/Views: \d+/) }) }) diff --git a/packages/cypress/src/integration/questions/discussions.spec.ts b/packages/cypress/src/integration/questions/discussions.spec.ts index 1b3be485a5..e5563b8810 100644 --- a/packages/cypress/src/integration/questions/discussions.spec.ts +++ b/packages/cypress/src/integration/questions/discussions.spec.ts @@ -2,138 +2,75 @@ // how-tos and research. Any changes here should be replicated there. import { MOCK_DATA } from '../../data' +import { question } from '../../fixtures/question' import { generateNewUserDetails } from '../../utils/TestUtils' -const questions = Object.values(MOCK_DATA.questions) - -const item = questions[0] -const discussion = Object.values(MOCK_DATA.discussions).find( - ({ sourceId }) => sourceId === item._id, -) - describe('[Questions.Discussions]', () => { it('can open using deep links', () => { - const firstComment = discussion.comments[0] + const item = Object.values(MOCK_DATA.questions)[0] + const discussion = Object.values(MOCK_DATA.discussions).find( + ({ sourceId }) => sourceId === item._id, + ) - cy.signUpNewUser() + const firstComment = discussion.comments[0] cy.visit(`/questions/${item.slug}#comment:${firstComment._id}`) + cy.wait(2000) cy.checkCommentItem('@demo_user - I like your logo', 2) }) it('allows authenticated users to contribute to discussions', () => { - const newComment = 'An interesting question. The answer must be...' - const updatedNewComment = - 'An interesting question. The answer must be that when the sky is red, the apocalypse _might_ be on the way.' - const newReply = 'Thanks Dave and Ben. What does everyone else think?' - const updatedNewReply = 'Anyone else?' - const secondReply = 'Quick reply' - const visitor = generateNewUserDetails() + cy.addQuestion(question, visitor) cy.signUpNewUser(visitor) - cy.visit(`/questions/${item.slug}`) + + const newComment = `An interesting question. The answer must be... ${visitor.username}` + const updatedNewComment = `An interesting question. The answer must be that when the sky is red, the apocalypse _might_ be on the way. Love, ${visitor.username}` + const newReply = `Thanks Dave and Ben. What does everyone else think? - ${visitor.username}` + const updatedNewReply = `Anyone else? Your truly ${visitor.username}` + + const questionPath = `/questions/quick-question-for-${visitor.username}` cy.step('Can add comment') + cy.visit(questionPath) + cy.contains('Start the discussion') + cy.contains('0 comments') cy.addComment(newComment) - cy.contains(`${discussion.comments.length + 1} comments`) - cy.contains(newComment) - cy.contains('less than a minute ago') + cy.contains('1 comment') cy.step('Can edit their comment') - cy.editDiscussionItem('CommentItem', updatedNewComment) - cy.contains(updatedNewComment) - cy.contains(newComment).should('not.exist') - cy.contains('Edited less than a minute ago') + cy.editDiscussionItem('CommentItem', newComment, updatedNewComment) + + cy.step('Another user can add reply') + const secondCommentor = generateNewUserDetails() - cy.step('Can add reply') + cy.logout() + cy.signUpNewUser(secondCommentor) + cy.visit(questionPath) cy.addReply(newReply) - cy.contains(`${discussion.comments.length + 2} comments`) - cy.contains(newReply) - cy.queryDocuments('questions', '_id', '==', item._id).then((docs) => { - const [question] = docs - expect(question.commentCount).to.eq(discussion.comments.length + 2) - // Updated to the just added comment iso datetime - expect(question.latestCommentDate).to.not.eq(item.latestCommentDate) - }) + cy.wait(1000) + cy.contains('2 comments') cy.step('Can edit their reply') - cy.editDiscussionItem('ReplyItem', updatedNewReply) - cy.contains(updatedNewReply) - cy.contains(newReply).should('not.exist') + cy.editDiscussionItem('ReplyItem', newReply, updatedNewReply) - cy.step('Can delete their reply') - cy.deleteDiscussionItem('ReplyItem') - cy.contains(updatedNewReply).should('not.exist') + cy.step('Another user can leave a reply') + const secondReply = `Quick reply. ${visitor.username}` - // Prep for: Replies still show for deleted comments - cy.get('[data-cy=show-replies]:last').click() - cy.get('[data-cy=reply-form]:last').type(secondReply) - cy.get('[data-cy=reply-submit]:last').click() - cy.contains('[data-cy="Confirm.modal: Modal"]').should('not.exist') - cy.get('[data-cy=ReplyItem]:last').contains(secondReply) + cy.step('First commentor can respond') + cy.logout() + cy.login(visitor.email, visitor.password) + cy.visit(questionPath) + + cy.addReply(secondReply) cy.step('Can delete their comment') - cy.deleteDiscussionItem('CommentItem') - cy.contains(updatedNewComment).should('not.exist') + cy.deleteDiscussionItem('CommentItem', updatedNewComment) cy.step('Replies still show for deleted comments') cy.get('[data-cy="deletedComment"]').should('be.visible') - cy.contains(secondReply) + cy.get('[data-cy=OwnReplyItem]').contains(secondReply) cy.step('Can delete their reply') - cy.deleteDiscussionItem('ReplyItem') - cy.contains(updatedNewReply).should('not.exist') - cy.contains(`${discussion.comments.length} comments`) - cy.queryDocuments('questions', '_id', '==', item._id).then((docs) => { - const [question] = docs - expect(question.commentCount).to.eq(discussion.comments.length) - expect(question.latestCommentDate).to.eq(item.latestCommentDate) - }) - cy.contains('[data-cy=deletedComment]').should('not.exist') - - // Putting these at the end to avoid having to put a wait in the test - cy.step('Comment generated notification for question author') - cy.queryDocuments('users', 'userName', '==', item._createdBy).then( - (docs) => { - const [user] = docs - const discussionNotification = user.notifications.find( - ({ type, triggeredBy }) => - type === 'new_comment_discussion' && - triggeredBy.userId === visitor.username, - ) - expect(discussionNotification.relevantUrl).to.include( - `/questions/${item.slug}#comment:`, - ), - expect(discussionNotification.title).to.eq(item.title), - expect(discussionNotification.triggeredBy.userId).to.eq( - visitor.username, - ) - }, - ) - - cy.step('Reply generates notification for comment author') - cy.queryDocuments('users', 'userName', '==', 'howto_creator').then( - (docs) => { - const [user] = docs - const discussionNotification = user.notifications.find( - ({ type, triggeredBy }) => - type === 'new_comment_discussion' && - triggeredBy.userId === visitor.username, - ) - expect(discussionNotification.relevantUrl).to.include( - `/questions/${item.slug}#comment:`, - ), - expect(discussionNotification.title).to.eq(item.title), - expect(discussionNotification.triggeredBy.userId).to.eq( - visitor.username, - ) - }, - ) - - cy.step('User avatars only visible to beta-testers') - cy.contains('[data-cy=commentAvatar]').should('not.exist') - cy.logout() - cy.login('demo_beta_tester@example.com', 'demo_beta_tester') - cy.visit(`/questions/${item.slug}`) - cy.get('[data-cy="commentAvatar"]') + cy.deleteDiscussionItem('ReplyItem', secondReply) }) }) diff --git a/packages/cypress/src/integration/questions/read.spec.ts b/packages/cypress/src/integration/questions/read.spec.ts index f3b8627fbf..a75ecffa90 100644 --- a/packages/cypress/src/integration/questions/read.spec.ts +++ b/packages/cypress/src/integration/questions/read.spec.ts @@ -24,7 +24,6 @@ describe('[Questions]', () => { describe('[Individual questions]', () => { it('[By Everyone]', () => { const { - _id, description, images, slug, @@ -34,10 +33,6 @@ describe('[Questions]', () => { questionCategory, } = question - const questionDiscussion = Object.values(MOCK_DATA.discussions).find( - (discussion) => discussion.sourceId === _id, - ) - const pageTitle = `${title} - Question - Precious Plastic` const image = images[0].downloadUrl @@ -45,9 +40,9 @@ describe('[Questions]', () => { cy.visit(`/questions/${slug}`) cy.step('All metadata visible') - cy.contains(`${question.subscribers.length} following`) - cy.contains(`${question.votedUsefulBy.length} useful`) - cy.contains(`${questionDiscussion.comments.length} comments`) + cy.contains(/\d+ following/) + cy.contains(/\d+ useful/) + cy.contains(/\d+ comments/) cy.step('[Populates title, SEO and social tags]') cy.title().should('eq', pageTitle) diff --git a/packages/cypress/src/integration/questions/write.spec.ts b/packages/cypress/src/integration/questions/write.spec.ts index 1e781ded2f..f5c363c981 100644 --- a/packages/cypress/src/integration/questions/write.spec.ts +++ b/packages/cypress/src/integration/questions/write.spec.ts @@ -1,20 +1,22 @@ import { MOCK_DATA } from '../../data' +import { generateAlphaNumeric } from '../../utils/TestUtils' const questions = Object.values(MOCK_DATA.questions) const item = questions[0] describe('[Question]', () => { describe('[Create a question]', () => { - const initialTitle = 'Health cost of plastic?' - const initialExpectedSlug = 'health-cost-of-plastic' + const initialRandomId = generateAlphaNumeric(8).toLowerCase() + const initialTitle = initialRandomId + ' Health cost of plastic?' + const initialExpectedSlug = initialRandomId + '-health-cost-of-plastic' const initialQuestionDescription = "Hello! I'm wondering how people feel about the health concerns about working with melting plastic and being in environments with microplastics. I have been working with recycling plastic (hdpe) for two years now, shredding and injection molding and haven't had any bad consequences yet. But with the low knowledge around micro plastics and its effects on the human body, and many concerns and hypotheses I have been a bit concerned lately.So I would like to ask the people in this community how you are feeling about it, and if you have experienced any issues with the microplastics or gases yet, if so how long have you been working with it? And what extra steps do you take to be protected from it? I use a gas mask with dust filters" const category = 'exhibition' const tag1 = 'product' const tag2 = 'workshop' - - const updatedTitle = 'Real health cost of plastic?' - const updatedExpectedSlug = 'real-health-cost-of-plastic' + const updatedRandomId = generateAlphaNumeric(8).toLowerCase() + const updatedTitle = updatedRandomId + ' Real health cost of plastic?' + const updatedExpectedSlug = updatedRandomId + '-real-health-cost-of-plastic' const updatedQuestionDescription = `${initialQuestionDescription} and super awesome goggles` it('[By Authenticated]', () => { diff --git a/packages/cypress/src/integration/research/discussions.spec.ts b/packages/cypress/src/integration/research/discussions.spec.ts index ef42727270..1c44322294 100644 --- a/packages/cypress/src/integration/research/discussions.spec.ts +++ b/packages/cypress/src/integration/research/discussions.spec.ts @@ -1,7 +1,10 @@ // This is basically an identical set of steps to the discussion tests for // questions and how-tos. Any changes here should be replicated there. +import { ExternalLinkLabel } from 'oa-shared' + import { MOCK_DATA } from '../../data' +import { research } from '../../fixtures/research' import { generateNewUserDetails } from '../../utils/TestUtils' const item = Object.values(MOCK_DATA.research)[0] @@ -12,132 +15,95 @@ const discussion = Object.values(MOCK_DATA.discussions).find( describe('[Research.Discussions]', () => { const firstComment = discussion.comments[0] + it('can open using deep links', () => { const commentUrl = `/research/${item.slug}#update_${item.updates[0]._id}-comment:${firstComment._id}` cy.visit(commentUrl) + cy.wait(2000) cy.checkCommentItem(firstComment.text, 1) }) it('allows authenticated users to contribute to discussions', () => { const visitor = generateNewUserDetails() + cy.addResearch(research, visitor) cy.signUpNewUser(visitor) - cy.visit(`/research/${item.slug}`) - - const comment = 'An example comment' - const updatedNewComment = "I've updated my comment now" - const newReply = "An interesting point, I hadn't thought about that." - const updatedNewReply = "I hadn't thought about that. Really good point." - const updateId = item.updates[0]._id - - cy.step('Can create their own comment') - cy.get('[data-cy="HideDiscussionContainer: button open-comments"]') - .first() - .contains('View 1 comment') - .click() - cy.get('[data-cy="comments-form"]').type(comment) - cy.get('[data-cy="comment-submit"]').click() - cy.get(`[data-cy="ResearchUpdate: ${updateId}"]`).contains('2 Comments') - cy.get('[data-cy="CommentItem"]').last().should('contain', comment) - - cy.step('Can edit their own comment') - cy.editDiscussionItem('CommentItem', updatedNewComment) - cy.contains(updatedNewComment) - cy.contains(comment).should('not.exist') - - cy.step('Can delete their own comment') - cy.deleteDiscussionItem('CommentItem') - cy.contains(updatedNewComment).should('not.exist') - - cy.step('Can add reply') + + const newComment = `An example comment from ${visitor.username}` + const updatedNewComment = `I've updated my comment now. Love ${visitor.username}` + + const researchPath = `/research/${visitor.username}-in-discussion-research` + + cy.step('Can add comment') + + cy.visit(researchPath) + cy.get( + '[data-cy="HideDiscussionContainer: button open-comments no-comments"]', + ).click() + cy.contains('Start the discussion') + cy.contains('0 comments') + + cy.addComment(newComment) + cy.contains('1 Comment') + + cy.step('Can edit their comment') + cy.editDiscussionItem('CommentItem', newComment, updatedNewComment) + + cy.step('Another user can add reply') + const secondCommentor = generateNewUserDetails() + const newReply = `An interesting point, I hadn't thought about that. All the best ${secondCommentor.username}` + const updatedNewReply = `I hadn't thought about that. Really good point. ${secondCommentor.username}` + + cy.logout() + + cy.signUpNewUser(secondCommentor) + cy.visit(researchPath) + cy.get( + '[data-cy="HideDiscussionContainer: button open-comments has-comments"]', + ).click() + cy.addReply(newReply) - cy.contains(`${discussion.comments.length + 1} Comments`) - cy.contains(newReply) cy.wait(1000) - cy.queryDocuments('research', '_id', '==', item._id).then((docs) => { - const [research] = docs - expect(research.totalCommentCount).to.eq(discussion.comments.length + 1) - // Updated to the just added comment iso datetime - expect(research.latestCommentDate).to.not.eq(item.latestCommentDate) - }) + cy.contains('2 Comments') cy.step('Can edit their reply') - cy.editDiscussionItem('ReplyItem', updatedNewReply) - cy.contains(updatedNewReply) - cy.contains(newReply).should('not.exist') - - cy.step('Can delete their reply') - cy.deleteDiscussionItem('ReplyItem') - cy.contains(updatedNewReply).should('not.exist') - cy.contains(`1 Comment`) - - cy.step('Check comments number after deletion') - cy.get('[data-cy="HideDiscussionContainer: button false"]').click() - cy.get('[data-cy="HideDiscussionContainer: button open-comments"]') - .first() - .contains('View 1 comment') - cy.queryDocuments('research', '_id', '==', item._id).then((docs) => { - const [research] = docs - expect(research.totalCommentCount).to.eq(discussion.comments.length) - expect(research.latestCommentDate).to.eq(item.latestCommentDate) + cy.editDiscussionItem('ReplyItem', newReply, updatedNewReply) + + cy.step('Updating user settings shows on comments') + cy.visit('/settings') + cy.get('[data-cy=loader]').should('not.exist') + cy.setSettingBasicUserInfo({ + country: 'Saint Lucia', + description: "I'm a commenter", + displayName: secondCommentor.username, }) - - // Putting these at the end to avoid having to put a wait in the test - cy.step('Comment generated a notification for primary research author') - cy.queryDocuments('users', 'userName', '==', item._createdBy).then( - (docs) => { - const [user] = docs - const discussionNotification = user.notifications.find( - ({ type, triggeredBy }) => - type === 'new_comment_discussion' && - triggeredBy.userId === visitor.username, - ) - expect(discussionNotification.relevantUrl).to.include( - `/research/${item.slug}#update_${updateId}`, - ), - expect(discussionNotification.title).to.eq(item.title), - expect(discussionNotification.triggeredBy.userId).to.eq( - visitor.username, - ) - }, - ) - - cy.step('Comment generated a notification for update collaborators') - cy.queryDocuments( - 'users', - 'userName', - '==', - item.updates[0].collaborators[0], - ).then((docs) => { - const [user] = docs - const discussionNotification = user.notifications.find( - ({ type, triggeredBy }) => - type === 'new_comment_discussion' && - triggeredBy.userId === visitor.username, - ) - expect(discussionNotification.relevantUrl).to.include( - `/research/${item.slug}#update_${updateId}`, - ), - expect(discussionNotification.title).to.eq(item.title), - expect(discussionNotification.triggeredBy.userId).to.eq( - visitor.username, - ) + cy.setSettingImage('avatar', 'userImage') + cy.setSettingAddContactLink({ + index: 0, + label: ExternalLinkLabel.SOCIAL_MEDIA, + url: 'http://something.to.delete/', }) + cy.saveSettingsForm() + + cy.step('First commentor can respond') + const secondReply = `Quick reply. ${visitor.username}` - cy.step('Reply generated a notification for comment parent') - cy.queryDocuments('users', 'userName', '==', firstComment._creatorId).then( - (docs) => { - const [user] = docs - const discussionNotification = user.notifications.find( - ({ type }) => type === 'new_comment_discussion', - ) - expect(discussionNotification.relevantUrl).to.include( - `/research/${item.slug}#update_${updateId}`, - ), - expect(discussionNotification.title).to.eq(item.title), - expect(discussionNotification.triggeredBy.userId).to.eq( - visitor.username, - ) - }, - ) + cy.logout() + cy.login(visitor.email, visitor.password) + cy.visit(researchPath) + cy.get( + '[data-cy="HideDiscussionContainer: button open-comments has-comments"]', + ).click() + + cy.addReply(secondReply) + + cy.step('Can delete their comment') + cy.deleteDiscussionItem('CommentItem', updatedNewComment) + + cy.step('Replies still show for deleted comments') + cy.get('[data-cy="deletedComment"]').should('be.visible') + cy.get('[data-cy=OwnReplyItem]').contains(secondReply) + + cy.step('Can delete their reply') + cy.deleteDiscussionItem('ReplyItem', secondReply) }) }) diff --git a/packages/cypress/src/integration/research/follow.spec.ts b/packages/cypress/src/integration/research/follow.spec.ts index 07590a1b22..789b8216a4 100644 --- a/packages/cypress/src/integration/research/follow.spec.ts +++ b/packages/cypress/src/integration/research/follow.spec.ts @@ -24,6 +24,7 @@ describe('[Research]', () => { cy.step('Should follow on click') cy.get('[data-cy="follow-button"]').first().click() + cy.wait(2000) cy.get('[data-cy="follow-button"]') .first() .should('contain.text', 'Following') diff --git a/packages/cypress/src/integration/research/write.spec.ts b/packages/cypress/src/integration/research/write.spec.ts index 5b8aee1d04..6d36190635 100644 --- a/packages/cypress/src/integration/research/write.spec.ts +++ b/packages/cypress/src/integration/research/write.spec.ts @@ -42,7 +42,6 @@ describe('[Research]', () => { const newCollaborator = generateNewUserDetails() cy.signUpNewUser(newCollaborator) - cy.logout() cy.step('Create the research article') cy.login(researcherEmail, researcherPassword) @@ -156,8 +155,9 @@ describe('[Research]', () => { }) it('[Any PP user]', () => { - const title = 'PP plastic stuff' - const expectSlug = 'pp-plastic-stuff' + const randomId = generateAlphaNumeric(8).toLowerCase() + const title = randomId + ' PP plastic stuff' + const expectSlug = randomId + '-pp-plastic-stuff' const description = 'Bespoke research topic' const updateTitle = 'First wonderful update' @@ -166,7 +166,6 @@ describe('[Research]', () => { const updateVideoUrl = 'https://www.youtube.com/watch?v=U3mrj84p3cM' setIsPreciousPlastic() - cy.logout() cy.signUpNewUser() cy.step('Can access create form') @@ -187,7 +186,6 @@ describe('[Research]', () => { .type(description) cy.get('[data-cy=submit]').click() - cy.step('Publishes as expected') cy.get('[data-cy=view-research]:enabled', { timeout: 20000 }) .click() @@ -258,14 +256,14 @@ describe('[Research]', () => { describe('[Displays draft updates for Author]', () => { it('[By Authenticated]', () => { - const id = generateAlphaNumeric(5) - const updateTitle = `Create a research update ${id}` + const randomId = generateAlphaNumeric(8).toLowerCase() + const updateTitle = `${randomId} Create a research update` const updateDescription = 'This is the description for the update.' const updateVideoUrl = 'http://youtube.com/watch?v=sbcWY7t-JX8' const expected = { description: 'After creating, the research will be deleted.', - title: `Create research article test ${id}`, - slug: `create-research-article-test-${id.toLowerCase()}`, + title: `${randomId} Create research article test`, + slug: `${randomId}-create-research-article-test`, } cy.login(researcherEmail, researcherPassword) @@ -316,7 +314,7 @@ describe('[Research]', () => { cy.get('[data-cy=DraftUpdateLabel]').should('be.visible') cy.step('Draft not visible to others') - cy.logout() + cy.logout(false) cy.visit(`/research/${expected.slug}`) cy.get(updateTitle).should('not.exist') cy.get('[data-cy=DraftUpdateLabel]').should('not.exist') diff --git a/packages/cypress/src/integration/settings.spec.ts b/packages/cypress/src/integration/settings.spec.ts index 067b5b5dcc..b8f27755a2 100644 --- a/packages/cypress/src/integration/settings.spec.ts +++ b/packages/cypress/src/integration/settings.spec.ts @@ -21,453 +21,444 @@ const mapDetails = (description) => ({ searchKeyword: 'singapo', locationName: locationStub.value, }) - describe('[Settings]', () => { - beforeEach(() => { - cy.interceptAddressSearchFetch(SingaporeStubResponse) - setIsPreciousPlastic() - cy.visit('/sign-in') - }) + it('[Cancel edit profile and get confirmation]', () => { + cy.signUpNewUser() - describe('[Focus Member]', () => { - it('[Cancel edit profile and get confirmation]', () => { - cy.signUpNewUser() + cy.step('Go to User Settings') + cy.clickMenuItem(UserMenuItem.Settings) - cy.step('Go to User Settings') - cy.clickMenuItem(UserMenuItem.Settings) + cy.get('[data-cy=displayName').clear().type('Wrong user') - cy.get('[data-cy=displayName').clear().type('Wrong user') + cy.step('Confirm shown when attempting to go to another page') + cy.get('[data-cy=page-link]').contains('How-to').click() + cy.get('[data-cy="Confirm.modal: Modal"]').should('be.visible') + }) - cy.step('Confirm shown when attempting to go to another page') - cy.get('[data-cy=page-link]').contains('How-to').click() - cy.get('[data-cy="Confirm.modal: Modal"]').should('be.visible') + describe('[Fixing Fashion]', () => { + beforeEach(() => { + localStorage.setItem('VITE_PLATFORM_PROFILES', 'member,space') + cy.visit('/sign-in') }) - it('[Edit a new profile]', () => { - const country = 'Bolivia' - const userImage = 'avatar' - const displayName = 'settings_member_new' - const description = "I'm a very active member" - const mapPinDescription = 'Fun, vibrant and full of amazing people' - const profileType = 'member' - const user = generateNewUserDetails() - const url = 'https://social.network' - - cy.step('Incomplete profile banner visible when logged out') - cy.get('[data-cy=notificationBanner]').should('not.exist') - - cy.signUpNewUser(user) - - cy.step('Incomplete profile banner visible') - cy.get('[data-cy=emailNotVerifiedBanner]').should('be.visible') - cy.get('[data-cy=incompleteProfileBanner]').click() - - cy.step('Member profile badge shown in header by default') - cy.get(`[data-cy=MemberBadge-${profileType}]`) - - cy.setSettingFocus(profileType) - - cy.step("Can't save without required fields being populated") - cy.get('[data-cy=save]').click() - cy.get('[data-cy=errors-container]').should('be.visible') - cy.get('[data-cy=CompleteProfileHeader]').should('be.visible') - - cy.step('Can set the required fields') - cy.setSettingBasicUserInfo({ - displayName, - country, - description, - }) - cy.get('[data-cy="country:BO"]') - - cy.step('Errors if trying to upload invalid image') - cy.get(`[data-cy=userImage]`) - .find(':file') - .attachFile(`images/file.random`) - cy.get('[data-cy=ImageUploadError]').should('be.visible') - cy.get('[data-cy=ImageUploadError-Button]').click() - - cy.step('Can add avatar') - cy.setSettingImage(userImage, 'userImage') - - cy.step("Can't add cover image") - cy.get('[data-cy=coverImages]').should('not.exist') - - cy.setSettingAddContactLink({ - index: 0, - label: ExternalLinkLabel.SOCIAL_MEDIA, - url: 'http://something.to.delete/', - }) + it('[Member]', () => { + it('[Edit a new profile]', () => { + const country = 'Bolivia' + const userImage = 'avatar' + const displayName = 'settings_member_new' + const description = "I'm a very active member" + const mapPinDescription = 'Fun, vibrant and full of amazing people' + const profileType = 'member' + const user = generateNewUserDetails() + const url = 'https://social.network' + + cy.step('Incomplete profile banner visible when logged out') + cy.get('[data-cy=notificationBanner]').should('not.exist') + + cy.signUpNewUser(user) + + cy.step('Incomplete profile banner visible') + cy.get('[data-cy=emailNotVerifiedBanner]').should('be.visible') + cy.get('[data-cy=incompleteProfileBanner]').click() + + cy.step('Member profile badge shown in header by default') + cy.get(`[data-cy=MemberBadge-${profileType}]`) + + cy.setSettingFocus(profileType) + + cy.step("Can't save without required fields being populated") + cy.get('[data-cy=save]').click() + cy.get('[data-cy=errors-container]').should('be.visible') + cy.get('[data-cy=CompleteProfileHeader]').should('be.visible') + + cy.step('Can set the required fields') + cy.setSettingBasicUserInfo({ + displayName, + country, + description, + }) + cy.get('[data-cy="country:BO"]') + + cy.step('Errors if trying to upload invalid image') + cy.get(`[data-cy=userImage]`) + .find(':file') + .attachFile(`images/file.random`) + cy.get('[data-cy=ImageUploadError]').should('be.visible') + cy.get('[data-cy=ImageUploadError-Button]').click() + + cy.step('Can add avatar') + cy.setSettingImage(userImage, 'userImage') + + cy.step("Can't add cover image") + cy.get('[data-cy=coverImages]').should('not.exist') + + cy.setSettingAddContactLink({ + index: 0, + label: ExternalLinkLabel.SOCIAL_MEDIA, + url: 'http://something.to.delete/', + }) - cy.setSettingAddContactLink({ - index: 1, - label: ExternalLinkLabel.SOCIAL_MEDIA, - url, + cy.setSettingAddContactLink({ + index: 1, + label: ExternalLinkLabel.SOCIAL_MEDIA, + url, + }) + + // Remove first item + cy.get('[data-cy="delete-link-0"]').last().trigger('click') + cy.get('[data-cy="Confirm.modal: Modal"]').should('be.visible') + cy.get('[data-cy="Confirm.modal: Confirm"]').trigger('click') + + cy.saveSettingsForm() + + cy.step('Incomplete profile prompts no longer visible') + cy.get('[data-cy=incompleteProfileBanner]').should('not.exist') + cy.get('[data-cy=CompleteProfileHeader]').should('not.exist') + cy.get('[data-cy=emailNotVerifiedBanner]').should('be.visible') + + cy.step('User image shown in header') + cy.get('[data-cy="header-avatar"]') + .should('have.attr', 'src') + .and('include', userImage) + + cy.step('Updated settings display on profile') + cy.visit(`u/${user.username}`) + cy.contains(user.username) + cy.contains(displayName) + cy.contains(description) + cy.contains(country) + cy.get('[data-cy="country:bo"]') + cy.get(`[data-cy="MemberBadge-${profileType}"]`) + cy.get('[data-cy="profile-avatar"]') + .should('have.attr', 'src') + .and('include', userImage) + cy.get('[data-cy="profile-link"]').should('have.attr', 'href', url) + + cy.step('Can add map pin') + cy.get('[data-cy=EditYourProfile]').click({ force: true }) + cy.get('[data-cy="tab-Map"]').click() + cy.get('[data-cy=descriptionMember').should('be.visible') + cy.contains('No map pin currently saved') + cy.fillSettingMapPin(mapDetails(mapPinDescription)) + cy.get('[data-cy=save-map-pin]').click() + cy.contains('Map pin saved successfully') + cy.contains('Your current map pin is here:') + cy.contains(locationStub.country) + + cy.step('Setting map pin makes location field disappear') + cy.get('[data-cy="tab-Profile"]').click() + cy.get('[data-cy=location-dropdown]').should('not.exist') + + cy.step('Can delete map pin') + cy.get('[data-cy="tab-Map"]').click() + cy.get('[data-cy=remove-map-pin]').click() + cy.get('[data-cy="Confirm.modal: Confirm"]').click() + cy.contains('No map pin currently saved') + cy.get('[data-cy="tab-Profile"]').click() + cy.get('[data-cy=location-dropdown]').should('be.visible') + + cy.step('Can update email notification preference') + cy.get('[data-cy="tab-Notifications"]').click() + cy.get('.data-cy__single-value').last().should('have.text', 'Weekly') + cy.selectTag('Daily', '[data-cy=NotificationSettingsSelect]') + cy.get('[data-cy=save-notification-settings]').click() + cy.contains('Notification setting saved successfully') + cy.get('.data-cy__single-value').last().should('have.text', 'Daily') }) - - // Remove first item - cy.get('[data-cy="delete-link-0"]').last().trigger('click') - cy.get('[data-cy="Confirm.modal: Modal"]').should('be.visible') - cy.get('[data-cy="Confirm.modal: Confirm"]').trigger('click') - - cy.saveSettingsForm() - - cy.step('Incomplete profile prompts no longer visible') - cy.get('[data-cy=incompleteProfileBanner]').should('not.exist') - cy.get('[data-cy=CompleteProfileHeader]').should('not.exist') - cy.get('[data-cy=emailNotVerifiedBanner]').should('be.visible') - - cy.step('User image shown in header') - cy.get('[data-cy="header-avatar"]') - .should('have.attr', 'src') - .and('include', userImage) - - cy.step('Updated settings display on profile') - cy.visit(`u/${user.username}`) - cy.contains(user.username) - cy.contains(displayName) - cy.contains(description) - cy.contains(country) - cy.get('[data-cy="country:bo"]') - cy.get(`[data-cy="MemberBadge-${profileType}"]`) - cy.get('[data-cy="profile-avatar"]') - .should('have.attr', 'src') - .and('include', userImage) - cy.get('[data-cy="profile-link"]').should('have.attr', 'href', url) - - cy.step('Can add map pin') - cy.get('[data-cy=EditYourProfile]').click({ force: true }) - cy.get('[data-cy="tab-Map"]').click() - cy.get('[data-cy=descriptionMember').should('be.visible') - cy.contains('No map pin currently saved') - cy.fillSettingMapPin(mapDetails(mapPinDescription)) - cy.get('[data-cy=save-map-pin]').click() - cy.contains('Map pin saved successfully') - cy.contains('Your current map pin is here:') - cy.contains(locationStub.country) - - cy.step('Setting map pin makes location field disappear') - cy.get('[data-cy="tab-Profile"]').click() - cy.get('[data-cy=location-dropdown]').should('not.exist') - - cy.step('Can delete map pin') - cy.get('[data-cy="tab-Map"]').click() - cy.get('[data-cy=remove-map-pin]').click() - cy.get('[data-cy="Confirm.modal: Confirm"]').click() - cy.contains('No map pin currently saved') - cy.get('[data-cy="tab-Profile"]').click() - cy.get('[data-cy=location-dropdown]').should('be.visible') - - cy.step('Can update email notification preference') - cy.get('[data-cy="tab-Notifications"]').click() - cy.get('.data-cy__single-value').last().should('have.text', 'Weekly') - cy.selectTag('Daily', '[data-cy=NotificationSettingsSelect]') - cy.get('[data-cy=save-notification-settings]').click() - cy.contains('Notification setting saved successfully') - cy.get('.data-cy__single-value').last().should('have.text', 'Daily') }) - }) - - describe('[Focus Workplace]', () => { - it('[Editing a new Profile]', () => { - const coverImage = 'profile-cover-1-edited' - const userImage = 'avatar' - const displayName = 'settings_workplace_new' - const description = 'We have some space to run a workplace' - const profileType = 'workspace' - const user = generateNewUserDetails() - const url = 'something@test.com' - const impactFields = [ - { name: 'plastic', value: 5 }, - { name: 'revenue', value: 10003 }, - { name: 'employees', value: 7 }, - { name: 'volunteers', value: 28 }, - { name: 'machines', value: 2, visible: false }, - ] - - cy.signUpNewUser(user) - - cy.step('Go to User Settings') - cy.visit('/settings') - cy.setSettingFocus(profileType) - - cy.step("Can't save without required fields being populated") - cy.get('[data-cy=save]').click() - cy.get('[data-cy=errors-container]').should('be.visible') - - cy.step('Populate profile') - cy.get('[data-cy=shredder').click() - cy.setSettingBasicUserInfo({ - displayName, - description, - }) - cy.step('Can add avatar and cover image') - cy.setSettingImage(userImage, 'userImage') - cy.setSettingImage(coverImage, 'coverImages-0') - - cy.setSettingAddContactLink({ - index: 0, - label: ExternalLinkLabel.EMAIL, - url, - }) - cy.saveSettingsForm() - - cy.step('Updated settings display on profile') - cy.visit(`u/${user.username}`) - cy.contains(user.username) - cy.contains(displayName) - cy.contains(description) - cy.get('[data-cy="ImpactTab"]').should('not.exist') - cy.get(`[data-cy="MemberBadge-${profileType}"]`) - cy.get('[data-cy="userImage"]') - .should('have.attr', 'src') - .and('include', userImage) - cy.get('[data-cy="active-image"]') - .should('have.attr', 'src') - .and('include', coverImage) - - cy.step('Updated settings display on contact tab') - cy.get('[data-cy="contact-tab"]').click() - cy.contains(`Send a message to ${displayName}`) - cy.get('[data-cy="profile-link"]').should( - 'have.attr', - 'href', - `mailto:${url}`, - ) - - cy.step('Set and display impact data') + it('[Space]', () => { + cy.signUpNewUser() cy.visit('/settings') - cy.setSettingImpactData(2022, impactFields) - cy.visit(`u/${user.username}`) - cy.get('[data-cy="ImpactTab"]').click() - - // From visibleImpactFields above - cy.contains('5 Kg of plastic recycled') - cy.contains('USD 10,003 revenue') - cy.contains('7 full time employees') - cy.contains('28 volunteers') + cy.setSettingFocus('space') }) }) - describe('[Focus Machine Builder]', () => { - it('[Edit a new profile]', () => { - const coverImage = 'profile-cover-2-edited' - const description = "We're mechanics and our jobs are making machines" - const displayName = 'machine_builder_pro' - const machineBuilderXp = ['electronics', 'welding'] - const mapPinDescription = 'Informative workshop on machines every week' - const profileType = 'machine-builder' - const user = generateNewUserDetails() - const url = 'https://shop.com/' - - cy.signUpNewUser(user) - - cy.step('Go to User Settings') - cy.visit('/settings') - cy.setSettingFocus(profileType) - cy.get('[data-cy=CompleteProfileHeader]').should('be.visible') - - cy.step("Can't save without required fields being populated") - cy.get('[data-cy=save]').click() - cy.get('[data-cy=errors-container]').should('be.visible') - - cy.step('Populate profile') - cy.setSettingBasicUserInfo({ - displayName, - description, - }) - cy.setSettingImage(coverImage, 'coverImages-0') - - cy.step('Choose Expertise') - cy.get(`[data-cy=${machineBuilderXp[0]}]`).click() - cy.get(`[data-cy=${machineBuilderXp[1]}]`).click() - - cy.setSettingAddContactLink({ - index: 0, - label: ExternalLinkLabel.BAZAR, - url, - }) - - cy.setSettingPublicContact() - cy.saveSettingsForm() - cy.get('[data-cy=CompleteProfileHeader]').should('not.exist') - - cy.step('Updated settings display on main profile tab') - cy.visit(`u/${user.username}`) - cy.contains(user.username) - cy.contains(displayName) - cy.contains(description) - cy.get(`[data-cy=MemberBadge-${profileType}]`) - cy.get('[data-cy="active-image"]') - .should('have.attr', 'src') - .and('include', coverImage) - - cy.step('Updated settings display on contact tab') - cy.get('[data-cy="contact-tab"]').click() - cy.contains(`Send a message to ${displayName}`).should('not.exist') - cy.get('[data-cy="profile-link"]').should('have.attr', 'href', url) - - cy.step('Can add map pin') - cy.get('[data-cy=EditYourProfile]').click() - cy.get('[data-cy="link-to-map-setting"]').click() - cy.get('[data-cy=descriptionSpace').should('be.visible') - cy.get('[data-cy=WorkspaceMapPinRequiredStars').should('be.visible') - cy.contains('No map pin currently saved') - cy.fillSettingMapPin(mapDetails(mapPinDescription)) - cy.get('[data-cy=save-map-pin]').click() - cy.contains('Map pin saved successfully') - cy.contains('Your current map pin is here:') - cy.contains(locationStub.country) + describe('[Precious Plastic]', () => { + beforeEach(() => { + localStorage.setItem( + 'VITE_PLATFORM_PROFILES', + 'member,workspace,community-builder,collection-point,machine-builder', + ) + setIsPreciousPlastic() + cy.interceptAddressSearchFetch(SingaporeStubResponse) + cy.visit('/sign-in') }) - }) - describe('[Focus Community Builder]', () => { - it('[Edit a new profile]', () => { - const coverImage = 'profile-cover-1-edited' - const description = - 'An enthusiastic community that makes the world greener!' - const displayName = 'community_001' - const profileType = 'community-builder' - const user = generateNewUserDetails() - const url = 'http://www.settings_community_new-forum.org' - - cy.signUpNewUser(user) - - cy.step('Go to User Settings') - cy.visit('/settings') - cy.setSettingFocus(profileType) - - cy.setSettingBasicUserInfo({ - displayName, - description, + describe('[Focus Workplace]', () => { + it('[Editing a new Profile]', () => { + const coverImage = 'profile-cover-1-edited' + const userImage = 'avatar' + const displayName = 'settings_workplace_new' + const description = 'We have some space to run a workplace' + const profileType = 'workspace' + const user = generateNewUserDetails() + const url = 'something@test.com' + const impactFields = [ + { name: 'plastic', value: 5 }, + { name: 'revenue', value: 10003 }, + { name: 'employees', value: 7 }, + { name: 'volunteers', value: 28 }, + { name: 'machines', value: 2, visible: false }, + ] + + cy.signUpNewUser(user) + + cy.step('Go to User Settings') + cy.visit('/settings') + cy.setSettingFocus(profileType) + + cy.step("Can't save without required fields being populated") + cy.get('[data-cy=save]').click() + cy.get('[data-cy=errors-container]').should('be.visible') + + cy.step('Populate profile') + cy.get('[data-cy=shredder').click() + cy.setSettingBasicUserInfo({ + displayName, + description, + }) + + cy.step('Can add avatar and cover image') + cy.setSettingImage(userImage, 'userImage') + cy.setSettingImage(coverImage, 'coverImages-0') + + cy.setSettingAddContactLink({ + index: 0, + label: ExternalLinkLabel.EMAIL, + url, + }) + cy.saveSettingsForm() + + cy.step('Updated settings display on profile') + cy.visit(`u/${user.username}`) + cy.contains(user.username) + cy.contains(displayName) + cy.contains(description) + cy.get('[data-cy="ImpactTab"]').should('not.exist') + cy.get(`[data-cy="MemberBadge-${profileType}"]`) + cy.get('[data-cy="userImage"]') + .should('have.attr', 'src') + .and('include', userImage) + cy.get('[data-cy="active-image"]') + .should('have.attr', 'src') + .and('include', coverImage) + + cy.step('Updated settings display on contact tab') + cy.get('[data-cy="contact-tab"]').click() + cy.contains(`Send a message to ${displayName}`) + cy.get('[data-cy="profile-link"]').should( + 'have.attr', + 'href', + `mailto:${url}`, + ) + + cy.step('Set and display impact data') + cy.visit('/settings') + cy.setSettingImpactData(2022, impactFields) + cy.visit(`u/${user.username}`) + cy.get('[data-cy="ImpactTab"]').click() + + // From visibleImpactFields above + cy.contains('5 Kg of plastic recycled') + cy.contains('USD 10,003 revenue') + cy.contains('7 full time employees') + cy.contains('28 volunteers') }) - cy.setSettingImage(coverImage, 'coverImages-0') + }) - cy.setSettingAddContactLink({ - index: 0, - label: ExternalLinkLabel.SOCIAL_MEDIA, - url, + describe('[Focus Machine Builder]', () => { + it('[Edit a new profile]', () => { + const coverImage = 'profile-cover-2-edited' + const description = "We're mechanics and our jobs are making machines" + const displayName = 'machine_builder_pro' + const machineBuilderXp = ['Electronics', 'Welding'] + const mapPinDescription = 'Informative workshop on machines every week' + const profileType = 'machine-builder' + const user = generateNewUserDetails() + const url = 'https://shop.com/' + + cy.signUpNewUser(user) + + cy.step('Go to User Settings') + cy.visit('/settings') + cy.setSettingFocus(profileType) + cy.get('[data-cy=CompleteProfileHeader]').should('be.visible') + + cy.step("Can't save without required fields being populated") + cy.get('[data-cy=save]').click() + cy.get('[data-cy=errors-container]').should('be.visible') + + cy.step('Set profile tags') + cy.selectTag(machineBuilderXp[0], '[data-cy=tag-select]') + cy.selectTag(machineBuilderXp[1], '[data-cy=tag-select]') + + cy.step('Populate profile') + cy.setSettingBasicUserInfo({ + displayName, + description, + }) + cy.setSettingImage(coverImage, 'coverImages-0') + + cy.setSettingAddContactLink({ + index: 0, + label: ExternalLinkLabel.BAZAR, + url, + }) + + cy.setSettingPublicContact() + cy.saveSettingsForm() + cy.get('[data-cy=CompleteProfileHeader]').should('not.exist') + + cy.step('Updated settings display on main profile tab') + cy.visit(`u/${user.username}`) + cy.contains(user.username) + cy.contains(displayName) + cy.contains(description) + cy.contains(machineBuilderXp[0]) + cy.contains(machineBuilderXp[1]) + cy.get(`[data-cy=MemberBadge-${profileType}]`) + cy.get('[data-cy="active-image"]') + .should('have.attr', 'src') + .and('include', coverImage) + + cy.step('Updated settings display on contact tab') + cy.get('[data-cy="contact-tab"]').click() + cy.contains(`Send a message to ${displayName}`).should('not.exist') + cy.get('[data-cy="profile-link"]').should('have.attr', 'href', url) + + cy.step('Can add map pin') + cy.get('[data-cy=EditYourProfile]').click() + cy.get('[data-cy="link-to-map-setting"]').click() + cy.get('[data-cy=descriptionSpace').should('be.visible') + cy.get('[data-cy=WorkspaceMapPinRequiredStars').should('be.visible') + cy.contains('No map pin currently saved') + cy.fillSettingMapPin(mapDetails(mapPinDescription)) + cy.get('[data-cy=save-map-pin]').click() + cy.contains('Map pin saved successfully') + cy.contains('Your current map pin is here:') + cy.contains(locationStub.country) }) - - cy.saveSettingsForm() - - cy.step('Updated settings display on main profile tab') - cy.visit(`u/${user.username}`) - cy.contains(user.username) - cy.contains(displayName) - cy.contains(description) - cy.get(`[data-cy=MemberBadge-${profileType}]`) - cy.get('[data-cy="active-image"]') - .should('have.attr', 'src') - .and('include', coverImage) - - cy.step('Updated settings display on contact tab') - cy.get('[data-cy="contact-tab"]').click() - cy.contains(`Send a message to ${displayName}`) - cy.get('[data-cy="profile-link"]').should('have.attr', 'href', url) }) - }) - describe('Focus Plastic Collection Point', () => { - it('[Edit a new profile]', () => { - const coverImage = 'profile-cover-1' - const description = - 'We accept plastic currencies: Bottle, Nylon Bags, Plastic Lids/Straws' - const displayName = 'settings_community_new' - const openTimes = [ - { + describe('[Focus Community Builder]', () => { + it('[Edit a new profile]', () => { + const coverImage = 'profile-cover-1-edited' + const description = + 'An enthusiastic community that makes the world greener!' + const displayName = 'community_001' + const profileType = 'community-builder' + const user = generateNewUserDetails() + const url = 'http://www.settings_community_new-forum.org' + + cy.signUpNewUser(user) + + cy.step('Go to User Settings') + cy.visit('/settings') + cy.setSettingFocus(profileType) + + cy.setSettingBasicUserInfo({ + displayName, + description, + }) + cy.setSettingImage(coverImage, 'coverImages-0') + + cy.setSettingAddContactLink({ index: 0, - day: 'Monday', - from: '09:00 AM', - to: '06:00 PM', - }, - { - index: 1, - day: 'Tuesday', - from: '09:00 AM', - to: '06:00 PM', - }, - { - index: 2, - day: 'Wednesday', - from: '10:00 AM', - to: '08:00 PM', - }, - { - index: 3, - day: 'Friday', - from: '05:00 AM', - to: '02:00 PM', - }, - ] - const plasticTypes = ['hdpe', 'other'] - const profileType = 'collection-point' - const user = generateNewUserDetails() - const url = 'http://www.facebook.com/settings_plastic_new' - - cy.signUpNewUser(user) - - cy.step('Go to User Settings') - cy.visit('/settings') - cy.setSettingFocus(profileType) - - cy.step("Can't save without required fields being populated") - cy.get('[data-cy=save]').click() - cy.get('[data-cy=errors-container]').should('be.visible') - - cy.step('Populate profile') - cy.setSettingBasicUserInfo({ - displayName, - description, + label: ExternalLinkLabel.SOCIAL_MEDIA, + url, + }) + + cy.saveSettingsForm() + + cy.step('Updated settings display on main profile tab') + cy.visit(`u/${user.username}`) + cy.contains(user.username) + cy.contains(displayName) + cy.contains(description) + cy.get(`[data-cy=MemberBadge-${profileType}]`) + cy.get('[data-cy="active-image"]') + .should('have.attr', 'src') + .and('include', coverImage) + + cy.step('Updated settings display on contact tab') + cy.get('[data-cy="contact-tab"]').click() + cy.contains(`Send a message to ${displayName}`) + cy.get('[data-cy="profile-link"]').should('have.attr', 'href', url) }) - cy.setSettingImage(coverImage, 'coverImages-0') + }) - cy.setSettingAddContactLink({ - index: 0, - label: ExternalLinkLabel.SOCIAL_MEDIA, - url, + describe('Focus Plastic Collection Point', () => { + it('[Edit a new profile]', () => { + const coverImage = 'profile-cover-1' + const description = + 'We accept plastic currencies: Bottle, Nylon Bags, Plastic Lids/Straws' + const displayName = 'settings_community_new' + const plasticTypes = ['HDPE', 'LDPE'] + const profileType = 'collection-point' + const user = generateNewUserDetails() + const url = 'http://www.facebook.com/settings_plastic_new' + + cy.signUpNewUser(user) + + cy.step('Go to User Settings') + cy.visit('/settings') + cy.setSettingFocus(profileType) + + cy.step("Can't save without required fields being populated") + cy.get('[data-cy=save]').click() + cy.get('[data-cy=errors-container]').should('be.visible') + + cy.step('Set profile tags') + cy.selectTag(plasticTypes[0], '[data-cy=tag-select]') + cy.selectTag(plasticTypes[1], '[data-cy=tag-select]') + + cy.step('Populate profile') + cy.setSettingBasicUserInfo({ + displayName, + description, + }) + cy.setSettingImage(coverImage, 'coverImages-0') + + cy.setSettingAddContactLink({ + index: 0, + label: ExternalLinkLabel.SOCIAL_MEDIA, + url, + }) + + cy.saveSettingsForm() + + cy.step('Updated settings display on main profile tab') + cy.visit(`u/${user.username}`) + cy.contains(user.username) + cy.contains(displayName) + cy.contains(description) + cy.get(`[data-cy=MemberBadge-${profileType}]`) + cy.get('[data-cy="active-image"]') + .should('have.attr', 'src') + .and('include', coverImage) + cy.contains(plasticTypes[0]) + cy.contains(plasticTypes[1]) + + cy.step('Updated settings display on contact tab') + cy.get('[data-cy="contact-tab"]').click() + cy.get('[data-cy="profile-link"]').should('have.attr', 'href', url) + cy.contains(`Send a message to ${displayName}`) }) + }) + }) - cy.step('Update collection specific section') - openTimes.forEach((openTime) => { - cy.setSettingAddOpeningTime(openTime) - }) - cy.setSettingDeleteOpeningTime(1, false) - cy.setSettingDeleteOpeningTime(2, true) - - cy.get(`[data-cy=plastic-${plasticTypes[0]}]`).click() - cy.get(`[data-cy=plastic-${plasticTypes[1]}]`).click() - - cy.saveSettingsForm() - - cy.step('Updated settings display on main profile tab') - cy.visit(`u/${user.username}`) - cy.contains(user.username) - cy.contains(displayName) - cy.contains(description) - cy.get(`[data-cy=MemberBadge-${profileType}]`) - cy.get('[data-cy="active-image"]') - .should('have.attr', 'src') - .and('include', coverImage) - - cy.step('Updated collection specific section displayed') - cy.get(`[data-cy=plastic-type-${plasticTypes[0]}]`) - cy.get(`[data-cy=plastic-type-${plasticTypes[1]}]`) - cy.contains( - `${openTimes[0].day}: ${openTimes[0].from} - ${openTimes[0].to}`, - ) - cy.contains( - `${openTimes[1].day}: ${openTimes[1].from} - ${openTimes[1].to}`, - ) - cy.contains( - `${openTimes[3].day}: ${openTimes[3].from} - ${openTimes[3].to}`, - ) + describe('[Project Kamp]', () => { + beforeEach(() => { + localStorage.setItem('VITE_PLATFORM_PROFILES', 'member') + cy.visit('/sign-in') + }) - cy.step('Updated settings display on contact tab') - cy.get('[data-cy="contact-tab"]').click() - cy.get('[data-cy="profile-link"]').should('have.attr', 'href', url) - cy.contains(`Send a message to ${displayName}`) + it('[Member]', () => { + cy.signUpNewUser() + cy.visit('/settings') + cy.contains('Infos') + cy.get('[data-cy=FocusSection]').should('not.exist') }) }) }) diff --git a/packages/cypress/src/support/CustomAssertations.ts b/packages/cypress/src/support/CustomAssertations.ts index 833c221c97..c6f004905a 100644 --- a/packages/cypress/src/support/CustomAssertations.ts +++ b/packages/cypress/src/support/CustomAssertations.ts @@ -1,12 +1,12 @@ import chaiSubset from 'chai-subset' -import type { ProfileTypeName } from 'oa-shared' import type { IHowto, IHowtoStep, IResearchDB, IUserDB, -} from '../../../../src/models' + ProfileTypeName, +} from 'oa-shared' declare global { namespace Chai { @@ -171,19 +171,6 @@ const eqSettings = (chaiObj) => { expect(subject.workspaceType, 'workspaceType').to.containSubset( expected.workspaceType, ) - const machineExpertiseAssert: Assert = (subject, expected) => - expect(subject.machineBuilderXp, 'MachineBuilderXp').to.containSubset( - expected.machineBuilderXp, - ) - const openingHoursAssert: Assert = (subject, expected) => - expect(subject.openingHours, 'OpeningHours').to.containSubset( - expected.openingHours, - ) - const plasticTypeAssert: Assert = (subject, expected) => - expect( - subject.collectedPlasticTypes, - 'CollectedPlasticTypes', - ).to.containSubset(expected.collectedPlasticTypes) const assertMap: { [key in ProfileTypeName]: ChainAssert @@ -205,7 +192,6 @@ const eqSettings = (chaiObj) => { coverImageAssert, linkAssert, locationAssert, - machineExpertiseAssert, ), 'community-builder': new ChainAssert( basicInfoAssert, @@ -218,8 +204,6 @@ const eqSettings = (chaiObj) => { coverImageAssert, linkAssert, locationAssert, - openingHoursAssert, - plasticTypeAssert, ), space: undefined, } diff --git a/packages/cypress/src/support/commands.ts b/packages/cypress/src/support/commands.ts index 0325bcacd1..b488584d03 100644 --- a/packages/cypress/src/support/commands.ts +++ b/packages/cypress/src/support/commands.ts @@ -5,6 +5,8 @@ import { deleteDB } from 'idb' import { Auth, TestDB } from './db/firebase' +import type { IHowtoDB, IQuestionDB, IResearchDB } from 'oa-shared' +import type { IUserSignUpDetails } from '../utils/TestUtils' import type { firebase } from './db/firebase' declare global { @@ -31,6 +33,15 @@ declare global { opStr: any, value: string, ): Chainable + addHowto(howto: IHowtoDB, user: IUserSignUpDetails): Chainable + addQuestion( + question: IQuestionDB, + user: IUserSignUpDetails, + ): Chainable + addResearch( + research: IResearchDB, + user: IUserSignUpDetails, + ): Chainable step(message: string) setSessionStorage(key: string, value: string): Promise } @@ -96,6 +107,7 @@ Cypress.Commands.add('clearServiceWorkers', () => { Auth.signOut().then(() => resolve()) }) }) + cy.wait(2000) cy.wrap(checkUI ? 'check logout ui' : 'skip ui check').then(() => { if (checkUI) { cy.get('[data-cy=login]') @@ -158,6 +170,33 @@ Cypress.Commands.add( }, ) +Cypress.Commands.add('addHowto', (howto, user) => { + const slug = `${howto.slug}-${user.username}` + + return firestore.addDocument('howtos', { + ...howto, + slug, + }) +}) + +Cypress.Commands.add('addQuestion', (question, user) => { + const slug = `${question.slug}-for-${user.username}` + + return firestore.addDocument('questions', { + ...question, + slug, + }) +}) + +Cypress.Commands.add('addResearch', (research, user) => { + const slug = `${user.username}-in-${research.slug}` + + return firestore.addDocument('research', { + ...research, + slug, + }) +}) + Cypress.Commands.add('step', (message: string) => { Cypress.log({ displayName: 'step', diff --git a/packages/cypress/src/support/commandsUi.ts b/packages/cypress/src/support/commandsUi.ts index 15278bff8a..48bc36cadb 100644 --- a/packages/cypress/src/support/commandsUi.ts +++ b/packages/cypress/src/support/commandsUi.ts @@ -1,7 +1,7 @@ import { form } from '../../../../src/pages/UserSettings/labels' import { generateNewUserDetails } from '../utils/TestUtils' -import type { IUser } from '../../../../src/models/user.models' +import type { IUser } from 'oa-shared' export enum UserMenuItem { Profile = 'Profile', @@ -23,22 +23,16 @@ interface IMapPin { locationName: string } -interface IOpeningTime { - index: number - day: string - from: string - to: string -} - declare global { namespace Cypress { interface Chainable { addComment(newComment: string): Chainable addReply(reply: string): Chainable clickMenuItem(menuItem: UserMenuItem): Chainable - deleteDiscussionItem(element: string) + deleteDiscussionItem(element: string, item: string) editDiscussionItem( element: string, + oldComment: string, updatedNewComment: string, ): Chainable fillSignupForm( @@ -63,9 +57,7 @@ declare global { **/ selectTag(tagName: string, selector?: string): Chainable setSettingAddContactLink(link: ILink) - setSettingAddOpeningTime(openingTime: IOpeningTime) setSettingBasicUserInfo(info: IInfo) - setSettingDeleteOpeningTime(index: number, confirmed: boolean) setSettingFocus(focus: string) setSettingImage(image: string, selector: string) setSettingImpactData(year: number, fields) @@ -108,31 +100,6 @@ Cypress.Commands.add('setSettingAddContactLink', (link: ILink) => { .blur({ force: true }) }) -Cypress.Commands.add( - 'setSettingAddOpeningTime', - (openingTime: IOpeningTime) => { - const selectOption = (selector: string, selectedValue: string) => { - cy.selectTag(selectedValue, selector) - } - - if (openingTime.index > 0) { - cy.get('[data-cy=add-opening-time]').click() - } - selectOption( - `[data-cy=opening-time-day-${openingTime.index}]`, - openingTime.day, - ) - selectOption( - `[data-cy=opening-time-from-${openingTime.index}]`, - openingTime.from, - ) - selectOption( - `[data-cy=opening-time-to-${openingTime.index}]`, - openingTime.to, - ) - }, -) - Cypress.Commands.add('setSettingBasicUserInfo', (info: IInfo) => { const { country, description, displayName } = info @@ -142,19 +109,6 @@ Cypress.Commands.add('setSettingBasicUserInfo', (info: IInfo) => { country && cy.selectTag(country, '[data-cy=location-dropdown]') }) -Cypress.Commands.add( - 'setSettingDeleteOpeningTime', - (index: number, confirmed: boolean) => { - cy.viewport('macbook-13') - cy.get(`[data-cy=delete-opening-time-${index}-desk]`).click() - if (confirmed) { - cy.get('[data-cy=confirm-delete]').click() - } else { - cy.get('[data-cy=cancel-delete]').click() - } - }, -) - Cypress.Commands.add('setSettingFocus', (focus: string) => { cy.get(`[data-cy=${focus}]`).click() }) @@ -265,25 +219,36 @@ Cypress.Commands.add( Cypress.Commands.add('addComment', (newComment: string) => { cy.get('[data-cy=comments-form]').last().type(newComment) cy.get('[data-cy=comment-submit]').last().click() + + cy.contains(newComment) + cy.get('[data-cy=OwnCommentItem]').contains('less than a minute ago') }) Cypress.Commands.add( 'editDiscussionItem', - (element: string, updatedNewComment: string) => { + (element, oldComment, updatedNewComment) => { cy.get(`[data-cy="${element}: edit button"]`).last().click() cy.get('[data-cy=edit-comment]').clear().type(updatedNewComment) cy.get('[data-cy=edit-comment-submit]').click() + cy.get('[data-cy=edit-comment]').should('not.exist') + cy.get(`[data-cy=Own${element}]`).contains(updatedNewComment) + cy.get(`[data-cy=Own${element}]`).contains('Edited less than a minute ago') + cy.get(`[data-cy=Own${element}]`).contains(oldComment).should('not.exist') }, ) -Cypress.Commands.add('deleteDiscussionItem', (element: string) => { +Cypress.Commands.add('deleteDiscussionItem', (element, item) => { cy.get(`[data-cy="${element}: delete button"]`).last().click() cy.get('[data-cy="Confirm.modal: Confirm"]').last().click() + + cy.contains(item).should('not.exist') }) Cypress.Commands.add('addReply', (reply: string) => { cy.get('[data-cy=show-replies]').first().click() cy.get('[data-cy=reply-form]').first().type(reply) cy.get('[data-cy=reply-submit]').first().click() + + cy.get('[data-cy=OwnReplyItem]').contains(reply) }) diff --git a/packages/cypress/src/support/db/endpoints.ts b/packages/cypress/src/support/db/endpoints.ts index 51bc432533..7ea72dac51 100644 --- a/packages/cypress/src/support/db/endpoints.ts +++ b/packages/cypress/src/support/db/endpoints.ts @@ -1,21 +1,5 @@ import { generateDBEndpoints } from 'oa-shared' -// React apps populate a process variable, however it might not always be accessible outside -// (e.g. cypress will instead use it's own env to populate a prefix) -const process = globalThis.process || ({} as any) -const e = import.meta.env || process.env || ({} as any) - -/** - * A prefix can be used to simplify large-scale schema changes or multisite hosting - * and allow multiple sites to use one DB (used for parallel test seed DBs) - * e.g. oa_ - * SessionStorage prefixes are used to allow test ci environments to dynamically set a db endpoint - */ -const DB_PREFIX = - (typeof sessionStorage !== 'undefined' && sessionStorage.DB_PREFIX) || - e.VITE_DB_PREFIX || - '' - /** * Mapping of generic database endpoints to specific prefixed and revisioned versions for the * current implementation @@ -24,6 +8,6 @@ const DB_PREFIX = * const allHowtos = await db.get(DB_ENDPOINTS.howtos) * ``` */ -export const DB_ENDPOINTS = generateDBEndpoints(DB_PREFIX) +export const DB_ENDPOINTS = generateDBEndpoints() export type DBEndpoint = keyof typeof DB_ENDPOINTS diff --git a/packages/cypress/src/support/db/firebase.ts b/packages/cypress/src/support/db/firebase.ts index 481e914faa..1711a49689 100644 --- a/packages/cypress/src/support/db/firebase.ts +++ b/packages/cypress/src/support/db/firebase.ts @@ -10,7 +10,7 @@ import 'firebase/compat/database' import { indexedDBLocalPersistence, initializeAuth } from 'firebase/auth' import { DB_ENDPOINTS } from 'oa-shared/models' -import { MOCK_DATA } from '../../data/index' +// import { MOCK_DATA } from '../../data' const fbConfig = { apiKey: 'AIzaSyDAxS_7M780mI3_tlwnAvpbaqRsQPlmp64', @@ -25,32 +25,13 @@ const db = firebase.firestore() // db.useEmulator('localhost', 8080) class FirestoreTestDB { - seedDB = async () => { - const endpoints = ensureDBPrefixes(DB_ENDPOINTS) - const dbWrites = Object.keys(MOCK_DATA).map(async (key) => { - const endpoint = endpoints[key] - await this.addDocuments(endpoint, Object.values(MOCK_DATA[key])) - return [endpoint, MOCK_DATA[key]] - }) - return Promise.all(dbWrites) - } - - clearDB = async () => { - const endpoints = ensureDBPrefixes(DB_ENDPOINTS) - const dbDeletes = Object.values(endpoints).map((endpoint) => { - return this.deleteAll(endpoint) - }) - return Promise.all(dbDeletes) - } - queryDocuments = ( collectionName: string, fieldPath: string, opStr: any, value: string, ): Cypress.Chainable => { - const endpoints = ensureDBPrefixes(DB_ENDPOINTS) - const endpoint = endpoints[collectionName] + const endpoint = DB_ENDPOINTS[collectionName] return cy .wrap(`query: ${endpoint} WHERE ${fieldPath}${opStr}${value}`) .then(() => { @@ -66,43 +47,22 @@ class FirestoreTestDB { }) } - private addDocuments = async (collectionName: string, docs: any[]) => { - cy.log(`DB Seed: ${collectionName}`) - const batch = db.batch() - const col = db.collection(collectionName) - docs.forEach((doc) => { - const ref = col.doc(doc._id) - batch.set(ref, doc) - }) - return batch.commit() - } - private deleteAll = async (collectionName: string) => { - cy.log(`DB Delete: ${collectionName}`) - const batch = db.batch() - const col = db.collection(collectionName) - const docs = (await col.get()) || [] - docs.forEach((d) => { - batch.delete(col.doc(d.id)) + addDocument = (collectionName: string, data: any): Cypress.Chainable => { + const endpoint = DB_ENDPOINTS[collectionName] + return cy.wrap(`adding to: ${collectionName} WITH ${data}`).then(() => { + return new Cypress.Promise((resolve, reject) => { + db.collection(`${endpoint}`) + .add(data) + .then((response) => { + resolve(response) + }) + .catch((err) => reject(err)) + }) }) - return batch.commit() } } + export const Auth = initializeAuth(firebase.app(), { persistence: indexedDBLocalPersistence, }) export const TestDB = new FirestoreTestDB() - -/** - * During initialisation the endpoints imported from endpoints.ts might be populated before the - * prefix is stored in localstorage. This function ensures they start with the correct prefix - */ -function ensureDBPrefixes(endpoints: { [key: string]: string }) { - const prefix = Cypress.env('DB_PREFIX') - Object.entries(endpoints).forEach(([key, value]) => { - if (!value.startsWith(prefix)) { - // The regex here is intended to remove collectionPrefix - endpoints[key] = `${prefix}${value.replace(/^[A-Za-z0-9]{5}_/, '')}` - } - }) - return endpoints -} diff --git a/packages/cypress/src/support/hooks.ts b/packages/cypress/src/support/hooks.ts index 08ff4bed2a..754808e84d 100644 --- a/packages/cypress/src/support/hooks.ts +++ b/packages/cypress/src/support/hooks.ts @@ -1,6 +1,3 @@ -import { generateAlphaNumeric } from '../utils/TestUtils' -import { TestDB } from './db/firebase' - /** * Before all tests begin seed the database. CY runs this before all specs. * Note, cy also automatically will clear browser caches. @@ -11,10 +8,6 @@ import { TestDB } from './db/firebase' * put aliases created in beforeAll will be (not currently required) */ before(() => { - if (!Cypress.env('DB_PREFIX')) { - Cypress.env('DB_PREFIX', `${generateAlphaNumeric(5)}_`) - } - // Add error handlers // https://docs.cypress.io/api/utilities/promise.html#Rejected-test-promises-do-not-fail-tests window.addEventListener('unhandledrejection', (event) => { @@ -24,59 +17,10 @@ before(() => { throw error }) cy.clearServiceWorkers() - // clear idb cy.deleteIDB('OneArmyCache') - // cy.deleteIDB('firebaseLocalStorageDb') - // seed db (ensure db_prefix available for seed) - cy.setSessionStorage('DB_PREFIX', Cypress.env('DB_PREFIX')) - cy.wrap('DB Init').then({ timeout: 120000 }, () => { - // large initial timeout in case server slow to respond - return new Cypress.Promise((resolve, reject) => { - // force resolve in case of server issues (sometimes a bit flaky) - setTimeout(() => { - resolve() - }, 10000) - // seed the database - TestDB.seedDB().then(resolve).catch(reject) - }) - }) - // the seeddb function returns an array of [db_key, db_data] entries - // ensure each db_key contains the correct db prefix and is not empty - // .each(data => { - // cy.wrap(data).should(entry => { - // expect(entry[0]).contains(Cypress.env('DB_PREFIX')) - // expect(entry[1]).length.greaterThan(0) - // }) - // }) -}) - -beforeEach(() => { - // set the db_prefix variable on platform session storage (cypress wipes between tests) - cy.setSessionStorage('DB_PREFIX', Cypress.env('DB_PREFIX')) }) afterEach(() => { // ensure all tests are also logged out (skip ui check in case page not loaded) cy.logout(false) }) - -/** - * After all tests have completed delete all the documents that have - * been added to the database - */ -after(() => { - cy.wrap('Clear DB').then({ timeout: 120000 }, () => { - return new Cypress.Promise((resolve, reject) => { - // force resolve in case of server issues (sometimes a bit flaky) - setTimeout(() => { - resolve() - }, 10000) - // clear the database - TestDB.clearDB().then( - () => resolve(), - (err) => reject(err), - ) - }) - }) - // remove service workers at end of test set -}) diff --git a/packages/cypress/src/support/rules.ts b/packages/cypress/src/support/rules.ts index 4b088626e2..1110f1027e 100644 --- a/packages/cypress/src/support/rules.ts +++ b/packages/cypress/src/support/rules.ts @@ -4,9 +4,12 @@ Cypress.on('uncaught:exception', (err) => { 'No document to update', 'KeyPath previousSlugs', 'KeyPath slug', - // 'There was an error while hydrating', - // 'Hydration failed because the initial UI does not match what was rendered on the server', - // 'An error occurred during hydration.', + 'Hydration', + 'hydration', + 'There was an error while hydrating', + 'Hydration failed because the initial UI does not match what was rendered on the server', + 'An error occurred during hydration.', + 'Minified React', ] const foundSkipError = skipErrors.find((error) => err.message.includes(error)) diff --git a/packages/cypress/src/utils/TestUtils.ts b/packages/cypress/src/utils/TestUtils.ts index 884f508c19..c293e15312 100644 --- a/packages/cypress/src/utils/TestUtils.ts +++ b/packages/cypress/src/utils/TestUtils.ts @@ -1,3 +1,9 @@ +export interface IUserSignUpDetails { + username: string + email: string + password: string +} + export enum Page { HOWTO = '/how-to', ACADEMY = '/academy', @@ -20,7 +26,7 @@ export enum DbCollectionName { howtos = 'howtos', } -export const generateNewUserDetails = () => { +export const generateNewUserDetails = (): IUserSignUpDetails => { const username = `CI_${generateAlphaNumeric(7)}`.toLocaleLowerCase() return { username, diff --git a/packages/themes/src/index.ts b/packages/themes/src/index.ts index 4a028d7bc4..fcd82cede8 100644 --- a/packages/themes/src/index.ts +++ b/packages/themes/src/index.ts @@ -1,8 +1,14 @@ +import { commonStyles } from './common/commonStyles' import { Theme as fixingFashionTheme } from './fixing-fashion' import { Theme as preciousPlasticTheme } from './precious-plastic' import { Theme as projectKampTheme } from './project-kamp' -export { preciousPlasticTheme, projectKampTheme, fixingFashionTheme } +export { + commonStyles, + preciousPlasticTheme, + projectKampTheme, + fixingFashionTheme, +} export { GlobalFonts } from './fonts' diff --git a/packages/themes/src/precious-plastic/styles.ts b/packages/themes/src/precious-plastic/styles.ts index df4d82a32c..a10b1ef229 100644 --- a/packages/themes/src/precious-plastic/styles.ts +++ b/packages/themes/src/precious-plastic/styles.ts @@ -58,6 +58,10 @@ export const styles: ThemeWithName = { lowDetail: MachineBadgeLowDetail, normal: MachineBadge, }, + space: { + lowDetail: LocalComBadgeLowDetail, + normal: LocalComBadge, + }, }, buttons: getButtons(colors), colors, diff --git a/shared/data/index.ts b/shared/data/index.ts new file mode 100644 index 0000000000..1709bdc580 --- /dev/null +++ b/shared/data/index.ts @@ -0,0 +1 @@ +export * from './profileTags' diff --git a/shared/data/profileTags.ts b/shared/data/profileTags.ts new file mode 100644 index 0000000000..2fd0afeee0 --- /dev/null +++ b/shared/data/profileTags.ts @@ -0,0 +1,76 @@ +import type { ITag } from '../models/tags' + +export const profileTags: ITag[] = [ + { + _id: 'uCzWZbz3aVKyx2keoqRi', + _created: '2018-01-01T00:00:00.001Z', + _deleted: false, + label: 'Electronics', + }, + { + _id: 'J3LF7fMsDfniYT2ZX3rf', + _created: '2018-01-01T00:00:00.001Z', + _deleted: false, + label: 'Machining', + }, + { + _id: 'QvxszeiUqy867CaVc7Kh', + _created: '2018-01-01T00:00:00.001Z', + _deleted: false, + label: 'Welding', + }, + { + _id: '6h3fWCv3AXGJ4bVr3Foc', + _created: '2018-01-01T00:00:00.001Z', + _deleted: false, + label: 'Assembling', + }, + { + _id: 'FhtRoqZvQYYaN6CN2txJ', + _created: '2018-01-01T00:00:00.001Z', + _deleted: false, + label: 'Mould Making', + }, + { + _id: '4ax6TzzVsAtG6Au8nEXJ', + _created: '2018-01-02T00:00:00.001Z', + _deleted: false, + label: 'PET', + }, + { + _id: 'farYGhuqJc6wrAwa2xyx', + _created: '2018-01-02T00:00:00.001Z', + _deleted: false, + label: 'HDPE', + }, + { + _id: 'XjBLmxaYi2Hu3H2n4QBw', + _created: '2018-01-02T00:00:00.001Z', + _deleted: false, + label: 'PVC', + }, + { + _id: 'HDNJjX4ohKFfM7YznEpL', + _created: '2018-01-02T00:00:00.001Z', + _deleted: false, + label: 'LDPE', + }, + { + _id: 'P7pk8KAQwzqHLqhGUvoy', + _created: '2018-01-02T00:00:00.001Z', + _deleted: false, + label: 'PP', + }, + { + _id: '3PLMJ7mXxZFRqrTLcM2h', + _created: '2018-01-02T00:00:00.001Z', + _deleted: false, + label: 'PS', + }, + { + _id: '3E6Eyxf7EYmPLXeF8nd3', + _created: '2018-01-02T00:00:00.001Z', + _deleted: false, + label: 'Other Plastics', + }, +] diff --git a/shared/index.ts b/shared/index.ts index a38d7b036e..e5b6b09485 100644 --- a/shared/index.ts +++ b/shared/index.ts @@ -1,3 +1,4 @@ +export * from './data' export * from './models' export * from './messages' export * from './mocks' diff --git a/shared/mocks/data/users.ts b/shared/mocks/data/users.ts index d04366cad0..7660b8e676 100644 --- a/shared/mocks/data/users.ts +++ b/shared/mocks/data/users.ts @@ -191,15 +191,11 @@ export const users = { _id: 'settings_community_new', profileType: null, coverImages: [], - isExpert: null, - collectedPlasticTypes: null, - openingHours: [], location: null, verified: true, _modified: '2020-01-07T12:15:08.726Z', _created: '2020-01-07T12:15:08.726Z', displayName: 'settings_community_new', - isV4Member: null, _deleted: false, workspaceType: null, country: null, @@ -211,14 +207,11 @@ export const users = { about: null, }, settings_machine_new: { - collectedPlasticTypes: null, location: null, - openingHours: [], verified: true, _modified: '2020-01-07T12:14:50.354Z', _created: '2020-01-07T12:14:50.354Z', displayName: 'settings_machine_new', - isV4Member: null, _deleted: false, workspaceType: null, country: null, @@ -231,10 +224,8 @@ export const users = { _id: 'settings_machine_new', profileType: null, coverImages: [], - isExpert: null, }, settings_member_new: { - isV4Member: null, _deleted: false, workspaceType: null, country: 'Poland', @@ -247,9 +238,6 @@ export const users = { _id: 'settings_member_new', profileType: null, coverImages: [], - isExpert: null, - collectedPlasticTypes: null, - openingHours: [], location: null, verified: true, _modified: '2020-01-07T12:14:30.030Z', @@ -257,14 +245,11 @@ export const users = { displayName: 'settings_member_new', }, settings_plastic_new: { - collectedPlasticTypes: [], - openingHours: [], location: null, verified: true, _modified: '2020-01-07T12:15:42.218Z', _created: '2020-01-07T12:15:42.218Z', displayName: 'settings_plastic_new', - isV4Member: null, _deleted: false, workspaceType: null, country: null, @@ -277,17 +262,13 @@ export const users = { _id: 'settings_plastic_new', profileType: null, coverImages: [], - isExpert: null, }, settings_workplace_empty: { - collectedPlasticTypes: [], - openingHours: [], location: null, verified: true, _modified: '2020-01-07T12:15:42.218Z', _created: '2020-01-07T12:15:42.218Z', displayName: 'settings_workplace_empty', - isV4Member: null, _deleted: false, workspaceType: null, country: null, @@ -300,16 +281,13 @@ export const users = { _id: 'settings_workplace_empty', profileType: 'workspace', coverImages: [], - isExpert: null, }, settings_workplace_new: { - openingHours: [], location: null, verified: true, _modified: '2020-01-07T12:14:15.081Z', _created: '2020-01-07T12:14:15.081Z', displayName: 'settings_workplace_new', - isV4Member: null, _deleted: false, workspaceType: null, country: null, @@ -322,8 +300,6 @@ export const users = { _id: 'settings_workplace_new', profileType: 'workspace', coverImages: [], - isExpert: null, - collectedPlasticTypes: null, email: 'settings_workplace_new@test.com', password: 'test1234', userRoles: [UserRole.BETA_TESTER], @@ -359,13 +335,11 @@ export const users = { isContactableByPublic: false, }, mapview_testing_rejected: { - openingHours: [], location: null, verified: true, _modified: '2020-01-07T12:14:15.081Z', _created: '2020-01-07T12:14:15.081Z', displayName: 'mapview_testing_rejected', - isV4Member: null, _deleted: false, workspaceType: null, country: null, @@ -378,13 +352,11 @@ export const users = { _id: 'mapview_testing_rejected', profileType: 'workspace', coverImages: [], - isExpert: null, - collectedPlasticTypes: null, email: 'mapview_testing_rejected@test.com', password: 'mapview_testing_rejected@test.com', }, profile_views: { - _authID: 'a0dFrGVJTlQUA9BqH0QmnQM6flX3', + _authID: '8th9KhBU0RRy6KwCrH0UQHNMxs42', _id: 'profile_views', _created: '2022-01-30T18:51:57.719Z', _modified: '2022-01-30T18:51:57.719Z', @@ -408,7 +380,6 @@ export const users = { ], about: 'Hi! I have 99 views', location: 'nl', - total_views: 99, }, profile_no_views: { _authID: 'a0dFrGVJTlQUA9BqH0QmnQM6flX4', diff --git a/shared/models/db.ts b/shared/models/db.ts index c7ab75093c..292f6be357 100644 --- a/shared/models/db.ts +++ b/shared/models/db.ts @@ -10,22 +10,22 @@ import type { ISODateString } from './common' * NOTE - these are a bit messy due to various migrations and changes * In the future all endpoints should try to just retain prefix-base-revision, e.g. oa_users_rev20201012 **************************************************************************************/ -export const generateDBEndpoints = (DB_PREFIX = '') => ({ - howtos: `${DB_PREFIX}v3_howtos`, - users: `${DB_PREFIX}v3_users`, - user_notifications: `${DB_PREFIX}user_notifications_rev20221209`, - tags: `${DB_PREFIX}v3_tags`, - categories: `${DB_PREFIX}v3_categories`, - researchCategories: `${DB_PREFIX}research_categories_rev20221224`, - mappins: `${DB_PREFIX}v3_mappins`, - messages: `${DB_PREFIX}messages_rev20231022`, - research: `${DB_PREFIX}research_rev20201020`, - aggregations: `${DB_PREFIX}aggregations_rev20220126`, - emails: `${DB_PREFIX}emails`, - questions: `${DB_PREFIX}questions_rev20230926`, - questionCategories: `${DB_PREFIX}question_categories_rev20231130`, - user_integrations: `${DB_PREFIX}user_integrations`, - discussions: `${DB_PREFIX}discussions_rev20231022`, +export const generateDBEndpoints = () => ({ + howtos: `v3_howtos`, + users: `v3_users`, + user_notifications: `user_notifications_rev20221209`, + tags: `v3_tags`, + categories: `v3_categories`, + researchCategories: `research_categories_rev20221224`, + mappins: `v3_mappins`, + messages: `messages_rev20231022`, + research: `research_rev20201020`, + aggregations: `aggregations_rev20220126`, + emails: `emails`, + questions: `questions_rev20230926`, + questionCategories: `question_categories_rev20231130`, + user_integrations: `user_integrations`, + discussions: `discussions_rev20231022`, }) /** @@ -45,20 +45,6 @@ if (!('process' in globalThis)) { globalThis.process = {} as any } -const e = process.env || ({} as any) - -// Check if sessionStorage exists (e.g. running in browser environment), and use if available -const storage = - typeof sessionStorage === 'undefined' ? ({} as any) : sessionStorage - -/** - * A prefix can be used to simplify large-scale schema changes or multisite hosting - * and allow multiple sites to use one DB (used for parallel test seed DBs) - * e.g. oa_ - * SessionStorage prefixes are used to allow test ci environments to dynamically set a db endpoint - */ -const DB_PREFIX = storage.DB_PREFIX || e.VITE_DB_PREFIX || '' - /** * Mapping of generic database endpoints to specific prefixed and revisioned versions for the * current implementation @@ -67,14 +53,14 @@ const DB_PREFIX = storage.DB_PREFIX || e.VITE_DB_PREFIX || '' * const allHowtos = await db.get(DB_ENDPOINTS.howtos) * ``` */ -export const DB_ENDPOINTS = generateDBEndpoints(DB_PREFIX) +export const DB_ENDPOINTS = generateDBEndpoints() export type DBEndpoint = keyof typeof DB_ENDPOINTS export interface DBDoc { _id: string _created: ISODateString - _modified: ISODateString + _modified?: ISODateString _deleted: boolean - _contentModifiedTimestamp: ISODateString + _contentModifiedTimestamp?: ISODateString } diff --git a/shared/models/maps.ts b/shared/models/maps.ts index 73f9a51638..e050c1d834 100644 --- a/shared/models/maps.ts +++ b/shared/models/maps.ts @@ -1,5 +1,6 @@ import type { ILatLng } from './common' import type { IModerationStatus } from './moderation' +import type { ITag } from './tags' import type { IUserBadges, ProfileTypeName, WorkspaceType } from './user' /** @@ -68,6 +69,7 @@ export interface IProfileCreator { displayName: string isContactableByPublic: boolean profileType: ProfileTypeName + tags?: ITag[] workspaceType?: string userImage?: string } diff --git a/shared/models/questions.ts b/shared/models/questions.ts index 9652c9df31..3a879b3330 100644 --- a/shared/models/questions.ts +++ b/shared/models/questions.ts @@ -20,6 +20,7 @@ export namespace IQuestion { _deleted: boolean subscribers?: UserIdList commentCount?: number + keywords?: string[] } & DBDoc & FormInput & ISharedFeatures diff --git a/shared/models/research.ts b/shared/models/research.ts index e1e23663f7..f4b82a29c1 100644 --- a/shared/models/research.ts +++ b/shared/models/research.ts @@ -59,6 +59,7 @@ export namespace IResearch { totalUpdates?: number totalUsefulVotes?: number totalCommentCount: number + latestCommentDate?: string keywords?: string[] } & Omit & DBDoc diff --git a/shared/models/tags.ts b/shared/models/tags.ts index 54486318e2..d68dc98e14 100644 --- a/shared/models/tags.ts +++ b/shared/models/tags.ts @@ -17,5 +17,5 @@ when building tag uploader it should enforce reasonable max size image (say 500p export interface ITag extends DBDoc { label: string - image: string + image?: string } diff --git a/shared/models/user.ts b/shared/models/user.ts index 7830e30b5e..5a464db3bf 100644 --- a/shared/models/user.ts +++ b/shared/models/user.ts @@ -3,6 +3,7 @@ import type { DBDoc } from './db' import type { IModerationStatus } from './moderation' import type { INotification, INotificationSettings } from './notifications' import type { IUploadedFileMeta } from './storage' +import type { ISelectedTags } from './tags' /* eslint-disable @typescript-eslint/naming-convention */ export enum UserRole { @@ -100,22 +101,6 @@ export type ProfileTypeName = // Below are primarily used for PP -export type PlasticTypeLabel = - | 'pet' - | 'hdpe' - | 'pvc' - | 'ldpe' - | 'pp' - | 'ps' - | 'other' - -export type MachineBuilderXpLabel = - | 'electronics' - | 'machining' - | 'welding' - | 'assembling' - | 'mould-making' - export type WorkspaceType = | 'shredder' | 'sheetpress' @@ -123,12 +108,6 @@ export type WorkspaceType = | 'injection' | 'mix' -export interface IPlasticType { - label: PlasticTypeLabel - number: string - imageSrc?: string -} - export interface IProfileType { label: ProfileTypeName imageSrc?: string @@ -143,16 +122,6 @@ export interface IWorkspaceType { subText?: string } -export interface IMAchineBuilderXp { - label: MachineBuilderXpLabel -} - -export interface IOpeningHours { - day: string - openFrom: string - openTo: string -} - export type UserMention = { username: string location: string @@ -193,17 +162,15 @@ export interface IUser { isContactableByPublic?: boolean patreon?: PatreonUser | null totalUseful?: number + total_views?: number + + // New generic profile field for all profile types + tags?: ISelectedTags // Primary PP profile type related fields profileType: ProfileTypeName - workspaceType?: WorkspaceType | null + workspaceType?: WorkspaceType | null // <-- to-do replace with tags mapPinDescription?: string | null - openingHours?: IOpeningHours[] - collectedPlasticTypes?: PlasticTypeLabel[] | null - machineBuilderXp?: IMAchineBuilderXp[] | null - isExpert?: boolean | null - isV4Member?: boolean | null - total_views?: number } export interface IUserBadges { diff --git a/src/assets/images/plastic-types/hdpe.svg b/src/assets/images/plastic-types/hdpe.svg deleted file mode 100755 index 4d5a5fefb6..0000000000 --- a/src/assets/images/plastic-types/hdpe.svg +++ /dev/null @@ -1 +0,0 @@ -icon plastic type hdpe \ No newline at end of file diff --git a/src/assets/images/plastic-types/ldpe.svg b/src/assets/images/plastic-types/ldpe.svg deleted file mode 100755 index 2538181824..0000000000 --- a/src/assets/images/plastic-types/ldpe.svg +++ /dev/null @@ -1,8 +0,0 @@ -icon plastic type ldpe - - - - - - - \ No newline at end of file diff --git a/src/assets/images/plastic-types/other.svg b/src/assets/images/plastic-types/other.svg deleted file mode 100755 index a62c4cd4f7..0000000000 --- a/src/assets/images/plastic-types/other.svg +++ /dev/null @@ -1 +0,0 @@ -icon plastic type other \ No newline at end of file diff --git a/src/assets/images/plastic-types/pet.svg b/src/assets/images/plastic-types/pet.svg deleted file mode 100755 index f99758e7c9..0000000000 --- a/src/assets/images/plastic-types/pet.svg +++ /dev/null @@ -1 +0,0 @@ -icon plastic type pet \ No newline at end of file diff --git a/src/assets/images/plastic-types/pp.svg b/src/assets/images/plastic-types/pp.svg deleted file mode 100755 index 46dd3aaa01..0000000000 --- a/src/assets/images/plastic-types/pp.svg +++ /dev/null @@ -1 +0,0 @@ -icon plastic type pp \ No newline at end of file diff --git a/src/assets/images/plastic-types/ps.svg b/src/assets/images/plastic-types/ps.svg deleted file mode 100755 index 64fad655ab..0000000000 --- a/src/assets/images/plastic-types/ps.svg +++ /dev/null @@ -1 +0,0 @@ -icon plastic type ps \ No newline at end of file diff --git a/src/assets/images/plastic-types/pvc.svg b/src/assets/images/plastic-types/pvc.svg deleted file mode 100755 index a8e2c74701..0000000000 --- a/src/assets/images/plastic-types/pvc.svg +++ /dev/null @@ -1 +0,0 @@ -icon plastic type pvc \ No newline at end of file diff --git a/src/common/DiscussionWrapper.test.tsx b/src/common/DiscussionWrapper.test.tsx index 8f416d0791..fa07094d6c 100644 --- a/src/common/DiscussionWrapper.test.tsx +++ b/src/common/DiscussionWrapper.test.tsx @@ -1,7 +1,7 @@ import '@testing-library/jest-dom/vitest' -import { ThemeProvider } from '@emotion/react' import { act, render, waitFor } from '@testing-library/react' +import { ThemeProvider } from '@theme-ui/core' import { Provider } from 'mobx-react' import { FactoryUser } from 'src/test/factories/User' import { testingThemeStyles } from 'src/test/utils/themeUtils' diff --git a/src/common/DiscussionWrapper.tsx b/src/common/DiscussionWrapper.tsx index e1aec127ea..f68e33cb94 100644 --- a/src/common/DiscussionWrapper.tsx +++ b/src/common/DiscussionWrapper.tsx @@ -185,7 +185,7 @@ export const DiscussionWrapper = observer((props: IProps) => { return ( <> - {isLoading && } + {isLoading && } {!isLoading && !discussion && {DISCUSSION_NOT_FOUND}} {discussion && canHideComments && ( ( +export const FileInputField = ({ + input, + ...rest +}: FieldProps & { admin: boolean }) => ( { diff --git a/src/common/Form/FileInput/FileInput.tsx b/src/common/Form/FileInput/FileInput.tsx index c05da4cac8..592b3d4be9 100644 --- a/src/common/Form/FileInput/FileInput.tsx +++ b/src/common/Form/FileInput/FileInput.tsx @@ -6,6 +6,7 @@ import { Button, DownloadStaticFile } from 'oa-components' import { Flex } from 'theme-ui' import { UPPY_CONFIG } from './UppyConfig' +import { UPPY_CONFIG_ADMIN } from './UppyConfigAdmin' import type { UppyFile } from '@uppy/core' @@ -18,14 +19,20 @@ interface IUppyFiles { interface IProps { onFilesChange?: (files: (Blob | File)[]) => void 'data-cy'?: string + admin: boolean } interface IState { open: boolean } export const FileInput = (props: IProps) => { const [state, setState] = useState({ open: false }) + const uploadConfig = props.admin ? UPPY_CONFIG_ADMIN : UPPY_CONFIG const [uppy] = useState( - () => new Uppy({ ...UPPY_CONFIG, onBeforeUpload: () => uploadTriggered() }), + () => + new Uppy({ + ...uploadConfig, + onBeforeUpload: () => uploadTriggered(), + }), ) useEffect(() => { diff --git a/src/common/Form/FileInput/UppyConfigAdmin.ts b/src/common/Form/FileInput/UppyConfigAdmin.ts new file mode 100644 index 0000000000..bab248008f --- /dev/null +++ b/src/common/Form/FileInput/UppyConfigAdmin.ts @@ -0,0 +1,26 @@ +import type { UppyOptions } from '@uppy/core' + +export const UPPY_CONFIG_ADMIN: Partial = { + restrictions: { + // max upload file size in bytes (i.e. 300 x 1048576 => 300 MB) + maxFileSize: 300 * 1048576, + maxNumberOfFiles: 5, + minNumberOfFiles: null, + allowedFileTypes: null, + }, + locale: { + strings: { + youCanOnlyUploadX: { + 0: 'You can only upload %{smart_count} file', + 1: 'You can only upload %{smart_count} files', + }, + youHaveToAtLeastSelectX: { + 0: 'You have to select at least %{smart_count} file', + 1: 'You have to select at least %{smart_count} files', + }, + exceedsSize: 'This file exceeds maximum allowed size of %{size}', + youCanOnlyUploadFileTypes: 'You can only upload: %{types}', + companionError: 'Connection with Companion failed', + }, + }, +} diff --git a/src/common/Form/ImageInput/ImageInput.tsx b/src/common/Form/ImageInput/ImageInput.tsx index b8aa0c599d..975eaaade0 100644 --- a/src/common/Form/ImageInput/ImageInput.tsx +++ b/src/common/Form/ImageInput/ImageInput.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react' -import Dropzone from 'react-dropzone' +import Dropzone from 'react-dropzone-esm' import { Button, Modal } from 'oa-components' import { logger } from 'src/logger' import { Box, Flex, Image, Text } from 'theme-ui' diff --git a/src/common/HideDiscussionContainer.tsx b/src/common/HideDiscussionContainer.tsx index a958b642c5..d1b1bacd6e 100644 --- a/src/common/HideDiscussionContainer.tsx +++ b/src/common/HideDiscussionContainer.tsx @@ -53,9 +53,7 @@ export const HideDiscussionContainer = ({ onClick={() => setViewComments((prev) => !prev)} backgroundColor={viewComments ? '#c2daf0' : '#e2edf7'} className={viewComments ? 'viewComments' : ''} - data-cy={`HideDiscussionContainer: button ${ - !viewComments && 'open-comments' - }`} + data-cy={`HideDiscussionContainer: button ${viewComments ? 'close-comments' : 'open-comments'} ${commentCount !== 0 ? 'has-comments' : 'no-comments'}`} > {buttonText} diff --git a/src/common/ProfileTags.tsx b/src/common/ProfileTags.tsx new file mode 100644 index 0000000000..d16bd97f86 --- /dev/null +++ b/src/common/ProfileTags.tsx @@ -0,0 +1,18 @@ +import { ProfileTagsList } from 'oa-components' +import { getValidTags } from 'src/utils/getValidTags' + +import type { ISelectedTags } from 'oa-shared' + +interface IProps { + tagIds: ISelectedTags +} + +export const ProfileTags = ({ tagIds }: IProps) => { + const tags = getValidTags(tagIds) + + if (tags.length === 0) { + return null + } + + return +} diff --git a/src/common/Tags/TagsList.tsx b/src/common/Tags/TagsList.tsx index 76b2e57159..3b09b7421c 100644 --- a/src/common/Tags/TagsList.tsx +++ b/src/common/Tags/TagsList.tsx @@ -8,7 +8,9 @@ interface IProps { } export const TagList = ({ tags }: IProps) => { - if (!tags) return + if (!tags) { + return null + } const { allTagsByKey } = useCommonStores().stores.tagsStore @@ -16,5 +18,5 @@ export const TagList = ({ tags }: IProps) => { .filter(Boolean) .map((key) => allTagsByKey[key]) - return !!tagList && + return tagList && tagList.length > 0 && } diff --git a/src/common/Tags/TagsSelect.tsx b/src/common/Tags/TagsSelect.tsx index 25ab10b0fc..ce78df2c11 100644 --- a/src/common/Tags/TagsSelect.tsx +++ b/src/common/Tags/TagsSelect.tsx @@ -14,6 +14,7 @@ export interface IProps extends Partial> { onChange: (val: ISelectedTags) => void styleVariant?: 'selector' | 'filter' placeholder?: string + tagsSource?: ITag[] } interface IState { selectedTags: string[] @@ -22,6 +23,8 @@ interface IState { const TagsSelect = (props: IProps) => { const { tagsStore } = useCommonStores().stores const { allTags } = tagsStore + + const allTagsData = props.tagsSource ? props.tagsSource : allTags const [state, setState] = useState({ selectedTags: [] }) // if we initialise with a value we want to update the state to reflect the selected tags @@ -42,8 +45,8 @@ const TagsSelect = (props: IProps) => { // as react-select can't keep track of which object key corresponds to the selected // value include manual lookup so that value can also be passed from props - const _getSelected = (allTags: ITag[]) => { - return allTags?.filter((tag) => state.selectedTags.includes(tag._id)) + const _getSelected = (allTagsData: ITag[]) => { + return allTagsData?.filter((tag) => state.selectedTags.includes(tag._id)) } // whilst we deal with arrays of selected tag ids in the component we want to store as a json map @@ -58,15 +61,15 @@ const TagsSelect = (props: IProps) => { return ( 0 ? 'tag-select' : 'tag-select-empty'} + data-cy={allTagsData?.length > 0 ? 'tag-select' : 'tag-select-empty'} > - -) - -// validation - return undefined if no error (i.e. valid) -const isRequired = (value: any) => (value ? undefined : 'Required') - -export const CustomCheckbox = (props: IProps) => { - const { - value, - index, - imageSrc, - isSelected, - btnLabel, - fullWidth, - 'data-cy': dataCy, - required, - } = props - const classNames: Array = [] - if (isSelected) { - classNames.push('selected') - } - if (fullWidth) { - classNames.push('full-width') - } - - return ( - - ) -} diff --git a/src/pages/UserSettings/content/fields/OpeningHoursPicker.field.tsx b/src/pages/UserSettings/content/fields/OpeningHoursPicker.field.tsx deleted file mode 100644 index b294d165f9..0000000000 --- a/src/pages/UserSettings/content/fields/OpeningHoursPicker.field.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import React, { useState } from 'react' -import { Field } from 'react-final-form' -import { Button, Modal } from 'oa-components' -import { SelectField } from 'src/common/Form/Select.field' -import { required } from 'src/utils/validators' -import { Flex, Text } from 'theme-ui' - -const WEEK_DAYS = [ - { - value: 'Monday', - label: 'Monday', - }, - { - value: 'Tuesday', - label: 'Tuesday', - }, - { - value: 'Wednesday', - label: 'Wednesday', - }, - { - value: 'Thursday', - label: 'Thursday', - }, - { - value: 'Friday', - label: 'Friday', - }, - { - value: 'Saturday', - label: 'Saturday', - }, - { - value: 'Sunday', - label: 'Sunday', - }, -] - -const OPENING_HOURS = [ - { - value: '01:00 AM', - label: '01:00 AM', - }, - { - value: '02:00 AM', - label: '02:00 AM', - }, - { - value: '03:00 AM', - label: '03:00 AM', - }, - { - value: '04:00 AM', - label: '04:00 AM', - }, - { - value: '05:00 AM', - label: '05:00 AM', - }, - { - value: '06:00 AM', - label: '06:00 AM', - }, - { - value: '07:00 AM', - label: '07:00 AM', - }, - { - value: '08:00 AM', - label: '08:00 AM', - }, - { - value: '09:00 AM', - label: '09:00 AM', - }, - { - value: '10:00 AM', - label: '10:00 AM', - }, - { - value: '11:00 AM', - label: '11:00 AM', - }, - { - value: '12:00 AM', - label: '12:00 AM', - }, - { - value: '01:00 PM', - label: '01:00 PM', - }, - { - value: '02:00 PM', - label: '02:00 PM', - }, - { - value: '03:00 PM', - label: '03:00 PM', - }, - { - value: '04:00 PM', - label: '04:00 PM', - }, - { - value: '05:00 PM', - label: '05:00 PM', - }, - { - value: '06:00 PM', - label: '06:00 PM', - }, - { - value: '07:00 PM', - label: '07:00 PM', - }, - { - value: '08:00 PM', - label: '08:00 PM', - }, - { - value: '09:00 PM', - label: '09:00 PM', - }, - { - value: '10:00 PM', - label: '10:00 PM', - }, - { - value: '11:00 PM', - label: '11:00 PM', - }, - { - value: '12:00 PM', - label: '12:00 PM', - }, -] - -interface IProps { - openingHoursValues?: string - index: number - onDelete: (index: number) => void -} -interface IState { - showDeleteModal: boolean - _toDocsList: boolean -} - -export const OpeningHoursPicker = (props: IProps) => { - const { openingHoursValues, index } = props - const [state, setState] = useState({ - showDeleteModal: false, - _toDocsList: false, - }) - - const toggleDeleteModal = () => { - setState((state) => ({ ...state, showDeleteModal: !state.showDeleteModal })) - } - const confirmDelete = () => { - toggleDeleteModal() - props.onDelete(props.index) - } - - return ( - - - - - - {index > 0 && ( - - )}{' '} - - - toggleDeleteModal()} - isOpen={state.showDeleteModal} - > - Are you sure you want to delete this schedule ? - - - - - - - - - - - ) -} diff --git a/src/pages/UserSettings/content/sections/Collection.section.tsx b/src/pages/UserSettings/content/sections/Collection.section.tsx deleted file mode 100644 index 21d06da873..0000000000 --- a/src/pages/UserSettings/content/sections/Collection.section.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import * as React from 'react' -import { FieldArray } from 'react-final-form-arrays' -import { Button } from 'oa-components' -import Hdpe from 'src/assets/images/plastic-types/hdpe.svg' -import Ldpe from 'src/assets/images/plastic-types/ldpe.svg' -import Other from 'src/assets/images/plastic-types/other.svg' -import Pet from 'src/assets/images/plastic-types/pet.svg' -import PP from 'src/assets/images/plastic-types/pp.svg' -import PS from 'src/assets/images/plastic-types/ps.svg' -import Pvc from 'src/assets/images/plastic-types/pvc.svg' -import { fields, headings } from 'src/pages/UserSettings/labels' -import { Flex, Grid, Heading, Text } from 'theme-ui' - -import { FlexSectionContainer } from '../elements' -import { CustomCheckbox } from '../fields/CustomCheckbox.field' -import { OpeningHoursPicker } from '../fields/OpeningHoursPicker.field' - -import type { IPlasticType, IUser } from 'oa-shared' - -interface IProps { - formValues: IUser - required: boolean -} - -export const CollectionSection = (props: IProps) => { - const { required } = props - const { description, title } = fields.openingHours - - return ( - - - {headings.collection} - - - - {`${title} *`} - - {({ fields }) => ( - <> - {fields.map((name, index: number) => ( - { - fields.remove(fieldIndex) - }} - /> - ))} - {fields.length && fields.length < 7 && ( - - )} - - )} - - - - {`${fields.plastic.title}`} * - {required && ( - - {fields.plastic.description} - - )} - - - {({ fields }) => ( - <> - {PLASTIC_TYPES.map((plastic, index: number) => ( - { - if (fields.value && fields.value.length !== 0) { - if (fields.value.includes(plastic.label)) { - // eslint-disable-next-line - fields.value.map((value, selectedValIndex) => { - if (value === plastic.label) { - fields.remove(selectedValIndex) - } - }) - } else { - fields.push(plastic.label) - } - } else { - fields.push(plastic.label) - } - }} - imageSrc={plastic.imageSrc} - /> - ))} - - )} - - - - - - ) -} - -const PLASTIC_TYPES: IPlasticType[] = [ - { - label: 'pet', - number: '1', - imageSrc: Pet, - }, - { - label: 'hdpe', - number: '2', - imageSrc: Hdpe, - }, - { - label: 'pvc', - number: '3', - imageSrc: Pvc, - }, - { - label: 'ldpe', - number: '4', - imageSrc: Ldpe, - }, - { - label: 'pp', - number: '5', - imageSrc: PP, - }, - { - label: 'ps', - number: '6', - imageSrc: PS, - }, - { - label: 'other', - number: '7', - imageSrc: Other, - }, -] diff --git a/src/pages/UserSettings/content/sections/Expertise.section.tsx b/src/pages/UserSettings/content/sections/Expertise.section.tsx deleted file mode 100644 index b4e1594b5d..0000000000 --- a/src/pages/UserSettings/content/sections/Expertise.section.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { FieldArray } from 'react-final-form-arrays' -import { fields, headings } from 'src/pages/UserSettings/labels' -import { Flex, Heading, Text } from 'theme-ui' - -import { FlexSectionContainer } from '../elements' -import { CustomCheckbox } from '../fields/CustomCheckbox.field' - -import type { IMAchineBuilderXp } from 'oa-shared' - -interface IProps { - required: boolean -} - -const MACHINE_BUILDER_XP: IMAchineBuilderXp[] = [ - { label: 'electronics' }, - { label: 'machining' }, - { label: 'welding' }, - { label: 'assembling' }, - { label: 'mould-making' }, -] - -export const ExpertiseSection = (props: IProps) => { - const { required } = props - const { description, title } = fields.expertise - - return ( - - - {headings.expertise} - {`${title} *`} - {required && ( - {description} - )} - - - - {({ fields }) => ( - <> - {MACHINE_BUILDER_XP.map((xp, index: number) => ( - { - if (fields.value && fields.value.length !== 0) { - if (fields.value.includes(xp.label)) { - // eslint-disable-next-line - fields.value.map((value, selectedValIndex) => { - if (value === xp.label) { - fields.remove(selectedValIndex) - } - }) - } else { - fields.push(xp.label) - } - } else { - fields.push(xp.label) - } - }} - btnLabel={xp.label} - /> - ))} - - )} - - - - - ) -} diff --git a/src/pages/UserSettings/content/sections/Focus.section.test.tsx b/src/pages/UserSettings/content/sections/Focus.section.test.tsx index 19c8202b96..1cdbba3298 100644 --- a/src/pages/UserSettings/content/sections/Focus.section.test.tsx +++ b/src/pages/UserSettings/content/sections/Focus.section.test.tsx @@ -1,7 +1,7 @@ import '@testing-library/jest-dom/vitest' -import { ThemeProvider } from '@emotion/react' import { render, screen } from '@testing-library/react' +import { ThemeProvider } from '@theme-ui/core' import { getSupportedProfileTypes } from 'src/modules/profile' import { FocusSection } from 'src/pages/UserSettings/content/sections/Focus.section' import { headings } from 'src/pages/UserSettings/labels' diff --git a/src/pages/UserSettings/content/sections/Focus.section.tsx b/src/pages/UserSettings/content/sections/Focus.section.tsx index 90248442d5..bf6d37298b 100644 --- a/src/pages/UserSettings/content/sections/Focus.section.tsx +++ b/src/pages/UserSettings/content/sections/Focus.section.tsx @@ -1,21 +1,22 @@ import { Field } from 'react-final-form' -import { useTheme } from '@emotion/react' import { ExternalLink } from 'oa-components' import { getSupportedProfileTypes } from 'src/modules/profile' import { buttons, fields, headings } from 'src/pages/UserSettings/labels' -import { Box, Flex, Grid, Heading, Paragraph, Text } from 'theme-ui' +import { Box, Flex, Grid, Heading, Paragraph, Text, useThemeUI } from 'theme-ui' import { FlexSectionContainer } from '../elements' import { CustomRadioField } from '../fields/CustomRadio.field' import type { ProfileTypeName } from 'oa-shared' +import type { ThemeWithName } from 'oa-themes' const ProfileTypes = () => { const profileGuidelinesUrl = import.meta.env.VITE_PROFILE_GUIDELINES_URL || process.env.VITE_PROFILE_GUIDELINES_URL const { description, error } = fields.activities - const theme = useTheme() + const themeUi = useThemeUI() + const theme = themeUi.theme as ThemeWithName const profileTypes = getSupportedProfileTypes().filter(({ label }) => Object.keys(theme.badges).includes(label), ) @@ -28,7 +29,7 @@ const ProfileTypes = () => { ( - + {headings.focus} diff --git a/src/pages/UserSettings/content/sections/ProfileTags.section.tsx b/src/pages/UserSettings/content/sections/ProfileTags.section.tsx new file mode 100644 index 0000000000..e47e1183c7 --- /dev/null +++ b/src/pages/UserSettings/content/sections/ProfileTags.section.tsx @@ -0,0 +1,47 @@ +import { Field } from 'react-final-form' +import TagsSelect from 'src/common/Tags/TagsSelect' +import { fields } from 'src/pages/UserSettings/labels' +import { userService } from 'src/services/user.service' +import { COMPARISONS } from 'src/utils/comparisons' +import { Flex, Heading, Text } from 'theme-ui' + +import { FlexSectionContainer } from '../elements' + +export const ProfileTags = () => { + const { description, title } = fields.tags + + return ( + + + {title} + {description} + + + + ) +} + +const WrappedTagsSelect = ({ input, ...rest }) => { + const { getProfileTags } = userService + const profileTags = getProfileTags() + + return ( + input.onChange(tags)} + tagsSource={profileTags} + {...rest} + /> + ) +} diff --git a/src/pages/UserSettings/content/sections/UserInfos.section.tsx b/src/pages/UserSettings/content/sections/UserInfos.section.tsx index 55f1020b5a..35f16cb48e 100644 --- a/src/pages/UserSettings/content/sections/UserInfos.section.tsx +++ b/src/pages/UserSettings/content/sections/UserInfos.section.tsx @@ -1,3 +1,4 @@ +import { useContext } from 'react' import { Field } from 'react-final-form' import { FieldArray } from 'react-final-form-arrays' import countriesList from 'countries-list' @@ -11,6 +12,7 @@ import { import { ProfileTypeList } from 'oa-shared' import { SelectField } from 'src/common/Form/Select.field' import { isModuleSupported, MODULE } from 'src/modules' +import { EnvironmentContext } from 'src/pages/common/EnvironmentContext' import { buttons, fields, headings } from 'src/pages/UserSettings/labels' import { required } from 'src/utils/validators' import { Flex, Heading, Text } from 'theme-ui' @@ -29,6 +31,8 @@ interface IProps { } export const UserInfosSection = ({ formValues }: IProps) => { + const env = useContext(EnvironmentContext) + const { countries } = countriesList const { profileType, links, location } = formValues const isMemberProfile = profileType === ProfileTypeList.MEMBER @@ -97,7 +101,10 @@ export const UserInfosSection = ({ formValues }: IProps) => { {noMapPin && ( {country.title} - {isModuleSupported(MODULE.MAP) && ( + {isModuleSupported( + env?.VITE_SUPPORTED_MODULES || '', + MODULE.MAP, + ) && ( diff --git a/src/pages/common/EnvironmentContext.ts b/src/pages/common/EnvironmentContext.ts new file mode 100644 index 0000000000..4afce1cbe5 --- /dev/null +++ b/src/pages/common/EnvironmentContext.ts @@ -0,0 +1,21 @@ +import { createContext } from 'react' +import { _supportedConfigurationOptions } from 'src/config/constants' + +import type { ConfigurationOption } from 'src/config/constants' + +export const getEnvVariables = (): Partial => { + const envVariables: Partial = {} + _supportedConfigurationOptions.forEach((option) => { + const value = import.meta.env[option] + if (value !== undefined) { + envVariables[option] = value + } + }) + return envVariables +} +export const EnvironmentContext = + createContext>(getEnvVariables()) + +type EnvVariables = { + [K in ConfigurationOption]: string +} diff --git a/src/pages/common/GlobalSiteFooter/GlobalSiteFooter.tsx b/src/pages/common/GlobalSiteFooter/GlobalSiteFooter.tsx index a4a24fe9bb..d192e412ff 100644 --- a/src/pages/common/GlobalSiteFooter/GlobalSiteFooter.tsx +++ b/src/pages/common/GlobalSiteFooter/GlobalSiteFooter.tsx @@ -1,25 +1,24 @@ -import { useEffect, useState } from 'react' +import { useContext, useMemo } from 'react' import { useLocation } from '@remix-run/react' import { SiteFooter } from 'oa-components' -const isFooterVisible = (path) => { - return ( - !path.startsWith('/map') && !path.startsWith('/academy') && path !== '/' - ) -} +import { EnvironmentContext } from '../EnvironmentContext' const GlobalSiteFooter = () => { + const env = useContext(EnvironmentContext) const location = useLocation() - const [showFooter, setShowFooter] = useState( - isFooterVisible(location.pathname), - ) - useEffect( - () => setShowFooter(isFooterVisible(location?.pathname)), - [location], - ) + const showFooter = useMemo(() => { + const path = location?.pathname + + return ( + !path.startsWith('/map') && !path.startsWith('/academy') && path !== '/' + ) + }, [location?.pathname]) - return showFooter ? : null + return showFooter ? ( + + ) : null } export default GlobalSiteFooter diff --git a/src/pages/common/Header/Header.tsx b/src/pages/common/Header/Header.tsx index c84a0174e4..265db6d9dd 100644 --- a/src/pages/common/Header/Header.tsx +++ b/src/pages/common/Header/Header.tsx @@ -1,9 +1,11 @@ -import React from 'react' -import { useTheme, withTheme } from '@emotion/react' +import React, { useContext } from 'react' +import { withTheme } from '@emotion/react' import { motion } from 'framer-motion' import { observer } from 'mobx-react' import { Button } from 'oa-components' import { UserRole } from 'oa-shared' +// eslint-disable-next-line import/no-unresolved +import { ClientOnly } from 'remix-utils/client-only' import { useCommonStores } from 'src/common/hooks/useCommonStores' import { isModuleSupported, MODULE } from 'src/modules' import Logo from 'src/pages/common/Header/Menu/Logo/Logo' @@ -13,26 +15,28 @@ import { NotificationsDesktop } from 'src/pages/common/Header/Menu/Notifications import { NotificationsIcon } from 'src/pages/common/Header/Menu/Notifications/NotificationsIcon' import { NotificationsMobile } from 'src/pages/common/Header/Menu/Notifications/NotificationsMobile' import Profile from 'src/pages/common/Header/Menu/Profile/Profile' -import { Flex, Text } from 'theme-ui' +import { Flex, Text, useThemeUI } from 'theme-ui' +import { EnvironmentContext } from '../EnvironmentContext' import { getFormattedNotifications } from './getFormattedNotifications' import { MobileMenuContext } from './MobileMenuContext' import type { ThemeWithName } from 'oa-themes' const MobileNotificationsWrapper = ({ children }) => { - const theme = useTheme() + const themeUi = useThemeUI() + const theme = themeUi.theme as ThemeWithName return ( { ) } -const Header = observer(({ theme }: { theme: ThemeWithName }) => { +const Header = observer(() => { + const { theme } = useThemeUI() + const env = useContext(EnvironmentContext) const { userNotificationsStore } = useCommonStores().stores const user = userNotificationsStore.user const notifications = getFormattedNotifications( @@ -96,11 +102,10 @@ const Header = observer(({ theme }: { theme: ThemeWithName }) => { > { (user.userRoles || []).includes(UserRole.BETA_TESTER) && ( { )} { } /> )} - {isModuleSupported(MODULE.USER) && } + {isModuleSupported(env.VITE_SUPPORTED_MODULES || '', MODULE.USER) && ( + + )} - - -