Merge pull request #7952 from scratchfoundation/release/2023-11-15

[Master] release/2023-11-15
This commit is contained in:
Christopher Willis-Ford 2023-11-16 07:28:43 -08:00 committed by GitHub
commit 6c6b675c44
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 2031 additions and 1350 deletions

View file

@ -1,197 +0,0 @@
version: 2.1
aliases:
- &defaults
docker:
- image: cimg/node:16.14.2-browsers
auth:
username: $DOCKERHUB_USERNAME
password: $DOCKERHUB_PASSWORD
working_directory: ~/repo
- &setup
name: "setup"
command: |
npm --production=false ci
mkdir ./test/results
- &lint
name: "run lint tests"
command: |
npm run test:lint:ci
- &build
name: "run npm build"
command: |
WWW_VERSION=${CIRCLE_SHA1:0:5} npm run build
- &unit
name: "Run unit tests"
command: |
JEST_JUNIT_OUTPUT_NAME=unit-jest-results.xml npm run test:unit:jest:unit -- --reporters=jest-junit
JEST_JUNIT_OUTPUT_NAME=localization-jest-results.xml npm run test:unit:jest:localization -- --reporters=jest-junit
npm run test:unit:tap -- --output-file ./test/results/unit-raw.tap
npm run test:unit:convertReportToXunit
- &setup_python
name: "setup python"
command: |
curl https://bootstrap.pypa.io/pip/3.5/get-pip.py -o get-pip.py
python3 get-pip.py pip==21.0.1
pip install s3cmd==2.1.0
- &deploy
name: "deploy"
command: |
npm run deploy
- &integration
name: "integration tests with Jest"
command: |
JEST_JUNIT_OUTPUT_NAME=integration-jest-results.xml npm run test:integration:remote -- --reporters=jest-junit
- &build_no_deploy
<<: *defaults
resource_class: large
steps:
- checkout
- run:
<<: *setup
- run:
<<: *lint
- run:
<<: *build
- run:
<<: *unit
- store_test_results:
path: test/results
- &build_and_deploy
<<: *defaults
resource_class: large
steps:
- checkout
- run:
<<: *setup
- run:
<<: *lint
- run:
<<: *build
- run:
<<: *unit
- run:
<<: *setup_python
- run:
<<: *deploy
- store_test_results:
path: test/results
- run:
name: Compress Artifacts
command: tar -cvzf build.tar build
- store_artifacts:
path: build.tar
- &integration_tests_and_store
<<: *defaults
resource_class: large
steps:
- checkout
- run:
<<: *setup
- run:
<<: *integration
- store_test_results:
path: test/results
- &update-translations
<<: *defaults
steps:
- checkout
- run:
name: "setup"
command: npm --production=false ci
- run:
name: "run i18n script"
command: npm run i18n:push
# build-test-deploy requires two separately named jobs
jobs:
build-and-deploy-staging:
<<: *build_and_deploy
build-and-deploy-production:
<<: *build_and_deploy
integration-tests:
<<: *integration_tests_and_store
update-translations:
<<: *update-translations
build-no-deploy:
<<: *build_no_deploy
workflows:
build-test-deploy:
jobs:
- build-and-deploy-staging:
context:
- scratch-www-all
- scratch-www-staging
- dockerhub-credentials
filters:
branches:
only:
- develop
- beta
- /^hotfix\/.*/
- /^release\/.*/
- integration-tests:
requires:
- build-and-deploy-staging
context:
- scratch-www-all
- scratch-www-staging
- dockerhub-credentials
filters:
branches:
only:
- develop
- beta
- /^hotfix\/.*/
- /^release\/.*/
- build-and-deploy-production:
context:
- scratch-www-all
- scratch-www-production
- dockerhub-credentials
filters:
branches:
only:
- master
- integration-tests:
requires:
- build-and-deploy-production
context:
- scratch-www-all
- scratch-www-production
- dockerhub-credentials
filters:
branches:
only:
- master
Update-translations:
triggers:
- schedule: # every evening at 7pm EST (8pm EDT, Midnight UTC)
cron: "0 0 * * *"
filters:
branches:
only: develop
jobs:
- update-translations:
context:
- scratch-www-all
- scratch-www-staging
- dockerhub-credentials
filters:
branches:
only:
- develop
build-test-no-deploy:
jobs:
- build-no-deploy:
context:
- dockerhub-credentials
filters:
branches:
ignore:
- develop
- master
- beta
- /^hotfix\/.*/
- /^release\/.*/

138
.github/workflows/ci-cd.yml vendored Normal file
View file

@ -0,0 +1,138 @@
name: CI/CD
on:
pull_request: # Runs whenever a pull request is created or updated
push: # Runs whenever a commit is pushed to the repository
branches: [master, develop, beta, hotfix/*] # ...on any of these branches
workflow_dispatch: # Allows you to run this workflow manually from the Actions tab
concurrency:
group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
cancel-in-progress: true
env:
CXX: g++-4.8
FASTLY_ACTIVATE_CHANGES: true
FASTLY_PURGE_ALL: true
NODE_ENV: production
SKIP_CLEANUP: true
jobs:
build-and-test-and-maybe-deploy:
runs-on: ubuntu-latest
environment: >-
${{
(
(github.ref == 'refs/heads/master') && 'production'
) ||
(
(
(github.ref == 'refs/heads/develop') ||
(github.ref == 'refs/heads/beta') ||
startsWith(github.ref, 'refs/heads/hotfix/') ||
startsWith(github.ref, 'refs/heads/release/')
) && 'staging'
) ||
''
}}
env:
# SCRATCH_ENV comes from the GitHub Environment
# See https://github.com/scratchfoundation/scratch-www/settings/variables/actions
SCRATCH_SHOULD_DEPLOY: ${{ vars.SCRATCH_ENV != '' }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: 'npm'
node-version-file: '.nvmrc'
- name: info
run: |
echo "Scratch environment: ${{ vars.SCRATCH_ENV }}"
echo "Node version: $(node --version)"
echo "NPM version: $(npm --version)"
- name: setup
run: |
npm --production=false ci
mkdir -p ./test/results
- name: lint
run: npm run test:lint:ci
- name: build
run: WWW_VERSION=${GITHUB_SHA:0:5} npm run build
env:
# webpack.config.js uses these with `DefinePlugin`
API_HOST: ${{ secrets.API_HOST }}
RECAPTCHA_SITE_KEY: ${{ secrets.RECAPTCHA_SITE_KEY }}
ASSET_HOST: ${{ secrets.ASSET_HOST }}
BACKPACK_HOST: ${{ secrets.BACKPACK_HOST }}
CLOUDDATA_HOST: ${{ secrets.CLOUDDATA_HOST }}
PROJECT_HOST: ${{ secrets.PROJECT_HOST }}
STATIC_HOST: ${{ secrets.STATIC_HOST }}
SCRATCH_ENV: ${{ vars.SCRATCH_ENV }}
# used by src/template-config.js
GTM_ID: ${{ secrets.GTM_ID }}
GTM_ENV_AUTH: ${{ secrets.GTM_ENV_AUTH }}
- name: unit tests
run: |
JEST_JUNIT_OUTPUT_NAME=unit-jest-results.xml npm run test:unit:jest:unit -- --reporters=jest-junit
JEST_JUNIT_OUTPUT_NAME=localization-jest-results.xml npm run test:unit:jest:localization -- --reporters=jest-junit
npm run test:unit:tap -- --output-file ./test/results/unit-raw.tap
npm run test:unit:convertReportToXunit
- name: setup Python
if: ${{ env.SCRATCH_SHOULD_DEPLOY == 'true' }}
run: |
curl https://bootstrap.pypa.io/pip/3.5/get-pip.py -o get-pip.py
python3 get-pip.py pip==21.0.1
pip install s3cmd==2.3.0
- name: deploy
if: ${{ env.SCRATCH_SHOULD_DEPLOY == 'true' }}
run: npm run deploy
env:
S3_LOCAL_DIR: build
S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
FASTLY_API_KEY: ${{ secrets.FASTLY_API_KEY }}
FASTLY_SERVICE_ID: ${{ secrets.FASTLY_SERVICE_ID }}
SLACK_WEBHOOK_CIRCLECI_NOTIFICATIONS: ${{ secrets.SLACK_WEBHOOK_CIRCLECI_NOTIFICATIONS }} # TODO: rename or replace
SLACK_WEBHOOK_ENGINEERING: ${{ secrets.SLACK_WEBHOOK_ENGINEERING }}
SLACK_WEBHOOK_MODS: ${{ secrets.SLACK_WEBHOOK_MODS }}
- name: integration tests
if: ${{ env.SCRATCH_SHOULD_DEPLOY == 'true' }}
run: |
# if the health test fails, there's no point in trying to run the integration tests
npm run test:health
# health test succeeded, so proceed with integration tests
JEST_JUNIT_OUTPUT_NAME=integration-jest-results.xml npm run test:integration -- --reporters=jest-junit
env:
ROOT_URL: ${{ secrets.ROOT_URL }}
# test/integration-legacy/selenium-helpers.js
CI: "true"
CIRCLECI: "true" # TODO
CIRCLE_BUILD_NUM: ${{ github.run_id }} # TODO
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SMOKE_REMOTE: "true" # use Sauce Labs
# test/integration/*
SMOKE_USERNAME: ${{ secrets.SMOKE_USERNAME }}
SMOKE_PASSWORD: ${{ secrets.SMOKE_PASSWORD }}
COMMENT_PROJECT_ID: ${{ secrets.COMMENT_PROJECT_ID }}
COMMENT_STUDIO_ID: ${{ secrets.COMMENT_STUDIO_ID }}
UNOWNED_SHARED_PROJECT_ID: ${{ secrets.UNOWNED_SHARED_PROJECT_ID }}
OWNED_SHARED_PROJECT_ID: ${{ secrets.OWNED_SHARED_PROJECT_ID }}
OWNED_UNSHARED_PROJECT_ID: ${{ secrets.OWNED_UNSHARED_PROJECT_ID }}
UNOWNED_UNSHARED_PROJECT_ID: ${{ secrets.UNOWNED_UNSHARED_PROJECT_ID }}
UNOWNED_SHARED_SCRATCH2_PROJECT_ID: ${{ secrets.UNOWNED_SHARED_SCRATCH2_PROJECT_ID }}
OWNED_UNSHARED_SCRATCH2_PROJECT_ID: ${{ secrets.OWNED_UNSHARED_SCRATCH2_PROJECT_ID }}
TEST_STUDIO_ID: ${{ secrets.TEST_STUDIO_ID }}
RATE_LIMIT_CHECK: ${{ secrets.RATE_LIMIT_CHECK }}
- name: compress artifact
if: ${{ env.SCRATCH_SHOULD_DEPLOY == 'true' }}
run: tar -czvf build.tgz build
- name: upload artifact
if: ${{ env.SCRATCH_SHOULD_DEPLOY == 'true' }}
uses: actions/upload-artifact@v3
with:
path: build.tgz

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
v16

425
package-lock.json generated
View file

@ -21,7 +21,7 @@
"react-twitter-embed": "^3.0.3",
"react-use": "^17.3.1",
"scratch-parser": "5.1.1",
"scratch-storage": "2.2.1"
"scratch-storage": "2.3.1"
},
"devDependencies": {
"@formatjs/intl-datetimeformat": "6.4.3",
@ -53,6 +53,7 @@
"enzyme-adapter-react-16": "1.14.0",
"eslint": "5.16.0",
"eslint-config-scratch": "7.0.0",
"eslint-plugin-jest": "24.7.0",
"eslint-plugin-json": "2.0.1",
"eslint-plugin-react": "7.14.2",
"eslint-plugin-react-hooks": "4.2.0",
@ -79,7 +80,7 @@
"minilog": "2.0.8",
"pako": "0.2.8",
"plotly.js": "1.47.4",
"postcss": "8.4.6",
"postcss": "8.4.31",
"postcss-loader": "4.2.0",
"prop-types": "15.6.0",
"query-string": "5.1.1",
@ -101,8 +102,8 @@
"regenerator-runtime": "0.13.9",
"sass": "1.49.7",
"sass-loader": "10.4.1",
"scratch-gui": "3.0.16",
"scratch-l10n": "3.16.20231003032155",
"scratch-gui": "3.2.37",
"scratch-l10n": "3.16.20231024152916",
"selenium-webdriver": "4.1.0",
"slick-carousel": "1.6.0",
"style-loader": "0.12.3",
@ -1888,9 +1889,9 @@
"dev": true
},
"node_modules/@scratch/paper": {
"version": "0.11.20200728195508",
"resolved": "https://registry.npmjs.org/@scratch/paper/-/paper-0.11.20200728195508.tgz",
"integrity": "sha512-cphYw/y/l36UJ8fv/LXyK+lHlxMXtoydJUsgA4u5QnaUaSZYepuSHik6PewJGT4qvaPwT5ImvHWwX2kElWXvoQ==",
"version": "0.11.20221201200345",
"resolved": "https://registry.npmjs.org/@scratch/paper/-/paper-0.11.20221201200345.tgz",
"integrity": "sha512-I3BZNrHeaQJt2H6TD7HGsuBKJPDDF/BIDOaRvnN7Gj/QBRvpSaRK8JAmzcrRHZ+AqNtKrG50eOkS/acMjTw3rw==",
"dev": true,
"engines": {
"node": ">=8.0.0"
@ -2073,6 +2074,224 @@
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/experimental-utils": {
"version": "4.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz",
"integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.7",
"@typescript-eslint/scope-manager": "4.33.0",
"@typescript-eslint/types": "4.33.0",
"@typescript-eslint/typescript-estree": "4.33.0",
"eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0"
},
"engines": {
"node": "^10.12.0 || >=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "*"
}
},
"node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"dev": true,
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-utils": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
"integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
"dev": true,
"dependencies": {
"eslint-visitor-keys": "^2.0.0"
},
"engines": {
"node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
},
"funding": {
"url": "https://github.com/sponsors/mysticatea"
},
"peerDependencies": {
"eslint": ">=5"
}
},
"node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-visitor-keys": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "4.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz",
"integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "4.33.0",
"@typescript-eslint/visitor-keys": "4.33.0"
},
"engines": {
"node": "^8.10.0 || ^10.13.0 || >=11.10.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/types": {
"version": "4.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz",
"integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==",
"dev": true,
"engines": {
"node": "^8.10.0 || ^10.13.0 || >=11.10.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "4.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz",
"integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "4.33.0",
"@typescript-eslint/visitor-keys": "4.33.0",
"debug": "^4.3.1",
"globby": "^11.0.3",
"is-glob": "^4.0.1",
"semver": "^7.3.5",
"tsutils": "^3.21.0"
},
"engines": {
"node": "^10.12.0 || >=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "4.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz",
"integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "4.33.0",
"eslint-visitor-keys": "^2.0.0"
},
"engines": {
"node": "^8.10.0 || ^10.13.0 || >=11.10.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/@vernier/godirect": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@vernier/godirect/-/godirect-1.5.0.tgz",
@ -8550,6 +8769,27 @@
"eslint": ">=5.3.0"
}
},
"node_modules/eslint-plugin-jest": {
"version": "24.7.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.7.0.tgz",
"integrity": "sha512-wUxdF2bAZiYSKBclsUMrYHH6WxiBreNjyDxbRv345TIvPeoCEgPNEn3Sa+ZrSqsf1Dl9SqqSREXMHExlMMu1DA==",
"dev": true,
"dependencies": {
"@typescript-eslint/experimental-utils": "^4.0.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@typescript-eslint/eslint-plugin": ">= 4",
"eslint": ">=5"
},
"peerDependenciesMeta": {
"@typescript-eslint/eslint-plugin": {
"optional": true
}
}
},
"node_modules/eslint-plugin-json": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-json/-/eslint-plugin-json-2.0.1.tgz",
@ -8874,16 +9114,24 @@
}
},
"node_modules/esrecurse": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
"integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dependencies": {
"estraverse": "^4.1.0"
"estraverse": "^5.2.0"
},
"engines": {
"node": ">=4.0"
}
},
"node_modules/esrecurse/node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"engines": {
"node": ">=4.0"
}
},
"node_modules/estraverse": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
@ -17409,10 +17657,16 @@
}
},
"node_modules/nanoid": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
"integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==",
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@ -19157,21 +19411,31 @@
}
},
"node_modules/postcss": {
"version": "8.4.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz",
"integrity": "sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==",
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"nanoid": "^3.2.0",
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
}
},
"node_modules/postcss-import": {
@ -23293,9 +23557,9 @@
}
},
"node_modules/scratch-audio": {
"version": "0.1.0-prerelease.20221123180128",
"resolved": "https://registry.npmjs.org/scratch-audio/-/scratch-audio-0.1.0-prerelease.20221123180128.tgz",
"integrity": "sha512-yj7mSkHmKxQVdBalAew1UjfTthOoTs3pejhh8Tl65KcC3X4+fLbmY1F3woYSrZbXxPtYUZTKyrcwlCh8r1DQjg==",
"version": "0.1.0-prerelease.20231013154641",
"resolved": "https://registry.npmjs.org/scratch-audio/-/scratch-audio-0.1.0-prerelease.20231013154641.tgz",
"integrity": "sha512-QHpN38xjm8v5KTTXDIfrW0MKPYw+R7y9gt5r6tzmah3SW8TPnk82w/XZ8zwgrVJcuzv0lu7/CTZUJv4XxK11Jw==",
"dev": true,
"dependencies": {
"audio-context": "1.0.1",
@ -23319,21 +23583,39 @@
}
},
"node_modules/scratch-blocks": {
"version": "0.2.0-prerelease.20231003094735",
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.2.0-prerelease.20231003094735.tgz",
"integrity": "sha512-TgaVZpZVVXcKCyvBZUnd+E2y6oHuKe6hNQsn5aUf1C9B+HWT+h7bUQRRfbg50n9VWrrAzBT0bGGCsSLhB7Dfnw==",
"version": "0.2.0-prerelease.20231013132110",
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.2.0-prerelease.20231013132110.tgz",
"integrity": "sha512-3ShqtHYHM0LmGcEb0wXNvIsPKI5iauEghzdJeXxxDUOElq1XG6I6iO1+3j5BUxD3HwfuG3LlG9VpfyKz67vnJQ==",
"dev": true,
"dependencies": {
"exports-loader": "0.7.0",
"google-closure-library": "20190301.0.0",
"imports-loader": "0.8.0",
"scratch-l10n": "3.16.20231003032155"
"scratch-l10n": "3.16.20231013034330"
}
},
"node_modules/scratch-blocks/node_modules/scratch-l10n": {
"version": "3.16.20231013034330",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.16.20231013034330.tgz",
"integrity": "sha512-zHiSFijBt5Dq7ceLIxEhXvgDn997r3HVXkGjMSQmyA4oyzYwCOVvzoxtu5U2UrxNEcJy7beFLfIn5GrV66/PvQ==",
"dev": true,
"dependencies": {
"@babel/cli": "^7.1.2",
"@babel/core": "^7.1.2",
"@transifex/api": "4.2.5",
"babel-plugin-react-intl": "^3.0.1",
"download": "^8.0.0",
"transifex": "1.6.6"
},
"bin": {
"build-i18n-src": "scripts/build-i18n-src.js",
"tx-push-src": "scripts/tx-push-src.js"
}
},
"node_modules/scratch-gui": {
"version": "3.0.16",
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-3.0.16.tgz",
"integrity": "sha512-AkI9vkxmSs/p8gslG6OidxNT5oMXmDaH/j4mLC2WzrVcCE2hWVG1NfxjZE+dKgW6e13JBBIz63sCDCTT+N6iqg==",
"version": "3.2.37",
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-3.2.37.tgz",
"integrity": "sha512-WbSrLycsE87/TqAtbbCW6F/ZgXV+AyDUTBs9/Y9KTzc0FURwDVTgdilnWY7pgj3sfHaRtCpLRTFAKjM5heRi8g==",
"dev": true,
"dependencies": {
"@microbit/microbit-universal-hex": "0.2.2",
@ -23387,15 +23669,15 @@
"react-virtualized": "9.20.1",
"redux": "3.7.2",
"redux-throttle": "0.1.1",
"scratch-audio": "0.1.0-prerelease.20221123180128",
"scratch-blocks": "0.2.0-prerelease.20231003094735",
"scratch-l10n": "3.16.20231003032155",
"scratch-paint": "2.1.21",
"scratch-render": "0.1.0-prerelease.20230913153807",
"scratch-render-fonts": "1.0.0-prerelease.20221102164332",
"scratch-storage": "2.2.1",
"scratch-svg-renderer": "0.2.0-prerelease.20230710144521",
"scratch-vm": "2.0.5",
"scratch-audio": "0.1.0-prerelease.20231013154641",
"scratch-blocks": "0.2.0-prerelease.20231013132110",
"scratch-l10n": "3.16.20231024152916",
"scratch-paint": "2.1.34",
"scratch-render": "0.1.0-prerelease.20231018030724",
"scratch-render-fonts": "1.0.0-prerelease.20231017225105",
"scratch-storage": "2.3.1",
"scratch-svg-renderer": "0.2.0-prerelease.20231013154115",
"scratch-vm": "2.1.14",
"startaudiocontext": "1.2.1",
"style-loader": "^0.23.0",
"text-encoding": "0.7.0",
@ -23715,12 +23997,12 @@
}
},
"node_modules/scratch-gui/node_modules/scratch-paint": {
"version": "2.1.21",
"resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-2.1.21.tgz",
"integrity": "sha512-9vjWDZEAjTlnWN1TWKOvH3DAPcznY4+EnPiUpviGVhpeFvlye57Hrj/OorYPmJ65lwNg2NeY6evKU2mUGRJDXQ==",
"version": "2.1.34",
"resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-2.1.34.tgz",
"integrity": "sha512-SoyaJF4HRJb3Rats5owRg8NV0zdxIoec0z4VR7177WeN1SoVHXvEPrHAqr3A1JSkJ6aG2ssoBjdppkxebyBtmA==",
"dev": true,
"dependencies": {
"@scratch/paper": "0.11.20200728195508",
"@scratch/paper": "0.11.20221201200345",
"classnames": "2.2.5",
"keymirror": "0.1.1",
"lodash.bindall": "4.4.0",
@ -23803,9 +24085,9 @@
}
},
"node_modules/scratch-l10n": {
"version": "3.16.20231003032155",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.16.20231003032155.tgz",
"integrity": "sha512-tDqWskv/CJGzLrFr0pUVtHoima+BSYKpLf5A0UzAwMInOgQ0yzjZiyDsuTKpGpkiiyVgns6m7QsBHvMlCv5uTQ==",
"version": "3.16.20231024152916",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.16.20231024152916.tgz",
"integrity": "sha512-dg1am1tHdRBshQvEYMgcveouVsNQEIW1sd4giAQIN57rE/PKBgER3R0Zc3fqZxymd1JL2zIpkYB8bkDZRFadWA==",
"dev": true,
"dependencies": {
"@babel/cli": "^7.1.2",
@ -23844,9 +24126,9 @@
}
},
"node_modules/scratch-render": {
"version": "0.1.0-prerelease.20230913153807",
"resolved": "https://registry.npmjs.org/scratch-render/-/scratch-render-0.1.0-prerelease.20230913153807.tgz",
"integrity": "sha512-A2bGt7/92VEoI9n64SHtz8jYc/6EEqfBLzVAYrOom4MOgu1aYYOw6eBi6sGkDdaCIHZIdNzS7k4L3lxDXYiHHw==",
"version": "0.1.0-prerelease.20231018030724",
"resolved": "https://registry.npmjs.org/scratch-render/-/scratch-render-0.1.0-prerelease.20231018030724.tgz",
"integrity": "sha512-XNn0tMEqOOy1UlbGpvw8qwhqEmYotB1uVe5RuKp7Om5QWNgKh3x3lyyQsgBOcGGR1YSrfFFUj/hagBm/bipuXw==",
"dev": true,
"dependencies": {
"grapheme-breaker": "0.3.2",
@ -23856,7 +24138,7 @@
"minilog": "3.1.0",
"raw-loader": "^0.5.1",
"scratch-storage": "^1.0.0",
"scratch-svg-renderer": "0.2.0-prerelease.20230710144521",
"scratch-svg-renderer": "0.2.0-prerelease.20231013154115",
"twgl.js": "4.4.0"
},
"peerDependencies": {
@ -23864,9 +24146,9 @@
}
},
"node_modules/scratch-render-fonts": {
"version": "1.0.0-prerelease.20221102164332",
"resolved": "https://registry.npmjs.org/scratch-render-fonts/-/scratch-render-fonts-1.0.0-prerelease.20221102164332.tgz",
"integrity": "sha512-22MbRDGUSArVEoHatg5rt7f/H0wWhMrcyN6HD0OQJeDqdlO3qSSX9/qvdzNJGYWwZkhrdJWcI5JGD1YuJfefmw==",
"version": "1.0.0-prerelease.20231017225105",
"resolved": "https://registry.npmjs.org/scratch-render-fonts/-/scratch-render-fonts-1.0.0-prerelease.20231017225105.tgz",
"integrity": "sha512-Ske5+x9OzfT7wf+eAnMQHutCzyle1er3ncywPMLPC6UDjKrlHYUFVNeTR3vJb3EoMdBNPl/yVKuJItGPQoUugg==",
"dev": true,
"dependencies": {
"base64-loader": "1.0.0"
@ -23934,9 +24216,9 @@
}
},
"node_modules/scratch-storage": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-2.2.1.tgz",
"integrity": "sha512-qogGcWBXqKUHgfvSgyUkos4fuj7z+SDDHBVlT3NNC4gtZgw4dq+USwHjKXCwtRs6BN/joI7+LafFJtSLii6G/w==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-2.3.1.tgz",
"integrity": "sha512-kBxJLFGQsimP35YHig+8op5V9+FLfO7mXbhSLj4712dyHTXvyMKYY8GAf7KT283kQrUpDApehJU4Jx5PXSjLXQ==",
"dependencies": {
"@babel/runtime": "7.21.0",
"arraybuffer-loader": "^1.0.3",
@ -23967,9 +24249,9 @@
}
},
"node_modules/scratch-svg-renderer": {
"version": "0.2.0-prerelease.20230710144521",
"resolved": "https://registry.npmjs.org/scratch-svg-renderer/-/scratch-svg-renderer-0.2.0-prerelease.20230710144521.tgz",
"integrity": "sha512-T+jmJGZWeBEAtIsV+6WpAzglANRaUJVxQLIKoD68VhD142l6XBf5WwHJiJkK9vysp6H24zchwTZwq3R34NF14Q==",
"version": "0.2.0-prerelease.20231013154115",
"resolved": "https://registry.npmjs.org/scratch-svg-renderer/-/scratch-svg-renderer-0.2.0-prerelease.20231013154115.tgz",
"integrity": "sha512-z487RJ9bxGsTeaoAhzdlKb/K6SdeiaL8fVhRK1JYZNDzJYbrxxm3N3PVN79tHBEY0x/gFizpCi1CNNNMlyVHAg==",
"dev": true,
"dependencies": {
"base64-js": "1.2.1",
@ -24006,9 +24288,9 @@
"dev": true
},
"node_modules/scratch-vm": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-2.0.5.tgz",
"integrity": "sha512-g1PVPsq4QWF7RLDLfcNeweTTpzfqlix8YVMgeKWItvpZtl4lpPaqfAoqPYQpPRduIP0fFIi4GT6OV9dsZMOBqw==",
"version": "2.1.14",
"resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-2.1.14.tgz",
"integrity": "sha512-tkoCYtJediovJN3CSd667/NtU3RLDFnv2vpfb6stwhXRUAfdjcOSmEB2QsdwhquED277gHmKTCPFM1hOJhqipQ==",
"dev": true,
"dependencies": {
"@vernier/godirect": "1.5.0",
@ -29309,6 +29591,21 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
},
"node_modules/tsutils": {
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
"integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
"dev": true,
"dependencies": {
"tslib": "^1.8.1"
},
"engines": {
"node": ">= 6"
},
"peerDependencies": {
"typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
}
},
"node_modules/tty-browserify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",

View file

@ -7,8 +7,8 @@
"test": "npm run test:lint && npm run build && npm run test:unit",
"test:lint": "eslint . --ext .js,.jsx,.json",
"test:lint:ci": "eslint . --ext .js,.jsx,.json --format junit -o ./test/results/lint-results.xml",
"test:health": "jest ./test/health/*.test.js",
"test:integration": "jest ./test/integration/*.test.js --reporters=default --maxWorkers=5",
"test:integration:remote": "SMOKE_REMOTE=true jest ./test/integration/*.test.js --reporters=default --maxWorkers=5",
"test:unit": "npm run test:unit:jest && npm run test:unit:tap",
"test:unit:jest": "npm run test:unit:jest:unit && npm run test:unit:jest:localization",
"test:unit:jest:unit": "jest ./test/unit/ --reporters=default",
@ -56,7 +56,7 @@
"react-twitter-embed": "^3.0.3",
"react-use": "^17.3.1",
"scratch-parser": "5.1.1",
"scratch-storage": "2.2.1"
"scratch-storage": "2.3.1"
},
"devDependencies": {
"@formatjs/intl-datetimeformat": "6.4.3",
@ -88,6 +88,7 @@
"enzyme-adapter-react-16": "1.14.0",
"eslint": "5.16.0",
"eslint-config-scratch": "7.0.0",
"eslint-plugin-jest": "24.7.0",
"eslint-plugin-json": "2.0.1",
"eslint-plugin-react": "7.14.2",
"eslint-plugin-react-hooks": "4.2.0",
@ -114,7 +115,7 @@
"minilog": "2.0.8",
"pako": "0.2.8",
"plotly.js": "1.47.4",
"postcss": "8.4.6",
"postcss": "8.4.31",
"postcss-loader": "4.2.0",
"prop-types": "15.6.0",
"query-string": "5.1.1",
@ -136,8 +137,8 @@
"regenerator-runtime": "0.13.9",
"sass": "1.49.7",
"sass-loader": "10.4.1",
"scratch-gui": "3.0.16",
"scratch-l10n": "3.16.20231003032155",
"scratch-gui": "3.2.37",
"scratch-l10n": "3.16.20231024152916",
"selenium-webdriver": "4.1.0",
"slick-carousel": "1.6.0",
"style-loader": "0.12.3",

View file

@ -1,6 +1,15 @@
module.exports = {
extends: ['scratch/react'],
extends: ['scratch/react', 'scratch/es6', 'plugin:jest/recommended'],
env: {
jest: true
},
rules: {
'jest/no-done-callback': 'off', // TODO: convert callback-based tests to async/await
'no-confusing-arrow': [
'error',
{
allowParens: true
}
],
}
};

View file

@ -0,0 +1,51 @@
/* eslint-disable no-console */
// this basic server health check is meant to be run before integration tests
// it should be run with the same environment variables as the integration tests
// and operate in the same way as the integration tests
const SeleniumHelper = require('../integration/selenium-helpers.js');
const rootUrl = process.env.ROOT_URL || (() => {
const ROOT_URL_DEFAULT = 'https://scratch.ly';
console.warn(`ROOT_URL not set, defaulting to ${ROOT_URL_DEFAULT}`);
return ROOT_URL_DEFAULT;
})();
jest.setTimeout(60000);
describe('www server health check', () => {
/** @type {import('selenium-webdriver').ThenableWebDriver} */
let driver;
/** @type {SeleniumHelper} */
let seleniumHelper;
beforeAll(() => {
seleniumHelper = new SeleniumHelper();
driver = seleniumHelper.buildDriver('www server health check');
});
afterAll(() => driver.quit());
test('server is healthy', async () => {
const healthUrl = new URL('health/', rootUrl);
await driver.get(healthUrl.toString());
// Note: driver.getPageSource() will return the pretty HTML form of the JSON
const pageText = await driver.executeScript('return document.body.innerText');
let healthObject;
let serverReturnedValidJson = false;
try {
healthObject = JSON.parse(pageText);
serverReturnedValidJson = true;
} catch (_e) {
// ignore
}
expect(serverReturnedValidJson).toBe(true);
expect(healthObject).toHaveProperty('healthy', true);
});
});

View file

@ -7,19 +7,17 @@ import {createIntl, IntlProvider} from 'react-intl';
import {mount, shallow} from 'enzyme';
import intlShape from '../../src/lib/intl-shape';
const shallowWithIntl = (node, {context} = {}) => {
return shallow(
node,
{
context: Object.assign({}, context),
wrappingComponent: IntlProvider,
wrappingComponentProps: {
locale: 'en',
messages: {}
}
const shallowWithIntl = (node, {context} = {}) => shallow(
node,
{
context: Object.assign({}, context),
wrappingComponent: IntlProvider,
wrappingComponentProps: {
locale: 'en',
messages: {}
}
).dive();
};
}
).dive();
const mountWithIntl = (node, {context, childContextTypes} = {}) => {
const intl = createIntl({locale: 'en', messages: {}});

View file

@ -36,8 +36,8 @@ class SeleniumHelper {
if (remote === 'true'){
let nameToUse;
if (ci === 'true'){
let ciName = usingCircle ? 'circleCi ' : 'unknown ';
nameToUse = ciName + buildID + ' : ' + name;
const ciName = usingCircle ? 'circleCi ' : 'unknown ';
nameToUse = `${ciName + buildID} : ${name}`;
} else {
nameToUse = name;
}
@ -50,14 +50,14 @@ class SeleniumHelper {
getDriver () {
const chromeCapabilities = webdriver.Capabilities.chrome();
let args = [];
const args = [];
if (headless) {
args.push('--headless');
args.push('window-size=1024,1680');
args.push('--no-sandbox');
}
chromeCapabilities.set('chromeOptions', {args});
let driver = new webdriver.Builder()
const driver = new webdriver.Builder()
.forBrowser('chrome')
.withCapabilities(chromeCapabilities)
.build();
@ -67,12 +67,12 @@ class SeleniumHelper {
getSauceDriver (username, accessKey, name) {
// Driver configs can be generated with the Sauce Platform Configurator
// https://wiki.saucelabs.com/display/DOCS/Platform+Configurator
let driverConfig = {
const driverConfig = {
browserName: 'chrome',
platform: 'macOS 10.14',
version: '76.0'
};
var driver = new webdriver.Builder()
const driver = new webdriver.Builder()
.withCapabilities({
browserName: driverConfig.browserName,
platform: driverConfig.platform,
@ -128,13 +128,13 @@ class SeleniumHelper {
}
dragFromXpathToXpath (startXpath, endXpath) {
return this.findByXpath(startXpath).then(startEl => {
return this.findByXpath(endXpath).then(endEl => {
return this.driver.actions()
return this.findByXpath(startXpath).then(startEl =>
this.findByXpath(endXpath).then(endEl =>
this.driver.actions()
.dragAndDrop(startEl, endEl)
.perform();
});
});
.perform()
)
);
}
urlMatches (regex) {
@ -145,24 +145,22 @@ class SeleniumHelper {
return this.driver.manage()
.logs()
.get('browser')
.then((entries) => {
return entries.filter((entry) => {
const message = entry.message;
for (let i = 0; i < whitelist.length; i++) {
if (message.indexOf(whitelist[i]) !== -1) {
// eslint-disable-next-line no-console
// console.warn('Ignoring whitelisted error: ' + whitelist[i]);
return false;
} else if (entry.level !== 'SEVERE') {
// eslint-disable-next-line no-console
// console.warn('Ignoring non-SEVERE entry: ' + message);
return false;
}
return true;
.then(entries => entries.filter(entry => {
const message = entry.message;
for (let i = 0; i < whitelist.length; i++) {
if (message.indexOf(whitelist[i]) !== -1) {
// eslint-disable-next-line no-console
// console.warn('Ignoring whitelisted error: ' + whitelist[i]);
return false;
} else if (entry.level !== 'SEVERE') {
// eslint-disable-next-line no-console
// console.warn('Ignoring non-SEVERE entry: ' + message);
return false;
}
return true;
});
});
}
return true;
}));
}
}

View file

@ -9,13 +9,13 @@ module.exports.constants = {
};
module.exports.fillUsernameSlide = function (driver, seleniumWebdriver) {
var passwordInput = driver.findElement(seleniumWebdriver.By.name('user.password'));
var usernameInput = driver.findElement(seleniumWebdriver.By.name('user.username'));
var usernamePromise = usernameInput.sendKeys('clipspringer');
var passwordPromise = passwordInput.sendKeys('educators');
var nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(module.exports.constants.nextStepXpath));
return Promise.all([usernamePromise, passwordPromise]).then(function () { // eslint-disable-line no-undef
nextStepButton.click().then(function () {
const passwordInput = driver.findElement(seleniumWebdriver.By.name('user.password'));
const usernameInput = driver.findElement(seleniumWebdriver.By.name('user.username'));
const usernamePromise = usernameInput.sendKeys('clipspringer');
const passwordPromise = passwordInput.sendKeys('educators');
const nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(module.exports.constants.nextStepXpath));
return Promise.all([usernamePromise, passwordPromise]).then(() => { // eslint-disable-line no-undef
nextStepButton.click().then(() => {
driver.wait(seleniumWebdriver.until
.elementLocated(seleniumWebdriver.By.className('demographics-step')));
});
@ -23,13 +23,13 @@ module.exports.fillUsernameSlide = function (driver, seleniumWebdriver) {
};
module.exports.fillDemographicsSlide = function (driver, seleniumWebdriver) {
var clickMaleInput = driver.findElement(seleniumWebdriver.By.xpath('//input[@value="male"' +
const clickMaleInput = driver.findElement(seleniumWebdriver.By.xpath('//input[@value="male"' +
'and @type="radio"]')).click();
var selectCountry = driver.findElement(seleniumWebdriver.By.xpath('//select[@name="user.country"]' +
const selectCountry = driver.findElement(seleniumWebdriver.By.xpath('//select[@name="user.country"]' +
'/option[@value="us"]')).click();
var nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(module.exports.constants.nextStepXpath));
return Promise.all([clickMaleInput, selectCountry]).then(function () { // eslint-disable-line no-undef
nextStepButton.click().then(function () {
const nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(module.exports.constants.nextStepXpath));
return Promise.all([clickMaleInput, selectCountry]).then(() => { // eslint-disable-line no-undef
nextStepButton.click().then(() => {
driver.wait(seleniumWebdriver.until
.elementLocated(seleniumWebdriver.By.className('name-step')));
});
@ -37,11 +37,11 @@ module.exports.fillDemographicsSlide = function (driver, seleniumWebdriver) {
};
module.exports.fillNameSlide = function (driver, seleniumWebdriver) {
var firstNamePromise = driver.findElement(seleniumWebdriver.By.name('user.name.first')).sendKeys('first');
var lastNamePromise = driver.findElement(seleniumWebdriver.By.name('user.name.last')).sendKeys('surname');
var nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(module.exports.constants.nextStepXpath));
return Promise.all([firstNamePromise, lastNamePromise]).then(function () { // eslint-disable-line no-undef
nextStepButton.click().then(function () {
const firstNamePromise = driver.findElement(seleniumWebdriver.By.name('user.name.first')).sendKeys('first');
const lastNamePromise = driver.findElement(seleniumWebdriver.By.name('user.name.last')).sendKeys('surname');
const nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(module.exports.constants.nextStepXpath));
return Promise.all([firstNamePromise, lastNamePromise]).then(() => { // eslint-disable-line no-undef
nextStepButton.click().then(() => {
driver.wait(seleniumWebdriver.until
.elementLocated(seleniumWebdriver.By.className('phone-step')));
});
@ -49,13 +49,13 @@ module.exports.fillNameSlide = function (driver, seleniumWebdriver) {
};
module.exports.fillPhoneSlide = function (driver, seleniumWebdriver) {
var phoneInput = driver.findElement(seleniumWebdriver.By.xpath('//input[@type="tel"]'));
var consentCheckbox = driver.findElement(seleniumWebdriver.By.name('phoneConsent'));
var nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(module.exports.constants.nextStepXpath));
var phoneNumberPromise = phoneInput.sendKeys('6172535960');
var consentPromise = consentCheckbox.click();
return Promise.all([phoneNumberPromise, consentPromise]).then(function () { // eslint-disable-line no-undef
nextStepButton.click().then(function () {
const phoneInput = driver.findElement(seleniumWebdriver.By.xpath('//input[@type="tel"]'));
const consentCheckbox = driver.findElement(seleniumWebdriver.By.name('phoneConsent'));
const nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(module.exports.constants.nextStepXpath));
const phoneNumberPromise = phoneInput.sendKeys('6172535960');
const consentPromise = consentCheckbox.click();
return Promise.all([phoneNumberPromise, consentPromise]).then(() => { // eslint-disable-line no-undef
nextStepButton.click().then(() => {
driver.wait(seleniumWebdriver.until
.elementLocated(seleniumWebdriver.By.className('organization-step')));
});
@ -63,16 +63,16 @@ module.exports.fillPhoneSlide = function (driver, seleniumWebdriver) {
};
module.exports.fillOrganizationSlide = function (driver, seleniumWebdriver) {
var organizationInput = driver.findElement(seleniumWebdriver.By.name('organization.name'));
var titleInput = driver.findElement(seleniumWebdriver.By.name('organization.title'));
var typeCheckbox = driver.findElement(seleniumWebdriver.By.xpath('//input[@type="checkbox" and @value="3"]'));
var nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(module.exports.constants.nextStepXpath));
var organizationPromise = organizationInput.sendKeys('MIT Media Lab');
var titlePromise = titleInput.sendKeys('Software Developer');
var typePromise = typeCheckbox.click();
const organizationInput = driver.findElement(seleniumWebdriver.By.name('organization.name'));
const titleInput = driver.findElement(seleniumWebdriver.By.name('organization.title'));
const typeCheckbox = driver.findElement(seleniumWebdriver.By.xpath('//input[@type="checkbox" and @value="3"]'));
const nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(module.exports.constants.nextStepXpath));
const organizationPromise = organizationInput.sendKeys('MIT Media Lab');
const titlePromise = titleInput.sendKeys('Software Developer');
const typePromise = typeCheckbox.click();
return Promise.all([organizationPromise, titlePromise, typePromise]) // eslint-disable-line no-undef
.then(function () {
nextStepButton.click().then(function () {
.then(() => {
nextStepButton.click().then(() => {
driver.wait(seleniumWebdriver.until
.elementLocated(seleniumWebdriver.By.className('address-step')));
});
@ -80,18 +80,18 @@ module.exports.fillOrganizationSlide = function (driver, seleniumWebdriver) {
};
module.exports.fillAddressSlide = function (driver, seleniumWebdriver) {
var addressInput = driver.findElement(seleniumWebdriver.By.name('address.line1'));
var cityInput = driver.findElement(seleniumWebdriver.By.name('address.city'));
var zipCodeInput = driver.findElement(seleniumWebdriver.By.name('address.zip'));
var nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(module.exports.constants.nextStepXpath));
var addressPromise = addressInput.sendKeys('77 Massachusetts Avenue, E14/E15');
var cityPromise = cityInput.sendKeys('Cambridge');
var statePromise = driver.findElement(seleniumWebdriver.By.xpath('//select[@name="address.state"]' +
const addressInput = driver.findElement(seleniumWebdriver.By.name('address.line1'));
const cityInput = driver.findElement(seleniumWebdriver.By.name('address.city'));
const zipCodeInput = driver.findElement(seleniumWebdriver.By.name('address.zip'));
const nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(module.exports.constants.nextStepXpath));
const addressPromise = addressInput.sendKeys('77 Massachusetts Avenue, E14/E15');
const cityPromise = cityInput.sendKeys('Cambridge');
const statePromise = driver.findElement(seleniumWebdriver.By.xpath('//select[@name="address.state"]' +
'/option[@value="us-ma"]')).click();
var zipPromise = zipCodeInput.sendKeys('02139');
const zipPromise = zipCodeInput.sendKeys('02139');
return Promise.all([addressPromise, cityPromise, statePromise, zipPromise]) // eslint-disable-line no-undef
.then(function () {
nextStepButton.click().then(function () {
.then(() => {
nextStepButton.click().then(() => {
driver.wait(seleniumWebdriver.until
.elementLocated(seleniumWebdriver.By.className('usescratch-step')));
});

View file

@ -4,27 +4,27 @@
* Test cases: https://github.com/LLK/scratch-www/wiki/Testing-Scratch-www#All_Test_Cases_Teacher_Join_Flow
*/
require('chromedriver');
var seleniumWebdriver = require('selenium-webdriver');
var tap = require('tap');
const seleniumWebdriver = require('selenium-webdriver');
const tap = require('tap');
var utils = require('./teacher_registration_utils.js');
var constants = utils.constants;
const utils = require('./teacher_registration_utils.js');
const constants = utils.constants;
// Set test url through environment variable
var rootUrl = process.env.ROOT_URL || 'http://localhost:8333';
const rootUrl = process.env.ROOT_URL || 'http://localhost:8333';
// chrome driver
var driver = new seleniumWebdriver.Builder().withCapabilities(seleniumWebdriver.Capabilities.chrome())
const driver = new seleniumWebdriver.Builder().withCapabilities(seleniumWebdriver.Capabilities.chrome())
.build();
tap.plan(2);
tap.tearDown(function () {
tap.tearDown(() => {
driver.quit();
});
tap.beforeEach(function () {
driver.get(rootUrl + '/educators/register');
driver.get(`${rootUrl}/educators/register`);
return utils.fillUsernameSlide(driver, seleniumWebdriver)
.then(utils.fillDemographicsSlide.bind(this, driver, seleniumWebdriver)) // eslint-disable-line no-invalid-this
.then(utils.fillNameSlide.bind(this, driver, seleniumWebdriver)) // eslint-disable-line no-invalid-this
@ -33,26 +33,26 @@ tap.beforeEach(function () {
});
// Selects Vatican City as the country, and checks that the state dropdown disappears
tap.test('checkStateDropdownOnlyPresentWhenNeeded', function (t) {
tap.test('checkStateDropdownOnlyPresentWhenNeeded', t => {
driver.findElement(seleniumWebdriver.By.xpath('//select[@name="address.country"]' +
'/option[@value="va"]')).click() // select Vatican City as the country
.then(function () {
.then(() => {
driver.findElements(seleniumWebdriver.By.name('address.state'))
.then(function (stateDropdown) {
.then(stateDropdown => {
t.equal(stateDropdown.length, 0);
t.end();
});
});
});
tap.test('checkZipCodeRequired', function (t) {
var nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
var errorMessageXPath = '//input[@name="address.zip"]/following-sibling::' +
tap.test('checkZipCodeRequired', t => {
const nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
const errorMessageXPath = '//input[@name="address.zip"]/following-sibling::' +
'span[@class="help-block validation-message"]/span[contains(text(),' +
'"This field is required")]';
nextStepButton.click().then(function () {
nextStepButton.click().then(() => {
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath))
.then(function (validationMessages) {
.then(validationMessages => {
t.equal(validationMessages.length, 1);
t.end();
});

View file

@ -4,41 +4,41 @@
* Test cases: https://github.com/LLK/scratch-www/wiki/Testing-Scratch-www#All_Test_Cases_Teacher_Join_Flow
*/
require('chromedriver');
var seleniumWebdriver = require('selenium-webdriver');
var tap = require('tap');
const seleniumWebdriver = require('selenium-webdriver');
const tap = require('tap');
var utils = require('./teacher_registration_utils.js');
var constants = utils.constants;
const utils = require('./teacher_registration_utils.js');
const constants = utils.constants;
// Set test url through environment variable
var rootUrl = process.env.ROOT_URL || 'http://localhost:8333';
const rootUrl = process.env.ROOT_URL || 'http://localhost:8333';
// chrome driver
var driver = new seleniumWebdriver.Builder().withCapabilities(seleniumWebdriver.Capabilities.chrome())
const driver = new seleniumWebdriver.Builder().withCapabilities(seleniumWebdriver.Capabilities.chrome())
.build();
tap.plan(2);
tap.tearDown(function () {
tap.tearDown(() => {
driver.quit();
});
tap.beforeEach(function () {
driver.get(rootUrl + '/educators/register');
tap.beforeEach(() => {
driver.get(`${rootUrl}/educators/register`);
return utils.fillUsernameSlide(driver, seleniumWebdriver);
});
// if the user selects the other gender option, they must input a gender
// selects the other gender option and attempt to advance the slide
tap.test('checkOtherGenderInput', function (t) {
var otherGenderRadio = driver.findElement(seleniumWebdriver.By.xpath('//input[@value="other"' +
tap.test('checkOtherGenderInput', t => {
const otherGenderRadio = driver.findElement(seleniumWebdriver.By.xpath('//input[@value="other"' +
'and @type="radio"]'));
var nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
const nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
driver.findElement(seleniumWebdriver.By.xpath('//select[@name="user.country"]/option[2]')).click();
otherGenderRadio.click().then(function () {
nextStepButton.click().then(function () {
otherGenderRadio.click().then(() => {
nextStepButton.click().then(() => {
driver.findElements(seleniumWebdriver.By.xpath(constants.generalErrorMessageXpath))
.then(function (validationMessages) {
.then(validationMessages => {
t.equal(validationMessages.length, 1);
t.end();
});
@ -48,12 +48,12 @@ tap.test('checkOtherGenderInput', function (t) {
// the user must select a gender
// tries to advance the slide without selecting a gender
tap.test('checkNoGenderInput', function (t) {
var nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
tap.test('checkNoGenderInput', t => {
const nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
driver.findElement(seleniumWebdriver.By.xpath('//select[@name="user.country"]/option[2]')).click();
nextStepButton.click().then(function () {
nextStepButton.click().then(() => {
driver.findElements(seleniumWebdriver.By.xpath(constants.generalErrorMessageXpath))
.then(function (validationMessages) {
.then(validationMessages => {
t.equal(validationMessages.length, 1);
t.end();
});

View file

@ -4,40 +4,40 @@
* Test cases: https://github.com/LLK/scratch-www/wiki/Testing-Scratch-www#All_Test_Cases_Teacher_Join_Flow
*/
require('chromedriver');
var seleniumWebdriver = require('selenium-webdriver');
var tap = require('tap');
const seleniumWebdriver = require('selenium-webdriver');
const tap = require('tap');
var utils = require('./teacher_registration_utils.js');
var constants = utils.constants;
const utils = require('./teacher_registration_utils.js');
const constants = utils.constants;
// Set test url through environment variable
var rootUrl = process.env.ROOT_URL || 'http://localhost:8333';
const rootUrl = process.env.ROOT_URL || 'http://localhost:8333';
// chrome driver
var driver = new seleniumWebdriver.Builder().withCapabilities(seleniumWebdriver.Capabilities.chrome())
const driver = new seleniumWebdriver.Builder().withCapabilities(seleniumWebdriver.Capabilities.chrome())
.build();
tap.plan(2);
tap.tearDown(function () {
tap.tearDown(() => {
driver.quit();
});
tap.beforeEach(function () {
driver.get(rootUrl + '/educators/register');
driver.get(`${rootUrl}/educators/register`);
return utils.fillUsernameSlide(driver, seleniumWebdriver)
.then(utils.fillDemographicsSlide.bind(this, driver, seleniumWebdriver)); // eslint-disable-line no-invalid-this
});
// attempts to advance the slide without inputting either name, checks that both give the correct error
tap.test('checkFirstNameRequired', function (t) {
var nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
var errorMessageXPath = '//input[@name="user.name.first"]/following-sibling::' +
tap.test('checkFirstNameRequired', t => {
const nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
const errorMessageXPath = '//input[@name="user.name.first"]/following-sibling::' +
'span[@class="help-block validation-message"]/span[contains(text(),' +
'"This field is required")]';
nextStepButton.click().then(function () {
nextStepButton.click().then(() => {
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath))
.then(function (validationMessages) {
.then(validationMessages => {
t.equal(validationMessages.length, 1);
t.end();
});
@ -45,14 +45,14 @@ tap.test('checkFirstNameRequired', function (t) {
});
// attempts to advance the slide without inputting either name, checks that both give the correct error
tap.test('checkLastNameRequired', function (t) {
var nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
var errorMessageXPath = '//input[@name="user.name.last"]/following-sibling::' +
tap.test('checkLastNameRequired', t => {
const nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
const errorMessageXPath = '//input[@name="user.name.last"]/following-sibling::' +
'span[@class="help-block validation-message"]/span[contains(text(),' +
'"This field is required")]';
nextStepButton.click().then(function () {
nextStepButton.click().then(() => {
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath))
.then(function (validationMessages) {
.then(validationMessages => {
t.equal(validationMessages.length, 1);
t.end();
});

View file

@ -4,41 +4,41 @@
* Test cases: https://github.com/LLK/scratch-www/wiki/Testing-Scratch-www#All_Test_Cases_Teacher_Join_Flow
*/
require('chromedriver');
var seleniumWebdriver = require('selenium-webdriver');
var tap = require('tap');
const seleniumWebdriver = require('selenium-webdriver');
const tap = require('tap');
var utils = require('./teacher_registration_utils.js');
var constants = utils.constants;
const utils = require('./teacher_registration_utils.js');
const constants = utils.constants;
// Set test url through environment variable
var rootUrl = process.env.ROOT_URL || 'http://localhost:8333';
const rootUrl = process.env.ROOT_URL || 'http://localhost:8333';
// chrome driver
var driver = new seleniumWebdriver.Builder().withCapabilities(seleniumWebdriver.Capabilities.chrome())
const driver = new seleniumWebdriver.Builder().withCapabilities(seleniumWebdriver.Capabilities.chrome())
.build();
tap.plan(4);
tap.tearDown(function () {
tap.tearDown(() => {
driver.quit();
});
tap.beforeEach(function () {
driver.get(rootUrl + '/educators/register');
driver.get(`${rootUrl}/educators/register`);
return utils.fillUsernameSlide(driver, seleniumWebdriver)
.then(utils.fillDemographicsSlide.bind(this, driver, seleniumWebdriver)) // eslint-disable-line no-invalid-this
.then(utils.fillNameSlide.bind(this, driver, seleniumWebdriver)) // eslint-disable-line no-invalid-this
.then(utils.fillPhoneSlide.bind(this, driver, seleniumWebdriver)); // eslint-disable-line no-invalid-this
});
tap.test('otherFieldRequiredIfChecked', function (t) {
var otherCheckbox = driver.findElement(seleniumWebdriver.By.xpath('//input[@type="checkbox" and @value="8"]'));
var nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
var errorMessageXPath = '//div[@class="other-input"]' + constants.generalErrorMessageXpath;
otherCheckbox.click().then(function () {
nextStepButton.click().then(function () {
tap.test('otherFieldRequiredIfChecked', t => {
const otherCheckbox = driver.findElement(seleniumWebdriver.By.xpath('//input[@type="checkbox" and @value="8"]'));
const nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
const errorMessageXPath = `//div[@class="other-input"]${constants.generalErrorMessageXpath}`;
otherCheckbox.click().then(() => {
nextStepButton.click().then(() => {
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath))
.then(function (validationMessages) {
.then(validationMessages => {
t.equal(validationMessages.length, 1);
t.end();
});
@ -46,42 +46,42 @@ tap.test('otherFieldRequiredIfChecked', function (t) {
});
});
tap.test('checkOrganizationFieldRequired', function (t) {
var nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
var errorMessageXPath = '//input[@name="organization.name"]/following-sibling::' +
tap.test('checkOrganizationFieldRequired', t => {
const nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
const errorMessageXPath = '//input[@name="organization.name"]/following-sibling::' +
'span[@class="help-block validation-message"]/span[contains(text(),' +
'"This field is required")]';
nextStepButton.click().then(function () {
nextStepButton.click().then(() => {
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath))
.then(function (validationMessages) {
.then(validationMessages => {
t.equal(validationMessages.length, 1);
t.end();
});
});
});
tap.test('checkRoleFieldRequired', function (t) {
var nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
var errorMessageXPath = '//input[@name="organization.title"]/following-sibling::' +
tap.test('checkRoleFieldRequired', t => {
const nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
const errorMessageXPath = '//input[@name="organization.title"]/following-sibling::' +
'span[@class="help-block validation-message"]/span[contains(text(),' +
'"This field is required")]';
nextStepButton.click().then(function () {
nextStepButton.click().then(() => {
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath))
.then(function (validationMessages) {
.then(validationMessages => {
t.equal(validationMessages.length, 1);
t.end();
});
});
});
tap.test('checkOrganizationTypeRequired', function (t) {
var nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
var errorMessageXPath = '//div[@class="checkbox"]/following-sibling::' +
tap.test('checkOrganizationTypeRequired', t => {
const nextStepButton = driver.findElement(seleniumWebdriver.By.xpath(constants.nextStepXpath));
const errorMessageXPath = '//div[@class="checkbox"]/following-sibling::' +
'span[@class="help-block validation-message" and contains(text(),' +
'"This field is required")]';
nextStepButton.click().then(function () {
nextStepButton.click().then(() => {
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath))
.then(function (validationMessages) {
.then(validationMessages => {
t.equal(validationMessages.length, 1);
t.end();
});

View file

@ -4,40 +4,40 @@
* Test cases: https://github.com/LLK/scratch-www/wiki/Testing-Scratch-www#All_Test_Cases_Teacher_Join_Flow
*/
require('chromedriver');
var seleniumWebdriver = require('selenium-webdriver');
var tap = require('tap');
const seleniumWebdriver = require('selenium-webdriver');
const tap = require('tap');
var utils = require('./teacher_registration_utils.js');
const utils = require('./teacher_registration_utils.js');
// Set test url through environment variable
var rootUrl = process.env.ROOT_URL || 'http://localhost:8333';
const rootUrl = process.env.ROOT_URL || 'http://localhost:8333';
// chrome driver
var driver = new seleniumWebdriver.Builder().withCapabilities(seleniumWebdriver.Capabilities.chrome())
const driver = new seleniumWebdriver.Builder().withCapabilities(seleniumWebdriver.Capabilities.chrome())
.build();
tap.plan(1);
tap.tearDown(function () {
tap.tearDown(() => {
driver.quit();
});
tap.beforeEach(function () {
driver.get(rootUrl + '/educators/register');
driver.get(`${rootUrl}/educators/register`);
return utils.fillUsernameSlide(driver, seleniumWebdriver)
.then(utils.fillDemographicsSlide.bind(this, driver, seleniumWebdriver)) // eslint-disable-line no-invalid-this
.then(utils.fillNameSlide.bind(this, driver, seleniumWebdriver)); // eslint-disable-line no-invalid-this
});
// inputs an invalid phone number and checks that the correct error message appears
tap.test('validatePhoneNumber', function (t) {
var phoneInput = driver.findElement(seleniumWebdriver.By.xpath('//input[@type="tel"]'));
var errorMessage = 'Please enter a valid phone number';
var errorMessageXPath = '//span[@class="help-block validation-message"]/span[contains(text(),"' +
errorMessage + '")]';
phoneInput.sendKeys(1234567890).then(function () {
tap.test('validatePhoneNumber', t => {
const phoneInput = driver.findElement(seleniumWebdriver.By.xpath('//input[@type="tel"]'));
const errorMessage = 'Please enter a valid phone number';
const errorMessageXPath = `//span[@class="help-block validation-message"]/span[contains(text(),"${
errorMessage}")]`;
phoneInput.sendKeys(1234567890).then(() => {
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath))
.then(function (validationMessages) {
.then(validationMessages => {
t.equal(validationMessages.length, 1);
t.end();
});

View file

@ -5,37 +5,35 @@
*/
require('chromedriver');
var seleniumWebdriver = require('selenium-webdriver');
var tap = require('tap');
const seleniumWebdriver = require('selenium-webdriver');
const tap = require('tap');
// Set test url through environment variable
var rootUrl = process.env.ROOT_URL || 'http://localhost:8333';
const rootUrl = process.env.ROOT_URL || 'http://localhost:8333';
// chrome driver
var driver = new seleniumWebdriver.Builder().withCapabilities(seleniumWebdriver.Capabilities.chrome())
const driver = new seleniumWebdriver.Builder().withCapabilities(seleniumWebdriver.Capabilities.chrome())
.build();
tap.plan(5);
tap.tearDown(function () {
tap.tearDown(() => {
driver.quit();
});
tap.beforeEach(function () {
return driver.get(rootUrl + '/educators/register');
});
tap.beforeEach(() => driver.get(`${rootUrl}/educators/register`));
// an error message should appear for a username less than 3 characters long
// input a username less than 3 characters and look for the validation message
tap.test('checkAtLeastThreeCharacters', function (t) {
tap.test('checkAtLeastThreeCharacters', t => {
// open scratch in a new instance of the browser
driver.get('https://scratch.mit.edu/educators/register');
var usernameInput = driver.findElement(seleniumWebdriver.By.name('user.username'));
var errorMessage = 'Usernames must be at least 3 characters';
var errorMessageXPath = '//span[@class="help-block validation-message" and contains(text(),"' +
errorMessage + '")]';
usernameInput.sendKeys('hi').then(function () {
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath)).then(function (validationMessages) {
const usernameInput = driver.findElement(seleniumWebdriver.By.name('user.username'));
const errorMessage = 'Usernames must be at least 3 characters';
const errorMessageXPath = `//span[@class="help-block validation-message" and contains(text(),"${
errorMessage}")]`;
usernameInput.sendKeys('hi').then(() => {
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath)).then(validationMessages => {
t.equal(validationMessages.length, 1);
t.end();
});
@ -44,16 +42,16 @@ tap.test('checkAtLeastThreeCharacters', function (t) {
// usernames have to be unique
// input a username that exists and check that an error message appears
tap.test('checkUsernameExistsError', function (t) {
var usernameInput = driver.findElement(seleniumWebdriver.By.name('user.username'));
var passwordInput = driver.findElement(seleniumWebdriver.By.name('user.password'));
var inputUsername = usernameInput.sendKeys('mres');
var passwordClick = passwordInput.click();
var errorMessage = 'Sorry, that username already exists';
var errorMessageXPath = '//span[@class="help-block validation-message" and contains(text(),"' +
errorMessage + '")]';
Promise.all([inputUsername, passwordClick]).then(function () { // eslint-disable-line no-undef
var errorBubble = driver.wait(seleniumWebdriver.until
tap.test('checkUsernameExistsError', t => {
const usernameInput = driver.findElement(seleniumWebdriver.By.name('user.username'));
const passwordInput = driver.findElement(seleniumWebdriver.By.name('user.password'));
const inputUsername = usernameInput.sendKeys('mres');
const passwordClick = passwordInput.click();
const errorMessage = 'Sorry, that username already exists';
const errorMessageXPath = `//span[@class="help-block validation-message" and contains(text(),"${
errorMessage}")]`;
Promise.all([inputUsername, passwordClick]).then(() => { // eslint-disable-line no-undef
const errorBubble = driver.wait(seleniumWebdriver.until
.elementLocated(seleniumWebdriver.By.xpath(errorMessageXPath)), 10000);
t.notEqual(errorBubble, undefined); // eslint-disable-line no-undefined
t.end();
@ -62,13 +60,13 @@ tap.test('checkUsernameExistsError', function (t) {
// passwords must be at least 6 characters
// find the validation message if the input password is less than 6 characters
tap.test('checkPasswordAtLeastSixCharacters', function (t) {
var passwordInput = driver.findElement(seleniumWebdriver.By.name('user.password'));
var errorMessage = 'Passwords must be at least six characters';
var errorMessageXPath = '//span[@class="help-block validation-message" and contains(text(),"' +
errorMessage + '")]';
passwordInput.sendKeys('hello').then(function () {
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath)).then(function (validationMessages) {
tap.test('checkPasswordAtLeastSixCharacters', t => {
const passwordInput = driver.findElement(seleniumWebdriver.By.name('user.password'));
const errorMessage = 'Passwords must be at least six characters';
const errorMessageXPath = `//span[@class="help-block validation-message" and contains(text(),"${
errorMessage}")]`;
passwordInput.sendKeys('hello').then(() => {
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath)).then(validationMessages => {
t.equal(validationMessages.length, 1);
t.end();
});
@ -77,15 +75,15 @@ tap.test('checkPasswordAtLeastSixCharacters', function (t) {
// password cannot be "password"
// find the validation message if the user inputs "password"
tap.test('checkPasswordNotPassword', function (t) {
tap.test('checkPasswordNotPassword', t => {
driver.get('https://scratch.mit.edu/educators/register');
var passwordInput = driver.findElement(seleniumWebdriver.By.name('user.password'));
const passwordInput = driver.findElement(seleniumWebdriver.By.name('user.password'));
// keeping "password" in messed with the xPath, may need to find a better way
var errorMessage = 'Your password may not be';
var errorMessageXPath = '//span[@class="help-block validation-message" and contains(text(),"' +
errorMessage + '")]';
passwordInput.sendKeys('password').then(function () {
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath)).then(function (validationMessages) {
const errorMessage = 'Your password may not be';
const errorMessageXPath = `//span[@class="help-block validation-message" and contains(text(),"${
errorMessage}")]`;
passwordInput.sendKeys('password').then(() => {
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath)).then(validationMessages => {
t.equal(validationMessages.length, 1);
t.end();
});
@ -94,18 +92,18 @@ tap.test('checkPasswordNotPassword', function (t) {
// the username and password cannot be the same
// find the validation message if the username and password match
tap.test('checkPasswordNotUsername', function (t) {
tap.test('checkPasswordNotUsername', t => {
driver.get('https://scratch.mit.edu/educators/register');
var passwordInput = driver.findElement(seleniumWebdriver.By.name('user.password'));
var usernameInput = driver.findElement(seleniumWebdriver.By.name('user.username'));
var errorMessage = 'Your password may not be your username';
var errorMessageXPath = '//span[@class="help-block validation-message" and contains(text(),"' +
errorMessage + '")]';
var usernamePromise = usernameInput.sendKeys('educator');
var passwordPromise = passwordInput.sendKeys('educator');
const passwordInput = driver.findElement(seleniumWebdriver.By.name('user.password'));
const usernameInput = driver.findElement(seleniumWebdriver.By.name('user.username'));
const errorMessage = 'Your password may not be your username';
const errorMessageXPath = `//span[@class="help-block validation-message" and contains(text(),"${
errorMessage}")]`;
const usernamePromise = usernameInput.sendKeys('educator');
const passwordPromise = passwordInput.sendKeys('educator');
// wait for both inputs to have the same text, and check for validation message
Promise.all([usernamePromise, passwordPromise]).then(function () { // eslint-disable-line no-undef
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath)).then(function (validationMessages) {
Promise.all([usernamePromise, passwordPromise]).then(() => { // eslint-disable-line no-undef
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath)).then(validationMessages => {
// there should be only one validation message
t.equal(validationMessages.length, 1);
t.end();

View file

@ -4,27 +4,27 @@
* Test cases: https://github.com/LLK/scratch-www/wiki/Testing-Scratch-www#All_Test_Cases_Teacher_Join_Flow
*/
require('chromedriver');
var seleniumWebdriver = require('selenium-webdriver');
var tap = require('tap');
const seleniumWebdriver = require('selenium-webdriver');
const tap = require('tap');
var utils = require('./teacher_registration_utils.js');
var constants = utils.constants;
const utils = require('./teacher_registration_utils.js');
const constants = utils.constants;
// Set test url through environment variable
var rootUrl = process.env.ROOT_URL || 'http://localhost:8333';
const rootUrl = process.env.ROOT_URL || 'http://localhost:8333';
// chrome driver
var driver = new seleniumWebdriver.Builder().withCapabilities(seleniumWebdriver.Capabilities.chrome())
const driver = new seleniumWebdriver.Builder().withCapabilities(seleniumWebdriver.Capabilities.chrome())
.build();
tap.plan(3);
tap.tearDown(function () {
tap.tearDown(() => {
driver.quit();
});
tap.beforeEach(function () {
driver.get(rootUrl + '/educators/register');
driver.get(`${rootUrl}/educators/register`);
return utils.fillUsernameSlide(driver, seleniumWebdriver)
.then(utils.fillDemographicsSlide.bind(this, driver, seleniumWebdriver)) // eslint-disable-line no-invalid-this
.then(utils.fillNameSlide.bind(this, driver, seleniumWebdriver)) // eslint-disable-line no-invalid-this
@ -33,11 +33,11 @@ tap.beforeEach(function () {
.then(utils.fillAddressSlide.bind(this, driver, seleniumWebdriver)); // eslint-disable-line no-invalid-this
});
tap.test('checkCharacterCountIsCorrect', function (t) {
var textarea = driver.findElement(seleniumWebdriver.By.name('useScratch'));
var charCount = driver.findElement(seleniumWebdriver.By.xpath('//p[@class="char-count"]'));
textarea.sendKeys('hello').then(function () {
charCount.getText().then(function (charCountText) {
tap.test('checkCharacterCountIsCorrect', t => {
const textarea = driver.findElement(seleniumWebdriver.By.name('useScratch'));
const charCount = driver.findElement(seleniumWebdriver.By.xpath('//p[@class="char-count"]'));
textarea.sendKeys('hello').then(() => {
charCount.getText().then(charCountText => {
t.equal(charCountText, '5/300');
t.end();
});
@ -46,24 +46,24 @@ tap.test('checkCharacterCountIsCorrect', function (t) {
// Inputs more than 300 characters and checks that the char count gets the class 'overmax'
// which turns the text orange
tap.test('checkCharacterCountTurnsOrangeWhenTooLong', function (t) {
var textarea = driver.findElement(seleniumWebdriver.By.name('useScratch'));
var charCount = driver.findElement(seleniumWebdriver.By.xpath('//p[@class="char-count"]'));
textarea.sendKeys(constants.loremIpsumTextLong).then(function () {
charCount.getAttribute('class').then(function (charCountClasses) {
tap.test('checkCharacterCountTurnsOrangeWhenTooLong', t => {
const textarea = driver.findElement(seleniumWebdriver.By.name('useScratch'));
const charCount = driver.findElement(seleniumWebdriver.By.xpath('//p[@class="char-count"]'));
textarea.sendKeys(constants.loremIpsumTextLong).then(() => {
charCount.getAttribute('class').then(charCountClasses => {
t.ok(charCountClasses.includes('overmax'));
t.end();
});
});
});
tap.test('checkCharacterCountErrorAppersWhenTooLong', function (t) {
var textarea = driver.findElement(seleniumWebdriver.By.name('useScratch'));
var errorMessage = 'Description must be at most 300 characters';
var errorMessageXPath = '//span[@class="help-block validation-message" and contains(text(),"' +
errorMessage + '")]';
textarea.sendKeys(constants.loremIpsumTextLong).then(function () {
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath)).then(function (validationMessages) {
tap.test('checkCharacterCountErrorAppersWhenTooLong', t => {
const textarea = driver.findElement(seleniumWebdriver.By.name('useScratch'));
const errorMessage = 'Description must be at most 300 characters';
const errorMessageXPath = `//span[@class="help-block validation-message" and contains(text(),"${
errorMessage}")]`;
textarea.sendKeys(constants.loremIpsumTextLong).then(() => {
driver.findElements(seleniumWebdriver.By.xpath(errorMessageXPath)).then(validationMessages => {
t.equal(validationMessages.length, 1);
t.end();
});

View file

@ -8,117 +8,116 @@ const {
clickXpath,
containsClass,
findByXpath,
navigate,
signIn
} = new SeleniumHelper();
// Using 1 and 2 here. Hopefully this is not confusing.
let username1 = process.env.SMOKE_USERNAME + '4';
let username2 = process.env.SMOKE_USERNAME + '5';
let password = process.env.SMOKE_PASSWORD;
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
const username1 = `${process.env.SMOKE_USERNAME}4`;
const username2 = `${process.env.SMOKE_USERNAME}5`;
const password = process.env.SMOKE_PASSWORD;
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
// project for comments (owned by username2)
let projectId = process.env.COMMENT_PROJECT_ID || 1300008409;
let projectUrl = `${rootUrl}/projects/${projectId}`;
const projectId = process.env.COMMENT_PROJECT_ID || 1300008409;
const projectUrl = `${rootUrl}/projects/${projectId}`;
// profile for comments (username2)
let profileUrl = `${rootUrl}/users/${username2}`;
const profileUrl = `${rootUrl}/users/${username2}`;
// studio for comments (hosted by username2) comments tab
let studioId = process.env.COMMENT_STUDIO_ID || 10005646;
let studioUrl = `${rootUrl}/studios/${studioId}/comments`;
const studioId = process.env.COMMENT_STUDIO_ID || 10005646;
const studioUrl = `${rootUrl}/studios/${studioId}/comments`;
// setup comments to leave
let date = new Date();
let dateString = `Y:${date.getFullYear()} - M:${date.getMonth() + 1} - D:${date.getDate()} ` +
`: ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
let buildNumber = process.env.CIRCLE_BUILD_NUM || dateString;
let projectComment = buildNumber + ' project';
let profileComment = buildNumber + ' profile';
let studioComment = buildNumber + ' studio';
// make sure they are unique and will not be censored (avoid numbers that might look like phone numbers or other PII)
const date = new Date();
const dateString = date.toISOString();
const projectComment = `${dateString} project`;
const profileComment = `${dateString} profile`;
const studioComment = `${dateString} studio`;
let projectReply = projectComment + ' reply';
let profileReply = profileComment + ' reply';
let studioReply = studioComment + ' reply';
const projectReply = `${projectComment} reply`;
const profileReply = `${profileComment} reply`;
const studioReply = `${studioComment} reply`;
jest.setTimeout(60000);
let driver;
describe('comment tests', async () => {
describe('comment tests', () => {
beforeAll(async () => {
driver = await buildDriver('www-integration project comments');
await driver.get(rootUrl);
await navigate(rootUrl);
});
afterAll(async () => await driver.quit());
afterAll(() => driver.quit());
describe('leave comments', async () => {
describe('leave comments', () => {
beforeAll(async () => {
await signIn(username1, password);
await findByXpath('//span[contains(@class, "profile-name")]');
});
afterAll(async () => {
await driver.get(rootUrl);
await navigate(rootUrl);
await clickXpath('//a[contains(@class, "user-info")]');
await clickText('Sign out');
});
test('leave comment on project', async () => {
await driver.get(projectUrl);
await navigate(projectUrl);
// leave the comment
let commentBox = await findByXpath('//textArea[@name="compose-comment"]');
const commentBox = await findByXpath('//textArea[@name="compose-comment"]');
await commentBox.sendKeys(projectComment);
await findByXpath(`//textarea[contains(text(), "${projectComment}")]`);
await clickXpath('//button[@class="button compose-post"]');
// find the comment
let commentXpath = await `//div[@class="comment-bubble"]/span/span[contains(text(),` +
` "${projectComment}")]`;
let postedComment = await findByXpath(commentXpath);
let commentVisible = await postedComment.isDisplayed();
await expect(commentVisible).toBe(true);
const commentXpath = `//div[@class="comment-bubble"]/span/span[contains(text(), "${projectComment}")]`;
const postedComment = await findByXpath(commentXpath);
const commentVisible = await postedComment.isDisplayed();
expect(commentVisible).toBe(true);
});
test('leave comment on a profile', async () => {
await driver.get(profileUrl);
await navigate(profileUrl);
// leave the comment
let commentXpath = await '//form[@id="main-post-form"]/div/textArea';
let commentArea = await findByXpath(commentXpath);
const commentXpath = '//form[@id="main-post-form"]/div/textArea';
const commentArea = await findByXpath(commentXpath);
await commentArea.sendKeys(profileComment);
await clickXpath('//div[@class="button small"]/a[contains(text(), "Post")]');
// find the comment
let newComment = await findByXpath(`//div[@class="comment "]/div/div[contains(text(),` +
const newComment = await findByXpath(`//div[@class="comment "]/div/div[contains(text(),` +
` "${profileComment}")]`);
let commentVisible = await newComment.isDisplayed();
await expect(commentVisible).toBe(true);
const commentVisible = await newComment.isDisplayed();
expect(commentVisible).toBe(true);
// return to homepage to sign out with www
await driver.get(rootUrl);
await navigate(rootUrl);
});
test('leave comment on studio', async () => {
await driver.get(studioUrl);
await navigate(studioUrl);
// leave the comment
let commentBox = await findByXpath('//textArea[@name="compose-comment"]');
const commentBox = await findByXpath('//textArea[@name="compose-comment"]');
await commentBox.sendKeys(studioComment);
await findByXpath(`//textarea[contains(text(), "${studioComment}")]`);
await clickXpath('//button[@class="button compose-post"]');
// find the comment
let commentXpath = `//div[@class="comment-bubble"]/span/span[contains(text(), "${studioComment}")]`;
let postedComment = await findByXpath(commentXpath);
let commentVisible = await postedComment.isDisplayed();
await expect(commentVisible).toBe(true);
const commentXpath = `//div[@class="comment-bubble"]/span/span[contains(text(), "${studioComment}")]`;
const postedComment = await findByXpath(commentXpath);
const commentVisible = await postedComment.isDisplayed();
expect(commentVisible).toBe(true);
});
});
describe('second user tests', async () => {
describe('second user tests', () => {
beforeAll(async () => {
await signIn(username2, password);
await findByXpath('//span[contains(@class, "profile-name")]');
@ -126,183 +125,182 @@ describe('comment tests', async () => {
// get notifications
test('get notification badge for comments', async () => {
let messages = await findByXpath('//span[@class = "message-count show"]');
let messagesVisible = await messages.isDisplayed();
await expect(messagesVisible).toBe(true);
const messages = await findByXpath('//span[@class = "message-count show"]');
const messagesVisible = await messages.isDisplayed();
expect(messagesVisible).toBe(true);
});
test('click notifications for comments', async () => {
await clickXpath('//li[@class="link right messages"]');
let messages = await findByXpath('//ul[@class="messages-social-list"]');
let messagesVisible = await messages.isDisplayed();
await expect(messagesVisible).toBe(true);
const messages = await findByXpath('//ul[@class="messages-social-list"]');
const messagesVisible = await messages.isDisplayed();
expect(messagesVisible).toBe(true);
});
test('project comment message visible', async () => {
await driver.get(rootUrl + '/messages');
await navigate(`${rootUrl}/messages`);
let projectMessageXpath = '//p[@class="emoji-text mod-comment" ' +
const projectMessageXpath = '//p[@class="emoji-text mod-comment" ' +
`and contains(text(), "${projectComment}")]`;
let projectMessage = await findByXpath(projectMessageXpath);
let projectMessageVisible = await projectMessage.isDisplayed();
await expect(projectMessageVisible).toBe(true);
const projectMessage = await findByXpath(projectMessageXpath);
const projectMessageVisible = await projectMessage.isDisplayed();
expect(projectMessageVisible).toBe(true);
});
test('profile comment message visible', async () => {
await driver.get(rootUrl + '/messages');
await navigate(`${rootUrl}/messages`);
let profileMessageXpath = '//p[@class="emoji-text mod-comment" ' +
const profileMessageXpath = '//p[@class="emoji-text mod-comment" ' +
`and contains(text(), "${profileComment}")]`;
let profileMessage = await findByXpath(profileMessageXpath);
let profileMessageVisible = await profileMessage.isDisplayed();
await expect(profileMessageVisible).toBe(true);
const profileMessage = await findByXpath(profileMessageXpath);
const profileMessageVisible = await profileMessage.isDisplayed();
expect(profileMessageVisible).toBe(true);
});
// studio comments do not send a notification
test('project message links you to project page', async () => {
let projectLinkXpath = '//p[@class="emoji-text mod-comment" ' +
const projectLinkXpath = '//p[@class="emoji-text mod-comment" ' +
`and contains(text(), "${projectComment}")]/../../../p[@class = "comment-message-info"]/span/a[2]`;
await driver.get(rootUrl + '/messages');
await navigate(`${rootUrl}/messages`);
await clickXpath(projectLinkXpath);
// find green flag overlay
let gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
const gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
await gfOverlay.isDisplayed();
});
test('project comment is on project page', async () => {
let projectLinkXpath = '//p[@class="emoji-text mod-comment" ' +
const projectLinkXpath = '//p[@class="emoji-text mod-comment" ' +
`and contains(text(), "${projectComment}")]/../../../p[@class = "comment-message-info"]/span/a[2]`;
await driver.get(rootUrl + '/messages');
await navigate(`${rootUrl}/messages`);
await clickXpath(projectLinkXpath);
let commentXpath = `//span[contains(text(), "${projectComment}")]`;
let singleComment = await findByXpath(commentXpath);
let commentVisible = await singleComment.isDisplayed();
await expect(commentVisible).toBe(true);
const commentXpath = `//span[contains(text(), "${projectComment}")]`;
const singleComment = await findByXpath(commentXpath);
const commentVisible = await singleComment.isDisplayed();
expect(commentVisible).toBe(true);
});
test('project comment is highlighted', async () => {
let projectLinkXpath = '//p[@class="emoji-text mod-comment" ' +
const projectLinkXpath = '//p[@class="emoji-text mod-comment" ' +
`and contains(text(), "${projectComment}")]/../../../p[@class = "comment-message-info"]/span/a[2]`;
let containerXpath = `//span[contains(text(), "${projectComment}")]/../../../..`;
const containerXpath = `//span[contains(text(), "${projectComment}")]/../../../..`;
await driver.get(rootUrl + '/messages');
await navigate(`${rootUrl}/messages`);
await clickXpath(projectLinkXpath);
let commentContainer = await findByXpath(containerXpath);
let isHighlighted = await containsClass(commentContainer, 'highlighted-comment');
await expect(isHighlighted).toBe(true);
const commentContainer = await findByXpath(containerXpath);
const isHighlighted = await containsClass(commentContainer, 'highlighted-comment');
expect(isHighlighted).toBe(true);
});
test('profile message links you to profile page', async () => {
let profileLinkXpath = await '//p[@class="emoji-text mod-comment" ' +
const profileLinkXpath = '//p[@class="emoji-text mod-comment" ' +
`and contains(text(), "${profileComment}")]/../../../` +
`p[@class = "comment-message-info"]/span/a[2]`;
await driver.get(rootUrl + '/messages');
'p[@class = "comment-message-info"]/span/a[2]';
await navigate(`${rootUrl}/messages`);
await clickXpath(profileLinkXpath);
// find profile data
let profileDataXpath = '//div[@id="profile-data"]';
let pathToUsername = '/div[@class="box-head"]/div[@class="header-text"]/h2';
const profileDataXpath = '//div[@id="profile-data"]';
const pathToUsername = '/div[@class="box-head"]/div[@class="header-text"]/h2';
await findByXpath(profileDataXpath);
let header = await findByXpath(profileDataXpath + pathToUsername);
let uname = await header.getText();
await expect(uname).toBe(username2);
const header = await findByXpath(profileDataXpath + pathToUsername);
const uname = await header.getText();
expect(uname).toBe(username2);
});
test('profile comment is on profile page', async () => {
let profileLinkXpath = await '//p[@class="emoji-text mod-comment" ' +
const profileLinkXpath = '//p[@class="emoji-text mod-comment" ' +
`and contains(text(), "${profileComment}")]/../../../` +
`p[@class = "comment-message-info"]/span/a[2]`;
await driver.get(rootUrl + '/messages');
'p[@class = "comment-message-info"]/span/a[2]';
await navigate(`${rootUrl}/messages`);
await clickXpath(profileLinkXpath);
// find comment
let commentXpath = `//div[contains(text(), "${profileComment}")]`;
let leftComment = await findByXpath(commentXpath);
let commentVisible = await leftComment.isDisplayed();
await expect(commentVisible).toBe(true);
const commentXpath = `//div[contains(text(), "${profileComment}")]`;
const leftComment = await findByXpath(commentXpath);
const commentVisible = await leftComment.isDisplayed();
expect(commentVisible).toBe(true);
});
test('profile comment is highlighted', async () => {
let profileLinkXpath = await '//p[@class="emoji-text mod-comment" ' +
const profileLinkXpath = '//p[@class="emoji-text mod-comment" ' +
`and contains(text(), "${profileComment}")]/../../../` +
`p[@class = "comment-message-info"]/span/a[2]`;
await driver.get(rootUrl + '/messages');
'p[@class = "comment-message-info"]/span/a[2]';
await navigate(`${rootUrl}/messages`);
await clickXpath(profileLinkXpath);
// comment highlighted?
let containerXpath = `//div[contains(text(), "${profileComment}")]/../../..`;
let commentContainer = await findByXpath(containerXpath);
let isHighlighted = await containsClass(commentContainer, 'highlighted');
await expect(isHighlighted).toBe(true);
const containerXpath = `//div[contains(text(), "${profileComment}")]/../../..`;
const commentContainer = await findByXpath(containerXpath);
const isHighlighted = await containsClass(commentContainer, 'highlighted');
expect(isHighlighted).toBe(true);
});
test('project: reply to comment', async () => {
await driver.get(projectUrl);
let commentXpath = `//span[contains(text(), "${projectComment}")]/../..`;
let replyXpath = commentXpath + '//span[@class = "comment-reply"]';
await navigate(projectUrl);
const commentXpath = `//span[contains(text(), "${projectComment}")]/../..`;
const replyXpath = `${commentXpath}//span[@class = "comment-reply"]`;
await clickXpath(replyXpath);
// type reply
let replyRow = '//div[contains(@class, "comment-reply-row")]';
let replyComposeXpath = replyRow + '//textArea[@class = "inplace-textarea"]';
let composeBox = await findByXpath(replyComposeXpath);
const replyRow = '//div[contains(@class, "comment-reply-row")]';
const replyComposeXpath = `${replyRow}//textArea[@class = "inplace-textarea"]`;
const composeBox = await findByXpath(replyComposeXpath);
await composeBox.sendKeys(projectReply);
// click post
let postButton = await findByXpath(replyRow + '//button[@class = "button compose-post"]');
await postButton.click();
await clickXpath(`${replyRow}//button[@class = "button compose-post"]`);
let postedReply = await findByXpath(`//span[contains(text(), "${projectReply}")]`);
let commentVisible = await postedReply.isDisplayed();
await expect(commentVisible).toBe(true);
const postedReply = await findByXpath(`//span[contains(text(), "${projectReply}")]`);
const commentVisible = await postedReply.isDisplayed();
expect(commentVisible).toBe(true);
});
test('profile reply to comment', async () => {
await driver.get(profileUrl);
await navigate(profileUrl);
// find the comment and click reply
let commentXpath = `//div[contains(text(), "${profileComment}")]/..`;
await clickXpath(commentXpath + '//a[@class = "reply"]');
const commentXpath = `//div[contains(text(), "${profileComment}")]/..`;
await clickXpath(`${commentXpath}//a[@class = "reply"]`);
// select reply box and type reply
let replyComposeBox = await findByXpath(commentXpath + '//textArea');
const replyComposeBox = await findByXpath(`${commentXpath}//textArea`);
await replyComposeBox.sendKeys(profileReply);
// click post
await clickXpath(commentXpath + '//a[contains(text(), "Post")]');
await clickXpath(`${commentXpath}//a[contains(text(), "Post")]`);
// The reply wasn't findable by xpath after several attempts, but it seems
// better to have this much of a test
});
test('studio: reply to comment', async () => {
await driver.get(studioUrl);
await navigate(studioUrl);
// find the comment and click reply
let commentXpath = `//span[contains(text(), "${studioComment}")]/../..`;
await clickXpath(commentXpath + '//span[@class = "comment-reply"]');
const commentXpath = `//span[contains(text(), "${studioComment}")]/../..`;
await clickXpath(`${commentXpath}//span[@class = "comment-reply"]`);
// type reply
let replyRow = '//div[contains(@class, "comment-reply-row")]';
let replyComposeXpath = replyRow + '//textArea[@class = "inplace-textarea"]';
let composeBox = await findByXpath(replyComposeXpath);
const replyRow = '//div[contains(@class, "comment-reply-row")]';
const replyComposeXpath = `${replyRow}//textArea[@class = "inplace-textarea"]`;
const composeBox = await findByXpath(replyComposeXpath);
await composeBox.sendKeys(studioReply);
// click post
let postButton = await findByXpath(replyRow + '//button[@class = "button compose-post"]');
const postButton = await findByXpath(`${replyRow}//button[@class = "button compose-post"]`);
await postButton.click();
// find reply
let postedReply = await findByXpath(`//span[contains(text(), "${studioReply}")]`);
let commentVisible = await postedReply.isDisplayed();
await expect(commentVisible).toBe(true);
const postedReply = await findByXpath(`//span[contains(text(), "${studioReply}")]`);
const commentVisible = await postedReply.isDisplayed();
expect(commentVisible).toBe(true);
});
});
});

View file

@ -5,10 +5,12 @@ const SeleniumHelper = require('./selenium-helpers.js');
const {
clickText,
buildDriver,
findText
findText,
navigate,
waitUntilDocumentReady
} = new SeleniumHelper();
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
jest.setTimeout(60000);
@ -20,39 +22,43 @@ describe('www-integration footer links', () => {
});
beforeEach(async () => {
await driver.get(rootUrl);
await navigate(rootUrl);
await findText('Create stories, games, and animations');
});
afterAll(async () => await driver.quit());
afterAll(() => driver.quit());
// ==== About Scratch column ====
test('click About Scratch link', async () => {
await clickText('About Scratch');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/about\/?$/);
});
test('click For Parents link', async () => {
await clickText('For Parents');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/parents\/?$/);
});
test('click For Educators link', async () => {
await clickText('For Educators');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/educators\/?$/);
});
test('click For Developers link', async () => {
await clickText('For Developers');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/developers\/?$/);
});
@ -60,22 +66,25 @@ describe('www-integration footer links', () => {
test('click Community Guidelines link', async () => {
await clickText('Community Guidelines');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/community_guidelines\/?$/);
});
test('click Discussion Forums link', async () => {
await clickText('Discussion Forums');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/discuss\/?$/);
});
test('click Statistics link', async () => {
await clickText('Statistics');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/statistics\/?$/);
});
@ -83,29 +92,33 @@ describe('www-integration footer links', () => {
test('click Ideas link', async () => {
await clickText('Ideas');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/ideas\/?$/);
});
test('click FAQ link', async () => {
await clickText('FAQ');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/faq\/?$/);
});
test('click Download link', async () => {
await clickText('Download');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/download\/?$/);
});
test('click Contact Us link', async () => {
await clickText('Contact Us');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/contact-us\/?$/);
});
@ -113,49 +126,43 @@ describe('www-integration footer links', () => {
test('click Terms of Use link', async () => {
await clickText('Terms of Use');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/terms_of_use\/?$/);
});
test('click Privacy Policy link', async () => {
await clickText('Privacy Policy');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/privacy_policy\/?$/);
});
test('click Cookies link', async () => {
await clickText('Cookies');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/cookies\/?$/);
// Verify localization of last updated message
let lastUpdated = await findText('The Scratch Cookie Policy was last updated June 28, 2023');
let lastUpdatedVisible = await lastUpdated.isDisplayed();
await expect(lastUpdatedVisible).toBe(true);
const lastUpdated = await findText('The Scratch Cookie Policy was last updated');
const lastUpdatedVisible = await lastUpdated.isDisplayed();
expect(lastUpdatedVisible).toBe(true);
});
test('click DMCA link', async () => {
await clickText('DMCA');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/DMCA\/?$/);
});
// ==== SCRATCH FAMILY column ====
test('click Scratch Conference link', async () => {
await clickText('Scratch Conference');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/scratch-conference\/?$/);
});
});
// The following links in are skipped because they are not on scratch.mit.edu
// The following links in the footer are skipped because they are not part of scratch-www
// Jobs
// Press
@ -166,3 +173,4 @@ describe('www-integration footer links', () => {
// SCRATCH JR (SCRATCHJR)
// SCRATCH DAY
// SCRATCH FOUNDATION
// Scratch Conference

View file

@ -3,12 +3,14 @@
const SeleniumHelper = require('./selenium-helpers.js');
const {
buildDriver,
clickXpath,
findByXpath,
buildDriver
navigate,
waitUntilDocumentReady
} = new SeleniumHelper();
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
jest.setTimeout(60000);
@ -17,40 +19,41 @@ let driver;
describe('www-integration project rows', () => {
beforeAll(async () => {
driver = await buildDriver('www-integration project rows');
// driver.get(rootUrl);
// navigate(rootUrl);
});
beforeEach(async () => {
await driver.get(rootUrl);
await navigate(rootUrl);
});
afterAll(async () => await driver.quit());
afterAll(() => driver.quit());
test('Featured Projects row title', async () => {
let projects = await findByXpath('//div[@class="box"]/div[@class="box-header"]/h4');
let projectsText = await projects.getText();
await expect(projectsText).toEqual('Featured Projects');
const projects = await findByXpath('//div[@class="box"]/div[@class="box-header"]/h4');
const projectsText = await projects.getText();
expect(projectsText).toEqual('Featured Projects');
});
test('Featured Project link', async () => {
await clickXpath('//div[@class="box"][descendant::text()="Featured Projects"]' +
'//div[contains(@class, "thumbnail")][1]/a[@class="thumbnail-image"]');
let guiPlayer = await findByXpath('//div[@class="guiPlayer"]');
let guiPlayerDisplayed = await guiPlayer.isDisplayed();
await expect(guiPlayerDisplayed).toBe(true);
const guiPlayer = await findByXpath('//div[@class="guiPlayer"]');
const guiPlayerDisplayed = await guiPlayer.isDisplayed();
expect(guiPlayerDisplayed).toBe(true);
});
test('Featured Studios row title', async () => {
let studios = await findByXpath('//div[@class="box"][2]/div[@class="box-header"]/h4');
let studiosText = await studios.getText();
await expect(studiosText).toEqual('Featured Studios');
const studios = await findByXpath('//div[@class="box"][2]/div[@class="box-header"]/h4');
const studiosText = await studios.getText();
expect(studiosText).toEqual('Featured Studios');
});
test('Featured Studios link', async () => {
await clickXpath('//div[@class="box"][descendant::text()="Featured Studios"]' +
'//div[contains(@class, "thumbnail")][1]/a[@class="thumbnail-image"]');
let studioInfo = await findByXpath('//div[contains(@class, "studio-info")]');
let studioInfoDisplayed = await studioInfo.isDisplayed();
await expect(studioInfoDisplayed).toBe(true);
await waitUntilDocumentReady();
const studioInfo = await findByXpath('//div[contains(@class, "studio-info")]');
const studioInfoDisplayed = await studioInfo.isDisplayed();
expect(studioInfoDisplayed).toBe(true);
});
});

View file

@ -3,13 +3,15 @@
const SeleniumHelper = require('./selenium-helpers.js');
const {
findByXpath,
buildDriver,
clickXpath,
buildDriver
findByXpath,
navigate,
waitUntilDocumentReady
} = new SeleniumHelper();
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
let takenUsername = process.env.SMOKE_USERNAME;
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
const takenUsername = process.env.SMOKE_USERNAME;
jest.setTimeout(60000);
@ -18,72 +20,77 @@ let driver;
describe('www-integration join flow', () => {
beforeAll(async () => {
driver = await buildDriver('www-integration join flow');
await driver.get(rootUrl);
});
afterAll(async () => await driver.quit());
afterAll(() => driver.quit());
beforeEach(async () => {
await driver.get(rootUrl);
await clickXpath('//a[@class="registrationLink"]');
await navigate(rootUrl); // navigate to home page
await clickXpath('//a[@class="registrationLink"]'); // navigate to join page
await waitUntilDocumentReady();
});
test('click Join opens join modal', async () => {
let joinModal = await findByXpath('//div[@class = "join-flow-outer-content"]');
let modalVisible = await joinModal.isDisplayed();
await expect(modalVisible).toBe(true);
const joinModal = await findByXpath('//div[@class = "join-flow-outer-content"]');
const modalVisible = await joinModal.isDisplayed();
expect(modalVisible).toBe(true);
});
test('username validation message appears', async () => {
await clickXpath('//input[contains(@name, "username")]');
let message = await findByXpath('//div[contains(@class, "validation-message")]');
let messageText = await message.getText();
await expect(messageText).toEqual('Don\'t use your real name');
const clickedInput = await clickXpath('//input[contains(@name, "username")]');
await driver.wait(() => driver.executeScript('return document.activeElement == arguments[0]', clickedInput));
const message = await findByXpath('//div[contains(@class, "validation-message")]');
const messageText = await message.getText();
expect(messageText).toEqual('Don\'t use your real name');
});
test('password validation message appears', async () => {
await clickXpath('//input[contains(@name, "password")]');
let message = await findByXpath('//div[contains(@class, "validation-message")]');
let messageText = await message.getText();
await expect(messageText).toContain('Write it down so you remember.');
const clickedInput = await clickXpath('//input[contains(@name, "password")]');
await driver.wait(() => driver.executeScript('return document.activeElement == arguments[0]', clickedInput));
const message = await findByXpath('//div[contains(@class, "validation-message")]');
const messageText = await message.getText();
expect(messageText).toContain('Write it down so you remember.');
});
test('password validation message appears', async () => {
await clickXpath('//input[contains(@name, "passwordConfirm")]');
let message = await findByXpath('//div[contains(@class, "validation-message")]');
let messageText = await message.getText();
await expect(messageText).toEqual('Type password again');
test('password confirmation validation message appears', async () => {
const clickedInput = await clickXpath('//input[contains(@name, "passwordConfirm")]');
await driver.wait(() => driver.executeScript('return document.activeElement == arguments[0]', clickedInput));
const message = await findByXpath('//div[contains(@class, "validation-message")]');
const messageText = await message.getText();
expect(messageText).toEqual('Type password again');
});
test('username validation: too short', async () => {
let textInput = await findByXpath('//input[contains(@name, "username")]');
const textInput = await findByXpath('//input[contains(@name, "username")]');
await textInput.click();
await driver.wait(() => driver.executeScript('return document.activeElement == arguments[0]', textInput));
await textInput.sendKeys('ab');
await clickXpath('//div[@class = "join-flow-outer-content"]');
let message = await findByXpath('//div[contains(@class, "validation-error")]');
let messageText = await message.getText();
await expect(messageText).toContain('Must be 3 letters or longer');
const message = await findByXpath('//div[contains(@class, "validation-error")]');
const messageText = await message.getText();
expect(messageText).toContain('Must be 3 letters or longer');
});
test('username validation: username taken', async () => {
let textInput = await findByXpath('//input[contains(@name, "username")]');
const textInput = await findByXpath('//input[contains(@name, "username")]');
await textInput.click();
await driver.wait(() => driver.executeScript('return document.activeElement == arguments[0]', textInput));
await textInput.sendKeys(takenUsername);
await clickXpath('//div[@class = "join-flow-outer-content"]');
let message = await findByXpath('//div[contains(@class, "validation-error")]');
let messageText = await message.getText();
await expect(messageText).toContain('Username taken.');
const message = await findByXpath('//div[contains(@class, "validation-error")]');
const messageText = await message.getText();
expect(messageText).toContain('Username taken.');
});
test('username validation: bad word', async () => {
let textInput = await findByXpath('//input[contains(@name, "username")]');
const textInput = await findByXpath('//input[contains(@name, "username")]');
await textInput.click();
await driver.wait(() => driver.executeScript('return document.activeElement == arguments[0]', textInput));
// Should be caught by the filter
await textInput.sendKeys('xxxxxxxxx');
await clickXpath('//div[@class = "join-flow-outer-content"]');
let message = await findByXpath('//div[contains(@class, "validation-error")]');
let messageText = await message.getText();
await expect(messageText).toContain('Username not allowed');
const message = await findByXpath('//div[contains(@class, "validation-error")]');
const messageText = await message.getText();
expect(messageText).toContain('Username not allowed');
});
});

View file

@ -7,15 +7,18 @@ const {
clickText,
clickXpath,
findByXpath,
signIn
navigate,
signIn,
urlMatches,
waitUntilDocumentReady
} = new SeleniumHelper();
let username = process.env.SMOKE_USERNAME + '1';
let password = process.env.SMOKE_PASSWORD;
const username = `${process.env.SMOKE_USERNAME}1`;
const password = process.env.SMOKE_PASSWORD;
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
let myStuffURL = rootUrl + '/mystuff';
let rateLimitCheck = process.env.RATE_LIMIT_CHECK || rootUrl;
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
const myStuffURL = `${rootUrl}/mystuff`;
const rateLimitCheck = process.env.RATE_LIMIT_CHECK || rootUrl;
jest.setTimeout(60000);
@ -24,109 +27,110 @@ let driver;
describe('www-integration my_stuff', () => {
beforeAll(async () => {
driver = await buildDriver('www-integration my_stuff');
await driver.get(rootUrl);
await driver.sleep(1000);
await navigate(rootUrl);
await signIn(username, password);
await findByXpath('//span[contains(@class, "profile-name")]');
});
afterAll(async () => await driver.quit());
afterAll(() => driver.quit());
test('verify My Stuff structure (tabs, title)', async () => {
await driver.get(myStuffURL);
let header = await findByXpath('//div[@class="box-head"]/h2');
let headerVisible = await header.isDisplayed();
await expect(headerVisible).toBe(true);
let allTab = await findByXpath('//li[@data-tab="projects"]/a');
let allTabVisible = await allTab.isDisplayed();
await expect(allTabVisible).toBe(true);
let sharedTab = await findByXpath('//li[@data-tab="shared"]/a');
let sharedTabVisible = await sharedTab.isDisplayed();
await expect(sharedTabVisible).toBe(true);
let unsharedTab = await findByXpath('//li[@data-tab="unshared"]/a');
let unsharedTabVisible = await unsharedTab.isDisplayed();
await expect(unsharedTabVisible).toBe(true);
let studios = await findByXpath('//li[@data-tab="galleries"]/a');
let studiosVisible = await studios.isDisplayed();
await expect(studiosVisible).toBe(true);
let trash = await findByXpath('//li[@data-tab="trash"]/a');
let trashVisible = await trash.isDisplayed();
await expect(trashVisible).toBe(true);
await navigate(myStuffURL);
const header = await findByXpath('//div[@class="box-head"]/h2');
const headerVisible = await header.isDisplayed();
expect(headerVisible).toBe(true);
const allTab = await findByXpath('//li[@data-tab="projects"]/a');
const allTabVisible = await allTab.isDisplayed();
expect(allTabVisible).toBe(true);
const sharedTab = await findByXpath('//li[@data-tab="shared"]/a');
const sharedTabVisible = await sharedTab.isDisplayed();
expect(sharedTabVisible).toBe(true);
const unsharedTab = await findByXpath('//li[@data-tab="unshared"]/a');
const unsharedTabVisible = await unsharedTab.isDisplayed();
expect(unsharedTabVisible).toBe(true);
const studios = await findByXpath('//li[@data-tab="galleries"]/a');
const studiosVisible = await studios.isDisplayed();
expect(studiosVisible).toBe(true);
const trash = await findByXpath('//li[@data-tab="trash"]/a');
const trashVisible = await trash.isDisplayed();
expect(trashVisible).toBe(true);
});
test('clicking a project title should take you to the project page', async () => {
await driver.get(myStuffURL);
await navigate(myStuffURL);
await clickXpath('//span[@class="media-info-item title"]');
await driver.sleep(6000);
let gui = await findByXpath('//div[@class="guiPlayer"]');
let guiVisible = await gui.isDisplayed();
await expect(guiVisible).toBe(true);
await waitUntilDocumentReady();
const gui = await findByXpath('//div[@class="guiPlayer"]');
const guiVisible = await gui.isDisplayed();
expect(guiVisible).toBe(true);
});
test('clicking "see inside" should take you to the editor', async () =>{
await driver.get(myStuffURL);
test('clicking "see inside" should take you to the editor', async () => {
await navigate(myStuffURL);
await clickXpath('//a[@data-control="edit"]');
let gf = await findByXpath('//img[@class="green-flag_green-flag_1kiAo"]');
let gfVisible = await gf.isDisplayed();
await expect(gfVisible).toBe(true);
await waitUntilDocumentReady();
const gf = await findByXpath('//img[@class="green-flag_green-flag_1kiAo"]');
const gfVisible = await gf.isDisplayed();
expect(gfVisible).toBe(true);
});
test('Add To button should bring up a list of studios', async () =>{
await driver.get(myStuffURL);
test('Add To button should bring up a list of studios', async () => {
await navigate(myStuffURL);
await clickXpath('//div[@id="sidebar"]/ul/li[@data-tab="shared"]');
await clickXpath('//div[@data-control="add-to"]');
let dropDown = await findByXpath('//div[@class="dropdown-menu"]/ul/li');
let dropDownVisible = await dropDown.isDisplayed();
await expect(dropDownVisible).toBe(true);
const dropDown = await findByXpath('//div[@class="dropdown-menu"]/ul/li');
const dropDownVisible = await dropDown.isDisplayed();
expect(dropDownVisible).toBe(true);
});
test('+ New Project button should open the editor', async () =>{
await driver.get(myStuffURL);
test('+ New Project button should open the editor', async () => {
await navigate(myStuffURL);
await clickText('+ New Project');
let gf = await findByXpath('//img[@class="green-flag_green-flag_1kiAo"]');
let gfVisible = await gf.isDisplayed();
await expect(gfVisible).toBe(true);
await waitUntilDocumentReady();
const gf = await findByXpath('//img[@class="green-flag_green-flag_1kiAo"]');
const gfVisible = await gf.isDisplayed();
expect(gfVisible).toBe(true);
});
test('+ New Studio button should take you to the studio page', async ()=>{
await driver.get(rateLimitCheck);
await driver.get(myStuffURL);
test('+ New Studio button should take you to the studio page', async () => {
await navigate(rateLimitCheck);
await navigate(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
let tabs = await findByXpath('//div[@class="studio-tabs"]');
let tabsVisible = await tabs.isDisplayed();
await expect(tabsVisible).toBe(true);
await waitUntilDocumentReady();
const tabs = await findByXpath('//div[@class="studio-tabs"]');
const tabsVisible = await tabs.isDisplayed();
expect(tabsVisible).toBe(true);
});
test('New studio rate limited to five', async () =>{
await driver.get(rateLimitCheck);
test('New studio rate limited to five', async () => {
await navigate(rateLimitCheck);
// 1st studio
await driver.get(myStuffURL);
await navigate(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
await findByXpath('//div[@class="studio-tabs"]');
await urlMatches(/\/studios\//);
// 2nd studio
await driver.get(myStuffURL);
await navigate(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
await findByXpath('//div[@class="studio-tabs"]');
await urlMatches(/\/studios\//);
// 3rd studio
await driver.get(myStuffURL);
await navigate(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
await findByXpath('//div[@class="studio-tabs"]');
await urlMatches(/\/studios\//);
// 4th studio
await driver.get(myStuffURL);
await navigate(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
await findByXpath('//div[@class="studio-tabs"]');
await urlMatches(/\/studios\//);
// 5th studio
await driver.get(myStuffURL);
await navigate(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
await findByXpath('//div[@class="studio-tabs"]');
await urlMatches(/\/studios\//);
// 6th studio should fail
await driver.get(myStuffURL);
await navigate(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
let alertMessage = await findByXpath('//div[contains(@class, "alert-error")]');
let errVisible = await alertMessage.isDisplayed();
await expect(errVisible).toBe(true);
// findByXpath checks for both presence and visibility
const alertMessage = await findByXpath('//div[contains(@class, "alert-error")]');
expect(alertMessage).toBeTruthy();
await driver.get(rateLimitCheck);
await navigate(rateLimitCheck);
});
});

View file

@ -4,11 +4,12 @@ const SeleniumHelper = require('./selenium-helpers.js');
const {
clickXpath,
buildDriver,
findByXpath,
buildDriver
navigate
} = new SeleniumHelper();
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
jest.setTimeout(60000);
@ -20,81 +21,81 @@ describe('www-integration navbar links', () => {
});
beforeEach(async () => {
await driver.get(rootUrl);
await navigate(rootUrl);
});
afterAll(async () => await driver.quit());
afterAll(() => driver.quit());
test('Check text of navbar items', async () => {
let create = await findByXpath('//li[@class="link create"]');
let createText = await create.getText();
await expect(createText).toEqual('Create');
const create = await findByXpath('//li[@class="link create"]');
const createText = await create.getText();
expect(createText).toEqual('Create');
let explore = await findByXpath('//li[@class="link explore"]');
let exploreText = await explore.getText();
await expect(exploreText).toEqual('Explore');
const explore = await findByXpath('//li[@class="link explore"]');
const exploreText = await explore.getText();
expect(exploreText).toEqual('Explore');
let ideas = await findByXpath('//li[@class="link ideas"]');
let ideasText = await ideas.getText();
await expect(ideasText).toEqual('Ideas');
const ideas = await findByXpath('//li[@class="link ideas"]');
const ideasText = await ideas.getText();
expect(ideasText).toEqual('Ideas');
let about = await findByXpath('//li[@class="link about"]');
let aboutText = await about.getText();
await expect(aboutText).toEqual('About');
const about = await findByXpath('//li[@class="link about"]');
const aboutText = await about.getText();
expect(aboutText).toEqual('About');
let join = await findByXpath('//a[@class="registrationLink"]');
let joinText = await join.getText();
await expect(joinText).toEqual('Join Scratch');
const join = await findByXpath('//a[@class="registrationLink"]');
const joinText = await join.getText();
expect(joinText).toEqual('Join Scratch');
let signIn = await findByXpath('//li[@class="link right login-item"]/a');
let signInText = await signIn.getText();
await expect(signInText).toEqual('Sign in');
const signIn = await findByXpath('//li[@class="link right login-item"]/a');
const signInText = await signIn.getText();
expect(signInText).toEqual('Sign in');
});
test('create when signed out', async () => {
await clickXpath('//li[@class="link create"]');
let gui = await findByXpath('//div[contains(@class, "gui")]');
let guiVisible = await gui.isDisplayed();
await expect(guiVisible).toBe(true);
const gui = await findByXpath('//div[contains(@class, "gui")]');
const guiVisible = await gui.isDisplayed();
expect(guiVisible).toBe(true);
});
test('Explore link when signed out', async () => {
await clickXpath('//li[@class="link explore"]');
let banner = await findByXpath('//h1[@class="title-banner-h1"]');
let bannerText = await banner.getText();
await expect(bannerText).toEqual('Explore');
const banner = await findByXpath('//h1[@class="title-banner-h1"]');
const bannerText = await banner.getText();
expect(bannerText).toEqual('Explore');
});
test('Ideas link when signed out', async () => {
await clickXpath('//li[@class="link ideas"]');
let banner = await findByXpath('//div[contains(@class, "ideas-banner")]');
let bannerVisible = await banner.isDisplayed();
await expect(bannerVisible).toBe(true);
const banner = await findByXpath('//div[contains(@class, "ideas-banner")]');
const bannerVisible = await banner.isDisplayed();
expect(bannerVisible).toBe(true);
});
test('About link when signed out', async () => {
await clickXpath('//li[@class="link about"]');
let aboutPage = await findByXpath('//div[@class="inner about"]');
let aboutPageVisible = await aboutPage.isDisplayed();
await expect(aboutPageVisible).toBe(true);
const aboutPage = await findByXpath('//div[@class="inner about"]');
const aboutPageVisible = await aboutPage.isDisplayed();
expect(aboutPageVisible).toBe(true);
});
test('Search Bar', async () => {
let searchBar = await findByXpath('//div[contains(@class, "search-wrapper")]/div/input');
const searchBar = await findByXpath('//div[contains(@class, "search-wrapper")]/div/input');
await searchBar.sendKeys('cat');
await driver.sleep(500); // without it sends an empty string on submit
await searchBar.submit();
let banner = await findByXpath('//h1[@class="title-banner-h1"]');
let bannerText = await banner.getText();
await expect(bannerText).toEqual('Search');
const banner = await findByXpath('//h1[@class="title-banner-h1"]');
const bannerText = await banner.getText();
expect(bannerText).toEqual('Search');
});
test('Scratch Logo', async () => {
await clickXpath('//li[@class="link explore"]');
await findByXpath('//h1[@class="title-banner-h1"]');
await clickXpath('//li[@class="logo"]');
let splash = await findByXpath('//div[@class="splash"]');
let splashVisible = await splash.isDisplayed();
const splash = await findByXpath('//div[@class="splash"]');
const splashVisible = await splash.isDisplayed();
expect(splashVisible).toBe(true);
});

View file

@ -3,6 +3,7 @@
// some tests use projects owned by user #2
const SeleniumHelper = require('./selenium-helpers.js');
const {until} = require('selenium-webdriver');
import path from 'path';
const {
@ -11,33 +12,35 @@ const {
clickXpath,
findText,
findByXpath,
isSignedIn,
signIn,
navigate,
waitUntilVisible
} = new SeleniumHelper();
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
// project IDs and URLs
let unownedSharedId = process.env.UNOWNED_SHARED_PROJECT_ID || 1300006196;
let unownedSharedUrl = rootUrl + '/projects/' + unownedSharedId;
const unownedSharedId = process.env.UNOWNED_SHARED_PROJECT_ID || 1300006196;
const unownedSharedUrl = `${rootUrl}/projects/${unownedSharedId}`;
let ownedSharedId = process.env.OWNED_SHARED_PROJECT_ID || 1300009464;
let ownedSharedUrl = rootUrl + '/projects/' + ownedSharedId;
const ownedSharedId = process.env.OWNED_SHARED_PROJECT_ID || 1300009464;
const ownedSharedUrl = `${rootUrl}/projects/${ownedSharedId}`;
let ownedUnsharedID = process.env.OWNED_UNSHARED_PROJECT_ID || 1300009465;
let ownedUnsharedUrl = rootUrl + '/projects/' + ownedUnsharedID;
const ownedUnsharedID = process.env.OWNED_UNSHARED_PROJECT_ID || 1300009465;
const ownedUnsharedUrl = `${rootUrl}/projects/${ownedUnsharedID}`;
let unownedUnsharedID = process.env.UNOWNED_UNSHARED_PROJECT_ID || 1300006306;
let unownedUnsharedUrl = rootUrl + '/projects/' + unownedUnsharedID;
const unownedUnsharedID = process.env.UNOWNED_UNSHARED_PROJECT_ID || 1300006306;
const unownedUnsharedUrl = `${rootUrl}/projects/${unownedUnsharedID}`;
let unownedSharedScratch2ID = process.env.UNOWNED_SHARED_SCRATCH2_PROJECT_ID || 1300009487;
let unownedSharedScratch2Url = rootUrl + '/projects/' + unownedSharedScratch2ID;
const unownedSharedScratch2ID = process.env.UNOWNED_SHARED_SCRATCH2_PROJECT_ID || 1300009487;
const unownedSharedScratch2Url = `${rootUrl}/projects/${unownedSharedScratch2ID}`;
let ownedUnsharedScratch2ID = process.env.OWNED_UNSHARED_SCRATCH2_PROJECT_ID || 1300009488;
let ownedUnsharedScratch2Url = rootUrl + '/projects/' + ownedUnsharedScratch2ID;
const ownedUnsharedScratch2ID = process.env.OWNED_UNSHARED_SCRATCH2_PROJECT_ID || 1300009488;
const ownedUnsharedScratch2Url = `${rootUrl}/projects/${ownedUnsharedScratch2ID}`;
let username = process.env.SMOKE_USERNAME + '6';
let password = process.env.SMOKE_PASSWORD;
const username = `${process.env.SMOKE_USERNAME}6`;
const password = process.env.SMOKE_PASSWORD;
const remote = process.env.SMOKE_REMOTE || false;
@ -52,62 +55,62 @@ describe('www-integration project-page signed out', () => {
beforeAll(async () => {
// expect(projectUrl).toBe(defined);
driver = await buildDriver('www-integration project-page signed out');
await driver.get(rootUrl);
await navigate(rootUrl);
});
beforeEach(async () => {
await driver.get(unownedSharedUrl);
let gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
await navigate(unownedSharedUrl);
const gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
await waitUntilVisible(gfOverlay, driver);
});
afterAll(async () => await driver.quit());
afterAll(() => driver.quit());
// LOGGED OUT TESTS
test('Find fullscreen button', async () => {
await clickXpath('//div[starts-with(@class, "stage_green-flag-overlay")]');
await clickXpath('//img[contains(@alt, "Enter full screen mode")]');
let fullscreenGui = await findByXpath('//div[@class="guiPlayer fullscreen"]');
let guiVisible = await fullscreenGui.isDisplayed();
await expect(guiVisible).toBe(true);
const fullscreenGui = await findByXpath('//div[@class="guiPlayer fullscreen"]');
const guiVisible = await fullscreenGui.isDisplayed();
expect(guiVisible).toBe(true);
});
test.skip('Open Copy Link modal', async () => {
await clickXpath('//button[@class="button action-button copy-link-button"]');
let projectLink = await findByXpath('//input[@name="link"]');
let linkValue = await projectLink.getAttribute('value');
await expect(linkValue).toEqual(unownedSharedUrl);
const projectLink = await findByXpath('//input[@name="link"]');
const linkValue = await projectLink.getAttribute('value');
expect(linkValue).toEqual(unownedSharedUrl);
});
test('Click Username to go to profile page', async ()=> {
test('Click Username to go to profile page', async () => {
await clickXpath('//div[@class="title"]/a');
let userContent = await findByXpath('//div[@class="user-content"]');
let contentVisible = await userContent.isDisplayed();
await expect(contentVisible).toBe(true);
const userContent = await findByXpath('//div[@class="user-content"]');
const contentVisible = await userContent.isDisplayed();
expect(contentVisible).toBe(true);
});
test('click See Inside to go to the editor', async ()=> {
test('click See Inside to go to the editor', async () => {
await clickXpath('//button[@class="button button see-inside-button"]');
let infoArea = await findByXpath('//div[@class="sprite-info_sprite-info_3EyZh box_box_2jjDp"]');
let areaVisible = await infoArea.isDisplayed();
await expect(areaVisible).toBe(true);
const infoArea = await findByXpath('//div[@class="sprite-info_sprite-info_3EyZh box_box_2jjDp"]');
const areaVisible = await infoArea.isDisplayed();
expect(areaVisible).toBe(true);
});
test('click View All remixes takes you to remix page', async ()=> {
test('click View All remixes takes you to remix page', async () => {
await clickXpath('//div[@class="list-header-link"]');
let originalLink = await findByXpath('//h2/a');
let link = await originalLink.getAttribute('href');
await expect(link).toEqual(rootUrl + '/projects/' + unownedSharedId + '/');
const originalLink = await findByXpath('//h2/a');
const link = await originalLink.getAttribute('href');
expect(link).toEqual(`${rootUrl}/projects/${unownedSharedId}/`);
});
// Load an unshared project while signed out, get error
test('Load an ushared project you do not own (error)', async () => {
await driver.get(unownedUnsharedUrl);
let unavailableImage = await findByXpath('//img[@class="not-available-image"]');
await navigate(unownedUnsharedUrl);
const unavailableImage = await findByXpath('//img[@class="not-available-image"]');
await waitUntilVisible(unavailableImage, driver);
let unavailableVisible = await unavailableImage.isDisplayed();
await expect(unavailableVisible).toBe(true);
const unavailableVisible = await unavailableImage.isDisplayed();
expect(unavailableVisible).toBe(true);
});
});
@ -117,126 +120,123 @@ describe('www-integration project-page signed in', () => {
beforeAll(async () => {
// expect(projectUrl).toBe(defined);
driver = await buildDriver('www-integration project-page signed in');
await driver.get(rootUrl);
await driver.sleep(1000);
await signIn(username, password);
await findByXpath('//span[contains(@class, "profile-name")]');
});
beforeEach(async () => {
await driver.get(rootUrl);
// The browser may or may not retain cookies between tests, depending on configuration.
await navigate(rootUrl);
if (!await isSignedIn()) {
await signIn(username, password);
}
});
afterAll(async () => await driver.quit());
afterAll(() => driver.quit());
// LOGGED in TESTS
// Load a shared project you own
test('Load a shared project you own', async () => {
await driver.get(ownedSharedUrl);
let gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
await navigate(ownedSharedUrl);
const gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
await waitUntilVisible(gfOverlay, driver);
let gfVisible = await gfOverlay.isDisplayed();
await expect(gfVisible).toBe(true);
const gfVisible = await gfOverlay.isDisplayed();
expect(gfVisible).toBe(true);
});
// Load a shared project you don't own
test('Load a shared project you do not own', async () => {
await driver.get(unownedSharedUrl);
let gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
await navigate(unownedSharedUrl);
const gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
await waitUntilVisible(gfOverlay, driver);
let gfVisible = await gfOverlay.isDisplayed();
await expect(gfVisible).toBe(true);
const gfVisible = await gfOverlay.isDisplayed();
expect(gfVisible).toBe(true);
});
// Load an unshared project you own
test('Load an unshared project you own', async () => {
await driver.get(ownedUnsharedUrl);
let gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
await navigate(ownedUnsharedUrl);
const gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
await waitUntilVisible(gfOverlay, driver);
let gfVisible = await gfOverlay.isDisplayed();
await expect(gfVisible).toBe(true);
const gfVisible = await gfOverlay.isDisplayed();
expect(gfVisible).toBe(true);
});
// Load an unshared project you don't own, get error
test('Load an ushared project you do not own (error)', async () => {
await driver.get(unownedUnsharedUrl);
let unavailableImage = await findByXpath('//img[@class="not-available-image"]');
await navigate(unownedUnsharedUrl);
const unavailableImage = await findByXpath('//img[@class="not-available-image"]');
await waitUntilVisible(unavailableImage, driver);
let unavailableVisible = await unavailableImage.isDisplayed();
await expect(unavailableVisible).toBe(true);
const unavailableVisible = await unavailableImage.isDisplayed();
expect(unavailableVisible).toBe(true);
});
// Load a shared scratch 2 project you don't own
test('Load a shared scratch 2 project you do not own', async () => {
await driver.get(unownedSharedScratch2Url);
let gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
await navigate(unownedSharedScratch2Url);
const gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
await waitUntilVisible(gfOverlay, driver);
let gfVisible = await gfOverlay.isDisplayed();
await expect(gfVisible).toBe(true);
const gfVisible = await gfOverlay.isDisplayed();
expect(gfVisible).toBe(true);
});
// Load an unshared scratch 2 project you own
test('Load an unshared scratch 2 project you own', async () => {
await driver.get(ownedUnsharedScratch2Url);
let gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
await navigate(ownedUnsharedScratch2Url);
const gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
await waitUntilVisible(gfOverlay, driver);
let gfVisible = await gfOverlay.isDisplayed();
await expect(gfVisible).toBe(true);
const gfVisible = await gfOverlay.isDisplayed();
expect(gfVisible).toBe(true);
});
});
describe('www-integration project-creation signed in', () => {
beforeAll(async () => {
// expect(projectUrl).toBe(defined);
driver = await buildDriver('www-integration project-creation signed in');
await driver.get(rootUrl);
await driver.sleep(1000);
await signIn(username, password);
await findByXpath('//span[contains(@class, "profile-name")]');
// SauceLabs doesn't have access to the sb3 used in 'load project from file' test
// https://support.saucelabs.com/hc/en-us/articles/115003685593-Uploading-Files-to-a-Sauce-Labs-Virtual-Machine-during-a-Test
if (remote) {
await driver.get('https://github.com/LLK/scratch-www/blob/develop/test/fixtures/project1.sb3');
await clickText('Download');
await navigate('https://github.com/scratchfoundation/scratch-www/blob/develop/test/fixtures/project1.sb3');
await clickXpath('//button[@data-testid="download-raw-button"]');
await driver.sleep(3000);
}
});
beforeEach(async () => {
await driver.get(rootUrl);
// The browser may or may not retain cookies between tests, depending on configuration.
await navigate(rootUrl);
if (!await isSignedIn()) {
await signIn(username, password);
}
});
afterAll(async () => await driver.quit());
afterAll(() => driver.quit());
test('make a copy of a project', async () => {
await driver.get(ownedUnsharedUrl + '/editor');
let gf = await findByXpath('//img[@class="green-flag_green-flag_1kiAo"]');
await gf.isDisplayed();
await navigate(`${ownedUnsharedUrl}/editor`);
await clickXpath(FILE_MENU_XPATH);
await clickText('Save as a copy');
let successAlert = await findText('Project saved as a copy.');
let alertVisible = await successAlert.isDisplayed();
await expect(alertVisible).toBe(true);
const successAlert = await findText('Project saved as a copy.');
const alertVisible = await successAlert.isDisplayed();
expect(alertVisible).toBe(true);
await driver.sleep(1000);
let infoArea = await findByXpath('//div[@class="sprite-info_sprite-info_3EyZh box_box_2jjDp"]');
let areaVisible = await infoArea.isDisplayed();
await expect(areaVisible).toBe(true);
const infoArea = await findByXpath('//div[@class="sprite-info_sprite-info_3EyZh box_box_2jjDp"]');
const areaVisible = await infoArea.isDisplayed();
expect(areaVisible).toBe(true);
});
test('remix a project', async () => {
await driver.get(unownedSharedUrl);
let gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
await navigate(unownedSharedUrl);
const gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
await waitUntilVisible(gfOverlay, driver);
await clickXpath('//button[@class="button remix-button"]');
let successAlert = await findText('Project saved as a remix.');
let alertVisible = await successAlert.isDisplayed();
await expect(alertVisible).toBe(true);
const successAlert = await findText('Project saved as a remix.');
const alertVisible = await successAlert.isDisplayed();
expect(alertVisible).toBe(true);
await driver.sleep(1000);
let infoArea = await findByXpath('//div[@class="sprite-info_sprite-info_3EyZh box_box_2jjDp"]');
let areaVisible = await infoArea.isDisplayed();
await expect(areaVisible).toBe(true);
const infoArea = await findByXpath('//div[@class="sprite-info_sprite-info_3EyZh box_box_2jjDp"]');
const areaVisible = await infoArea.isDisplayed();
expect(areaVisible).toBe(true);
});
test('load project from file', async () => {
@ -245,29 +245,29 @@ describe('www-integration project-creation signed in', () => {
'/Users/chef/Downloads/project1.sb3' :
path.resolve(__dirname, '../fixtures/project1.sb3');
// upload file
// create a new project so there's unsaved content to trigger an alert
await clickXpath('//li[@class="link create"]');
let gf = await findByXpath('//img[@class="green-flag_green-flag_1kiAo"]');
await gf.isDisplayed();
// upload file
await clickXpath(FILE_MENU_XPATH);
await clickText('Load from your computer');
await driver.sleep(1000);
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
await input.sendKeys(projectPath);
// accept alert
let alert = await driver.switchTo().alert();
await driver.wait(until.alertIsPresent());
const alert = await driver.switchTo().alert();
await alert.accept();
// check that project is loaded
let spriteTile = await findText('project1-sprite');
let spriteTileVisible = await spriteTile.isDisplayed();
await expect(spriteTileVisible).toBe(true);
const spriteTile = await findText('project1-sprite');
const spriteTileVisible = await spriteTile.isDisplayed();
expect(spriteTileVisible).toBe(true);
// check that gui is still there after some time has passed
await driver.sleep(1000);
let infoArea = await findByXpath('//div[@class="sprite-info_sprite-info_3EyZh box_box_2jjDp"]');
let areaVisible = await infoArea.isDisplayed();
await expect(areaVisible).toBe(true);
const infoArea = await findByXpath('//div[@class="sprite-info_sprite-info_3EyZh box_box_2jjDp"]');
const areaVisible = await infoArea.isDisplayed();
expect(areaVisible).toBe(true);
});
});

View file

@ -6,10 +6,11 @@ const {
buildDriver,
clickXpath,
findByXpath,
getKey
getKey,
navigate
} = new SeleniumHelper();
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
jest.setTimeout(60000);
@ -21,38 +22,38 @@ describe('www-integration search', () => {
});
beforeEach(async () => {
await driver.get(rootUrl);
await navigate(rootUrl);
});
afterAll(async () => await driver.quit());
afterAll(() => driver.quit());
test('search converts spaces', async () => {
let searchBar = await findByXpath('//div[contains(@class, "search-wrapper")]/div/input');
await searchBar.sendKeys('Test search string' + getKey('ENTER'));
const searchBar = await findByXpath('//div[contains(@class, "search-wrapper")]/div/input');
await searchBar.sendKeys(`Test search string${getKey('ENTER')}`);
// check url
let url = await driver.getCurrentUrl();
await expect(url).toMatch(/^.*\?q=Test%20search%20string$/);
const url = await driver.getCurrentUrl();
expect(url).toMatch(/^.*\?q=Test%20search%20string$/);
});
test('Search escapes symbols', async () => {
let searchBar = await findByXpath('//div[contains(@class, "search-wrapper")]/div/input');
await searchBar.sendKeys('100% pen' + getKey('ENTER'));
const searchBar = await findByXpath('//div[contains(@class, "search-wrapper")]/div/input');
await searchBar.sendKeys(`100% pen${getKey('ENTER')}`);
// check url
let url = await driver.getCurrentUrl();
await expect(url).toMatch(/^.*\?q=100%25%20pen$/);
const url = await driver.getCurrentUrl();
expect(url).toMatch(/^.*\?q=100%25%20pen$/);
});
test('Switching to studios maintains search string', async () => {
let searchBar = await findByXpath('//div[contains(@class, "search-wrapper")]/div/input');
await searchBar.sendKeys('100% pen' + getKey('ENTER'));
const searchBar = await findByXpath('//div[contains(@class, "search-wrapper")]/div/input');
await searchBar.sendKeys(`100% pen${getKey('ENTER')}`);
// switch to studios tab
clickXpath('//a/li/span[contains(text(),"Studios")]');
await clickXpath('//button//*[contains(text(),"Studios")]');
// check url
let url = await driver.getCurrentUrl();
await expect(url).toMatch(/^.*\?q=100%25%20pen$/);
const url = await driver.getCurrentUrl();
expect(url).toMatch(/^.*\?q=100%25%20pen$/);
});
});

View file

@ -1,3 +1,5 @@
jest.setTimeout(30000); // eslint-disable-line no-undef
const webdriver = require('selenium-webdriver');
const {PageLoadStrategy} = require('selenium-webdriver/lib/capabilities');
const bindAll = require('lodash.bindall');
@ -6,14 +8,116 @@ const chromedriverVersion = require('chromedriver').version;
const headless = process.env.SMOKE_HEADLESS || false;
const remote = process.env.SMOKE_REMOTE || false;
const ci = process.env.CI || false;
const usingCircle = process.env.CIRCLECI || false;
const buildID = process.env.CIRCLE_BUILD_NUM || '0000';
const ciBuildPrefix = process.env.CI ?
`CI #${process.env.GITHUB_RUN_ID}/${process.env.GITHUB_RUN_ATTEMPT}` :
''; // no prefix if not in CI
const {SAUCE_USERNAME, SAUCE_ACCESS_KEY} = process.env;
const {By, Key, until} = webdriver;
// The main reason for this timeout is so that we can control the timeout message and report details;
// if we hit the Jasmine default timeout then we get a terse message that we can't control.
// The Jasmine default timeout is 30 seconds so make sure this is lower.
const DEFAULT_TIMEOUT_MILLISECONDS = 20 * 1000;
// This removes confusing `regenerator-runtime` noise from stack traces.
// This is V8-specific code. Please don't use it in a browser or any production code.
const oldPrepareStackTrace = Error.prepareStackTrace;
Error.prepareStackTrace = function (error, stack) {
stack = stack.filter(callsite => {
const filename = callsite.getFileName();
return filename && !filename.includes('regenerator-runtime');
});
if (oldPrepareStackTrace) {
return oldPrepareStackTrace(error, stack);
}
return [
`${error.constructor.name}: ${error.message}`,
...stack.map(callsite => ` at ${callsite.toString()}`)
].join('\n');
};
/**
* An error thrown by SeleniumHelper.
* @extends Error
*/
class SeleniumHelperError extends Error {
/**
* Instantiate a new SeleniumHelperError.
* @param {string} message The error message for this layer.
* @param {Array} [kvList] Optional keys & values to add to the error message, for example to capture arguments.
* @example
* const e = new SeleniumHelperError('something failed', [{xpath}, {somethingElse}])
* try {
* doThings();
* } catch (inner) {
* throw await e.chain(inner, driver);
* }
*/
constructor (message, kvList = []) {
const baseMessage = [
message,
...kvList.map(kv => ` ${Object.keys(kv)[0]}: ${Object.values(kv)[0]}`)
].join('\n');
super(baseMessage);
Object.setPrototypeOf(this, SeleniumHelperError.prototype); // see https://stackoverflow.com/a/41102306
this.name = 'SeleniumHelperError';
Error.captureStackTrace(this, this.constructor);
}
/**
* Add a new layer to the error chain.
* Collects context from the webdriver if it is present AND this is the innermost `SeleniumHelperError`.
* @param {Error|SeleniumHelperError} innerError The error to add to the chain.
* @param {webdriver.ThenableWebDriver} [driver] Optional webdriver instance to collect context from.
* @returns {Promise<SeleniumHelperError>} This error, with the new layer added.
*/
async chain (innerError, driver) {
const messageLines = [
this.message,
innerError.message
];
// If the inner error has already collected context, don't collect it again.
if (driver && !(innerError && innerError.collectContext)) {
await this.collectContext(messageLines, driver);
}
this.message = messageLines.join('\n');
return this;
}
/**
* Collect error context from the webdriver.
* @param {Array<string>} messageLines Add context lines to this array.
* @param {webdriver.ThenableWebDriver} driver The webdriver instance to collect context from.
* @returns {Promise} A promise that resolves when the context is collected.
*/
async collectContext (messageLines, driver) {
// It would be really nice to wait until `message` time to collect all this information,
// but that's not an option because of all these async calls.
const [
url,
title,
// pageSource,
logEntries
] = await Promise.all([
driver.getCurrentUrl(),
driver.getTitle(),
// driver.getPageSource(),
driver.manage()
.logs()
.get('browser')
]);
messageLines.push(
`Browser URL: ${url}`,
`Browser title: ${title}`,
'Browser logs:',
'*****',
...logEntries.map(entry => entry.message),
'*****'
// 'Browser page source:', '*****', pageSource, '*****'
);
}
}
class SeleniumHelper {
constructor () {
bindAll(this, [
@ -31,17 +135,33 @@ class SeleniumHelper {
'getDriver',
'getLogs',
'getSauceDriver',
'isSignedIn',
'navigate',
'signIn',
'urlMatches',
'waitUntilDocumentReady',
'waitUntilGone'
]);
// Tests call this static function as if it were a method on an instance.
this.waitUntilVisible = SeleniumHelper.waitUntilVisible;
// this type declaration suppresses IDE type warnings throughout this file
/** @type {webdriver.ThenableWebDriver} */
this.driver = null;
}
/**
* Build a new webdriver instance. This will use Sauce Labs if the SMOKE_REMOTE environment variable is 'true', or
* `chromedriver` otherwise.
* @param {string} name The name to give to Sauce Labs.
* @returns {webdriver.ThenableWebDriver} The new webdriver instance.
*/
buildDriver (name) {
if (remote === 'true'){
let nameToUse;
if (ci === 'true'){
let ciName = usingCircle ? 'circleCi ' : 'unknown ';
nameToUse = ciName + buildID + ' : ' + name;
if (ciBuildPrefix){
nameToUse = `${ciBuildPrefix}: ${name}`;
} else {
nameToUse = name;
}
@ -52,9 +172,14 @@ class SeleniumHelper {
return this.driver;
}
/**
* Build a new webdriver instance using `chromedriver`.
* You should probably use `buildDriver` instead.
* @returns {webdriver.ThenableWebDriver} The new webdriver instance.
*/
getDriver () {
const chromeCapabilities = webdriver.Capabilities.chrome();
let args = [];
const args = [];
if (headless) {
args.push('--headless');
args.push('window-size=1024,1680');
@ -62,13 +187,16 @@ class SeleniumHelper {
}
chromeCapabilities.set('chromeOptions', {args});
chromeCapabilities.setPageLoadStrategy(PageLoadStrategy.EAGER);
let driver = new webdriver.Builder()
const driver = new webdriver.Builder()
.forBrowser('chrome')
.withCapabilities(chromeCapabilities)
.build();
return driver;
}
/**
* @returns {string} The version of chromedriver being used.
*/
getChromeVersionNumber () {
const versionFinder = /\d+\.\d+/;
const versionArray = versionFinder.exec(chromedriverVersion);
@ -78,16 +206,24 @@ class SeleniumHelper {
return versionArray[0];
}
/**
* Build a new webdriver instance using Sauce Labs.
* You should probably use `buildDriver` instead.
* @param {string} username The Sauce Labs username.
* @param {string} accessKey The Sauce Labs access key.
* @param {string} name The name to give to Sauce Labs.
* @returns {webdriver.ThenableWebDriver} The new webdriver instance.
*/
getSauceDriver (username, accessKey, name) {
const chromeVersion = this.getChromeVersionNumber();
// Driver configs can be generated with the Sauce Platform Configurator
// https://wiki.saucelabs.com/display/DOCS/Platform+Configurator
let driverConfig = {
const driverConfig = {
browserName: 'chrome',
platform: 'macOS 10.15',
version: chromeVersion
};
var driver = new webdriver.Builder()
const driver = new webdriver.Builder()
.withCapabilities({
browserName: driverConfig.browserName,
platform: driverConfig.platform,
@ -96,113 +232,363 @@ class SeleniumHelper {
accessKey: accessKey,
name: name
})
.usingServer(`http://${username}:${accessKey
}@ondemand.saucelabs.com:80/wd/hub`)
.usingServer(`http://${username}:${accessKey}@ondemand.saucelabs.com:80/wd/hub`)
.build();
return driver;
}
/**
* Retrieves a key string by name.
* @example
* getKey('ENTER') // => '\uE007'
* @param {string} keyName The name of the key to retrieve.
* @returns {string} The key.
*/
getKey (keyName) {
return Key[keyName];
}
findByXpath (xpath, timeoutMessage = `findByXpath timed out for path: ${xpath}`) {
return this.driver.wait(until.elementLocated(By.xpath(xpath)), DEFAULT_TIMEOUT_MILLISECONDS, timeoutMessage)
.then(el => (
this.driver.wait(el.isDisplayed(), DEFAULT_TIMEOUT_MILLISECONDS, `${xpath} is not visible`)
.then(() => el)
));
/**
* Wait until the document is ready (i.e. the document.readyState is 'complete')
* @returns {Promise} A promise that resolves when the document is ready
*/
async waitUntilDocumentReady () {
const outerError = new SeleniumHelperError('waitUntilDocumentReady failed');
try {
await this.driver.wait(
async () => await this.driver.executeScript('return document.readyState;') === 'complete',
DEFAULT_TIMEOUT_MILLISECONDS
);
} catch (cause) {
throw await outerError.chain(cause, this.driver);
}
}
waitUntilGone (element) {
return this.driver.wait(until.stalenessOf(element));
/**
* Navigate to the given URL and wait until the document is ready.
* The Selenium docs say the promise returned by `driver.get()` "will be resolved when the document has finished
* loading." In practice, that doesn't mean the page is ready for testing. I suspect it comes down to the
* difference between "interactive" and "complete" (or `DOMContentLoaded` and `load`).
* @param {string} url The URL to navigate to.
* @returns {Promise} A promise that resolves when the document is ready
*/
async navigate (url) {
const outerError = new SeleniumHelperError('navigate failed', [{url}]);
try {
await this.driver.get(url);
await this.waitUntilDocumentReady();
} catch (cause) {
throw await outerError.chain(cause, this.driver);
}
}
clickXpath (xpath) {
return this.findByXpath(xpath).then(el => el.click());
/**
* Find an element by xpath.
* @param {string} xpath The xpath to search for.
* @returns {Promise<webdriver.WebElement>} A promise that resolves to the element.
*/
async findByXpath (xpath) {
const outerError = new SeleniumHelperError('findByXpath failed', [{xpath}]);
try {
const el = await this.driver.wait(until.elementLocated(By.xpath(xpath)), DEFAULT_TIMEOUT_MILLISECONDS);
await this.driver.wait(el.isDisplayed(), DEFAULT_TIMEOUT_MILLISECONDS);
return el;
} catch (cause) {
throw await outerError.chain(cause, this.driver);
}
}
clickText (text) {
return this.clickXpath(`//*[contains(text(), '${text}')]`);
/**
* @param {webdriver.WebElement} element Wait until this element is gone (stale).
* @returns {Promise} A promise that resolves when the element is gone.
*/
async waitUntilGone (element) {
const outerError = new SeleniumHelperError('waitUntilGone failed', [{element}]);
try {
await this.driver.wait(until.stalenessOf(element), DEFAULT_TIMEOUT_MILLISECONDS);
} catch (cause) {
throw await outerError.chain(cause, this.driver);
}
}
findText (text) {
return this.driver.wait(until.elementLocated(By.xpath(`//*[contains(text(), '${text}')]`), 5 * 1000));
}
clickButton (text) {
return this.clickXpath(`//button[contains(text(), '${text}')]`);
}
findByCss (css) {
return this.driver.wait(until.elementLocated(By.css(css), 1000 * 5));
}
clickCss (css) {
return this.findByCss(css).then(el => el.click());
}
dragFromXpathToXpath (startXpath, endXpath) {
return this.findByXpath(startXpath).then(startEl => {
return this.findByXpath(endXpath).then(endEl => {
return this.driver.actions()
.dragAndDrop(startEl, endEl)
.perform();
});
});
}
// must be used on a www page
async signIn (username, password) {
await this.clickXpath('//li[@class="link right login-item"]/a');
let name = await this.findByXpath('//input[@id="frc-username-1088"]');
await name.sendKeys(username);
let word = await this.findByXpath('//input[@id="frc-password-1088"]');
await word.sendKeys(password + this.getKey('ENTER'));
await this.findByXpath('//span[contains(@class, "profile-name")]');
}
urlMatches (regex) {
return this.driver.wait(until.urlMatches(regex), 1000 * 5);
}
getLogs (whitelist) {
return this.driver.manage()
.logs()
.get('browser')
.then((entries) => {
return entries.filter((entry) => {
const message = entry.message;
for (let i = 0; i < whitelist.length; i++) {
if (message.indexOf(whitelist[i]) !== -1) {
// eslint-disable-next-line no-console
// console.warn('Ignoring whitelisted error: ' + whitelist[i]);
return false;
} else if (entry.level !== 'SEVERE') {
// eslint-disable-next-line no-console
// console.warn('Ignoring non-SEVERE entry: ' + message);
return false;
/**
* Wait until an element can be found by the provided xpath, then click on it.
* @param {string} xpath The xpath to click.
* @returns {Promise} A promise that resolves when the element is clicked.
*/
async clickXpath (xpath) {
const outerError = new SeleniumHelperError('clickXpath failed', [{xpath}]);
try {
return await this.driver.wait(new webdriver.WebElementCondition(
'for element click to succeed',
async () => {
const element = await this.findByXpath(xpath);
if (!element) {
return null;
}
try {
await element.click();
return element;
} catch (e) {
if (e instanceof webdriver.error.ElementClickInterceptedError) {
// something is in front of the element we want to click
// probably the loading screen
// this is the main reason for using wait()
return null;
}
return true;
throw e;
}
}
), DEFAULT_TIMEOUT_MILLISECONDS);
} catch (cause) {
throw await outerError.chain(cause, this.driver);
}
}
/**
* Wait until an element can be found by the provided text, then click on it.
* @param {string} text The text to click.
* @returns {Promise} A promise that resolves when the element is clicked.
*/
async clickText (text) {
const outerError = new SeleniumHelperError('clickText failed', [{text}]);
try {
await this.clickXpath(`//*[contains(text(), '${text}')]`);
} catch (cause) {
throw await outerError.chain(cause, this.driver);
}
}
/**
* Wait until an element can be found by the provided text.
* @param {string} text The text to find.
* @returns {Promise<webdriver.WebElement>} The element containing the text.
*/
async findText (text) {
const outerError = new SeleniumHelperError('findText failed', [{text}]);
try {
return await this.driver.wait(
until.elementLocated(By.xpath(`//*[contains(text(), '${text}')]`)),
DEFAULT_TIMEOUT_MILLISECONDS
);
} catch (cause) {
throw await outerError.chain(cause, this.driver);
}
}
/**
* Wait until a button can be found by the provided text, then click on it.
* @param {string} text The button text to find and click.
* @returns {Promise} A promise that resolves when the button is clicked.
*/
async clickButton (text) {
const outerError = new SeleniumHelperError('clickButton failed', [{text}]);
try {
await this.clickXpath(`//button[contains(text(), '${text}')]`);
} catch (cause) {
throw await outerError.chain(cause, this.driver);
}
}
/**
* Wait until an element can be found by the provided CSS selector.
* @param {string} css The CSS selector to find.
* @returns {Promise<webdriver.WebElement>} The element matching the CSS selector.
*/
async findByCss (css) {
const outerError = new SeleniumHelperError('findByCss failed', [{css}]);
try {
return await this.driver.wait(until.elementLocated(By.css(css)), DEFAULT_TIMEOUT_MILLISECONDS);
} catch (cause) {
throw await outerError.chain(cause, this.driver);
}
}
/**
* Wait until an element can be found by the provided CSS selector, then click on it.
* @param {string} css The CSS selector to find and click.
* @returns {Promise} A promise that resolves when the element is clicked.
*/
async clickCss (css) {
const outerError = new SeleniumHelperError('clickCss failed', [{css}]);
try {
const el = await this.findByCss(css);
await el.click();
} catch (cause) {
throw await outerError.chain(cause, this.driver);
}
}
/**
* Wait until the two elements can be found, then drag from the first to the second.
* @param {string} startXpath The xpath to drag from.
* @param {string} endXpath The xpath to drag to.
* @returns {Promise} A promise that resolves when the drag is complete.
*/
async dragFromXpathToXpath (startXpath, endXpath) {
const outerError = new SeleniumHelperError('dragFromXpathToXpath failed', [{startXpath}, {endXpath}]);
try {
const startEl = await this.findByXpath(startXpath);
const endEl = await this.findByXpath(endXpath);
await this.driver.actions()
.dragAndDrop(startEl, endEl)
.perform();
} catch (cause) {
throw await outerError.chain(cause, this.driver);
}
}
/**
* @returns {string} The xpath to the login button, which is present only if signed out.
*/
getPathForLogin () {
return '//li[@class="link right login-item"]/a';
}
/**
* @returns {string} The xpath to the profile name, which is present only if signed in.
*/
getPathForProfileName () {
return '//span[contains(@class, "profile-name")]';
}
/**
* @returns {Promise<boolean>} True if the user is signed in, false otherwise.
* @throws {SeleniumHelperError} If the user's sign-in state cannot be determined.
*/
async isSignedIn () {
const outerError = new SeleniumHelperError('isSignedIn failed');
try {
const state = await this.driver.wait(
() => this.driver.executeScript(
`
if (document.evaluate(arguments[0], document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)
.singleNodeValue) {
return 'signed in';
}
if (document.evaluate(arguments[1], document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)
.singleNodeValue) {
return 'signed out';
}
`,
this.getPathForProfileName(),
this.getPathForLogin()
),
DEFAULT_TIMEOUT_MILLISECONDS
);
switch (state) {
case 'signed in':
return true;
case 'signed out':
return false;
default:
throw new Error('Could not determine whether or not user is signed in');
}
} catch (cause) {
throw await outerError.chain(cause, this.driver);
}
}
/**
* Sign in on a `scratch-www` page.
* @param {string} username The username to sign in with.
* @param {string} password The password to sign in with.
* @returns {Promise} A promise that resolves when the user is signed in.
*/
async signIn (username, password) {
const outerError = new SeleniumHelperError('signIn failed', [
{username},
{password: password ? 'provided' : 'absent'}
]);
try {
await this.clickXpath(this.getPathForLogin());
const name = await this.findByXpath('//input[@id="frc-username-1088"]');
await name.sendKeys(username);
const word = await this.findByXpath('//input[@id="frc-password-1088"]');
await word.sendKeys(password + this.getKey('ENTER'));
await this.waitUntilDocumentReady();
await this.findByXpath(this.getPathForProfileName());
} catch (cause) {
throw await outerError.chain(cause, this.driver);
}
}
/**
* Wait until the URL matches the provided regex.
* @param {RegExp} regex The regex to match the url against.
* @returns {Promise} A promise that resolves when the url matches the regex.
*/
async urlMatches (regex) {
const outerError = new SeleniumHelperError('urlMatches failed', [{regex}]);
try {
await this.driver.wait(until.urlMatches(regex), DEFAULT_TIMEOUT_MILLISECONDS);
} catch (cause) {
throw await outerError.chain(cause, this.driver);
}
}
/**
* Get selected browser log entries.
* @param {Array<string>} whitelist A list of log strings to allow.
* @returns {Promise<Array<webdriver.logging.Entry>>} A promise that resolves to the log entries.
*/
async getLogs (whitelist) {
const outerError = new SeleniumHelperError('getLogs failed', [{whitelist}]);
try {
const entries = await this.driver.manage()
.logs()
.get('browser');
return entries.filter(entry => {
const message = entry.message;
for (const element of whitelist) {
if (message.indexOf(element) !== -1) {
// eslint-disable-next-line no-console
// console.warn('Ignoring whitelisted error: ' + whitelist[i]);
return false;
} else if (entry.level !== 'SEVERE') { // WARNING: this doesn't do what it looks like it does!
// eslint-disable-next-line no-console
// console.warn('Ignoring non-SEVERE entry: ' + message);
return false;
}
return true;
});
}
return true;
});
}
async containsClass (element, cl) {
let classes = await element.getAttribute('class');
let classList = classes.split(' ');
if (classList.includes(cl)){
return true;
} catch (cause) {
throw await outerError.chain(cause, this.driver);
}
return false;
}
async waitUntilVisible (element, driver) {
await driver.wait(until.elementIsVisible(element));
/**
* Check if an element's class attribute contains a given class.
* @param {webdriver.WebElement} element The element to check.
* @param {string} cl The class to check for.
* @returns {Promise<boolean>} True if the element's class attribute contains the given class, false otherwise.
*/
async containsClass (element, cl) {
const outerError = new SeleniumHelperError('containsClass failed', [{element}, {cl}]);
try {
const classes = await element.getAttribute('class');
const classList = classes.split(' ');
return classList.includes(cl);
} catch (cause) {
throw await outerError.chain(cause, this.driver);
}
}
/**
* @param {webdriver.WebElement} element Wait until this element is visible.
* @param {webdriver.ThenableWebDriver} driver The webdriver instance.
* @returns {Promise} A promise that resolves when the element is visible.
*/
static async waitUntilVisible (element, driver) {
const outerError = new SeleniumHelperError('waitUntilVisible failed', [{element}]);
try {
await driver.wait(until.elementIsVisible(element), DEFAULT_TIMEOUT_MILLISECONDS);
} catch (cause) {
throw await outerError.chain(cause, driver);
}
}
}
module.exports = SeleniumHelper;

View file

@ -9,15 +9,16 @@ const {
clickXpath,
findByXpath,
getKey,
navigate,
signIn,
waitUntilVisible
} = new SeleniumHelper();
let username = process.env.SMOKE_USERNAME;
let password = process.env.SMOKE_PASSWORD;
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
let scratchr2url = rootUrl + '/users/' + username;
let wwwURL = rootUrl;
const username = process.env.SMOKE_USERNAME;
const password = process.env.SMOKE_PASSWORD;
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
const scratchr2url = `${rootUrl}/users/${username}`;
const wwwURL = rootUrl;
jest.setTimeout(60000);
@ -34,45 +35,45 @@ describe('www-integration sign-in-and-out', () => {
describe('sign in', () => {
afterEach(async () => {
await driver.get(wwwURL);
await navigate(wwwURL);
await clickXpath('//div[@class="account-nav"]');
await clickText('Sign out');
});
test('sign in on www', async () => {
await driver.get(wwwURL);
await navigate(wwwURL);
await driver.sleep(1000);
await clickXpath('//li[@class="link right login-item"]/a');
let name = await findByXpath('//input[@id="frc-username-1088"]');
const name = await findByXpath('//input[@id="frc-username-1088"]');
await name.sendKeys(username);
let word = await findByXpath('//input[@id="frc-password-1088"]');
const word = await findByXpath('//input[@id="frc-password-1088"]');
await word.sendKeys(password);
await driver.sleep(500);
await clickXpath('//button[contains(@class, "button") and ' +
'contains(@class, "submit-button") and contains(@class, "white")]');
await driver.sleep(500);
let element = await findByXpath('//span[contains(@class, "profile-name")]');
let text = await element.getText();
await expect(text.toLowerCase()).toEqual(username.toLowerCase());
const element = await findByXpath('//span[contains(@class, "profile-name")]');
const text = await element.getText();
expect(text.toLowerCase()).toEqual(username.toLowerCase());
});
test('sign in on scratchr2', async () => {
await driver.get(scratchr2url);
await navigate(scratchr2url);
await clickXpath('//li[@class="sign-in dropdown"]/span');
let name = await findByXpath('//input[@id="login_dropdown_username"]');
const name = await findByXpath('//input[@id="login_dropdown_username"]');
await name.sendKeys(username);
let word = await findByXpath('//input[@name="password"]');
const word = await findByXpath('//input[@name="password"]');
await word.sendKeys(password);
await clickButton('Sign in');
let element = await findByXpath('//span[@class="user-name dropdown-toggle"]');
let text = await element.getText();
await expect(text.toLowerCase()).toEqual(username.toLowerCase());
const element = await findByXpath('//span[@class="user-name dropdown-toggle"]');
const text = await element.getText();
expect(text.toLowerCase()).toEqual(username.toLowerCase());
});
});
describe('sign out', () => {
beforeEach(async () => {
await driver.get(wwwURL);
await navigate(wwwURL);
await signIn(username, password);
await driver.sleep(500);
});
@ -80,73 +81,73 @@ describe('www-integration sign-in-and-out', () => {
test('sign out on www', async () => {
await clickXpath('//a[contains(@class, "user-info")]');
await clickText('Sign out');
let element = await findByXpath('//li[@class="link right login-item"]/a/span');
let text = await element.getText();
await expect(text.toLowerCase()).toEqual('Sign In'.toLowerCase());
const element = await findByXpath('//li[@class="link right login-item"]/a/span');
const text = await element.getText();
expect(text.toLowerCase()).toEqual('Sign In'.toLowerCase());
});
test('sign out on scratchr2', async () => {
await driver.get(scratchr2url);
await navigate(scratchr2url);
await clickXpath('//span[@class="user-name dropdown-toggle"]');
await clickXpath('//li[@id="logout"]');
let element = await findByXpath('//li[@class="link right login-item"]/a/span');
let text = await element.getText();
await expect(text.toLowerCase()).toEqual('Sign In'.toLowerCase());
const element = await findByXpath('//li[@class="link right login-item"]/a/span');
const text = await element.getText();
expect(text.toLowerCase()).toEqual('Sign In'.toLowerCase());
});
});
describe('login failures', async () => {
describe('login failures', () => {
test('sign in with no password in Scratchr2', async () => {
let nonsenseUsername = Math.random().toString(36)
const nonsenseUsername = Math.random().toString(36)
.replace(/[^a-z]+/g, '')
.substr(0, 5);
await driver.get(scratchr2url);
await navigate(scratchr2url);
await clickXpath('//li[@class="sign-in dropdown"]/span');
let name = await findByXpath('//input[@id="login_dropdown_username"]');
const name = await findByXpath('//input[@id="login_dropdown_username"]');
await name.sendKeys(nonsenseUsername + getKey('ENTER'));
// find error
let error = await findByXpath('//form[@id="login"]//div[@class="error"]');
const error = await findByXpath('//form[@id="login"]//div[@class="error"]');
await waitUntilVisible(error, driver);
let errorText = await error.getText();
await expect(errorText).toEqual('This field is required.');
const errorText = await error.getText();
expect(errorText).toEqual('This field is required.');
});
test('sign in with wrong username', async () => {
let nonsenseUsername = Math.random().toString(36)
const nonsenseUsername = Math.random().toString(36)
.replace(/[^a-z]+/g, '')
.substr(0, 5);
await driver.get(scratchr2url);
await navigate(scratchr2url);
await clickXpath('//li[@class="sign-in dropdown"]/span');
let name = await findByXpath('//input[@id="login_dropdown_username"]');
const name = await findByXpath('//input[@id="login_dropdown_username"]');
await name.sendKeys(nonsenseUsername);
let word = await findByXpath('//input[@name="password"]');
const word = await findByXpath('//input[@name="password"]');
await word.sendKeys(password + getKey('ENTER'));
// find error
let error = await findByXpath('//form[@id="login"]//div[@class="error"]');
const error = await findByXpath('//form[@id="login"]//div[@class="error"]');
await waitUntilVisible(error, driver);
let errorText = await error.getText();
await expect(errorText).toEqual('Incorrect username or password.');
const errorText = await error.getText();
expect(errorText).toEqual('Incorrect username or password.');
});
test('sign in with wrong password', async () => {
let nonsensePassword = Math.random().toString(36)
const nonsensePassword = Math.random().toString(36)
.replace(/[^a-z]+/g, '')
.substr(0, 5);
await driver.get(scratchr2url);
await navigate(scratchr2url);
await clickXpath('//li[@class="sign-in dropdown"]/span');
let name = await findByXpath('//input[@id="login_dropdown_username"]');
const name = await findByXpath('//input[@id="login_dropdown_username"]');
await name.sendKeys(username);
let word = await findByXpath('//input[@name="password"]');
const word = await findByXpath('//input[@name="password"]');
await word.sendKeys(nonsensePassword + getKey('ENTER'));
// find error
let error = await findByXpath('//form[@id="login"]//div[@class="error"]');
const error = await findByXpath('//form[@id="login"]//div[@class="error"]');
await waitUntilVisible(error, driver);
let errorText = await error.getText();
await expect(errorText).toEqual('Incorrect username or password.');
const errorText = await error.getText();
expect(errorText).toEqual('Incorrect username or password.');
});
});

View file

@ -6,56 +6,57 @@ const {
buildDriver,
clickText,
containsClass,
findByXpath
findByXpath,
navigate
} = new SeleniumHelper();
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
let statisticsPage = rootUrl + '/statistics';
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
const statisticsPage = `${rootUrl}/statistics`;
jest.setTimeout(60000);
let driver;
describe('www-integration statistics page', async () => {
describe('www-integration statistics page', () => {
beforeAll(async () => {
driver = await buildDriver('www-integration statistics page');
});
beforeEach(async () => {
await driver.get(statisticsPage);
await navigate(statisticsPage);
});
afterAll(async () => await driver.quit());
afterAll(() => driver.quit());
test('check Monthly Activity Trends title', async () => {
let chartTitle = await findByXpath('//div[contains(@class, "box0")]//h3');
let chartTitleText = await chartTitle.getText();
await expect(chartTitleText).toBe('Monthly Activity Trends');
const chartTitle = await findByXpath('//div[contains(@class, "box0")]//h3');
const chartTitleText = await chartTitle.getText();
expect(chartTitleText).toBe('Monthly Activity Trends');
});
test('New Projects label on first chart defaults to selected', async () => {
let toggleXpath = `(//div[@id="activity_chart"]/*[name()='svg']/*[name()='g']/*[name()='g']/*` +
const toggleXpath = `(//div[@id="activity_chart"]/*[name()='svg']/*[name()='g']/*[name()='g']/*` +
`[name()='g'])[4]/*[name()='g']/*[name()='g']/*[name()='g']`;
let newProjectsToggle = await findByXpath(toggleXpath);
let toggleState = await containsClass(newProjectsToggle, 'nv-disabled');
await expect(toggleState).toBe(false);
const newProjectsToggle = await findByXpath(toggleXpath);
const toggleState = await containsClass(newProjectsToggle, 'nv-disabled');
expect(toggleState).toBe(false);
});
test('New Projects label on first chart can be toggled', async () => {
let toggleXpath = `(//div[@id="activity_chart"]/*[name()='svg']/*[name()='g']/*[name()='g']/*` +
const toggleXpath = `(//div[@id="activity_chart"]/*[name()='svg']/*[name()='g']/*[name()='g']/*` +
`[name()='g'])[4]/*[name()='g']/*[name()='g']/*[name()='g']`;
let newProjectsToggle = await findByXpath(toggleXpath);
const newProjectsToggle = await findByXpath(toggleXpath);
// toggle off New Projects
await clickText('New Projects');
let toggleState = await containsClass(newProjectsToggle, 'nv-disabled');
await expect(toggleState).toBe(true);
expect(toggleState).toBe(true);
// toggle New Projects on again
await clickText('New Projects');
toggleState = await containsClass(newProjectsToggle, 'nv-disabled');
await expect(toggleState).toBe(false);
expect(toggleState).toBe(false);
});
});

View file

@ -3,24 +3,26 @@
import SeleniumHelper from './selenium-helpers.js';
const {
findByXpath,
buildDriver,
clickXpath,
clickText,
clickXpath,
findByXpath,
isSignedIn,
navigate,
signIn
} = new SeleniumHelper();
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
let studioId = process.env.TEST_STUDIO_ID || 10004360;
let studioUrl = rootUrl + '/studios/' + studioId;
let myStuffURL = rootUrl + '/mystuff';
let rateLimitCheck = process.env.RATE_LIMIT_CHECK || rootUrl;
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
const studioId = process.env.TEST_STUDIO_ID || 10004360;
const studioUrl = `${rootUrl}/studios/${studioId}`;
const myStuffURL = `${rootUrl}/mystuff`;
const rateLimitCheck = process.env.RATE_LIMIT_CHECK || rootUrl;
// since the usernames end in 2 and 3 we're using username2 and username3
// username 1 is used in other tests. Hopefully this is not confusing.
let username2 = process.env.SMOKE_USERNAME + '2';
let username3 = process.env.SMOKE_USERNAME + '3';
let password = process.env.SMOKE_PASSWORD;
const username2 = `${process.env.SMOKE_USERNAME}2`;
const username3 = `${process.env.SMOKE_USERNAME}3`;
const password = process.env.SMOKE_PASSWORD;
let promoteStudioURL;
let curatorTab;
@ -33,35 +35,34 @@ describe('studio page while signed out', () => {
beforeAll(async () => {
// expect(projectUrl).toBe(defined);
driver = await buildDriver('www-integration studio-page signed out');
await driver.get(rootUrl);
});
beforeEach(async () => {
await driver.get(studioUrl);
let studioNav = await findByXpath('//div[@class="studio-tabs"]');
await navigate(studioUrl);
const studioNav = await findByXpath('//div[@class="studio-tabs"]');
await studioNav.isDisplayed();
});
afterAll(async () => await driver.quit());
afterAll(() => driver.quit());
test('land on projects tab', async () => {
await driver.get(studioUrl);
let projectGrid = await findByXpath('//div[@class="studio-projects-grid"]');
let projectGridDisplayed = await projectGrid.isDisplayed();
await expect(projectGridDisplayed).toBe(true);
await navigate(studioUrl);
const projectGrid = await findByXpath('//div[@class="studio-projects-grid"]');
const projectGridDisplayed = await projectGrid.isDisplayed();
expect(projectGridDisplayed).toBe(true);
});
test('studio title', async () => {
let studioTitle = await findByXpath('//div[@class="studio-title"]');
let titleText = await studioTitle.getText();
await expect(titleText).toEqual('studio for automated testing');
const studioTitle = await findByXpath('//div[@class="studio-title"]');
const titleText = await studioTitle.getText();
expect(titleText).toEqual('studio for automated testing');
});
test('studio description', async () => {
let xpath = '//div[contains(@class, "studio-description")]';
let studioDescription = await findByXpath(xpath);
let descriptionText = await studioDescription.getText();
await expect(descriptionText).toEqual('a description');
const xpath = '//div[contains(@class, "studio-description")]';
const studioDescription = await findByXpath(xpath);
const descriptionText = await studioDescription.getText();
expect(descriptionText).toEqual('a description');
});
});
@ -70,53 +71,53 @@ describe('studio management', () => {
beforeAll(async () => {
driver = await buildDriver('www-integration studio management');
await driver.get(rootUrl);
await navigate(rootUrl);
// create a studio for tests
await signIn(username2, password);
await findByXpath('//span[contains(@class, "profile-name")]');
await driver.get(rateLimitCheck);
await driver.get(myStuffURL);
await navigate(rateLimitCheck);
await navigate(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
await findByXpath('//div[@class="studio-tabs"]');
promoteStudioURL = await driver.getCurrentUrl();
curatorTab = await promoteStudioURL + 'curators';
curatorTab = `${promoteStudioURL}curators`;
});
beforeEach(async () => {
await clickXpath('//a[contains(@class, "user-info")]');
await clickText('Sign out');
await driver.get(curatorTab);
await findByXpath('//div[@class="studio-tabs"]');
if (await isSignedIn()) {
await clickXpath('//a[contains(@class, "user-info")]');
await clickText('Sign out');
}
});
afterAll(async () => await driver.quit());
afterAll(() => driver.quit());
test('invite a curator', async () => {
// sign in as user2
await signIn(username2, password);
await findByXpath('//span[contains(@class, "profile-name")]');
await navigate(curatorTab);
// invite user3 to curate
let inviteBox = await findByXpath('//div[@class="studio-adder-row"]/input');
const inviteBox = await findByXpath('//div[@class="studio-adder-row"]/input');
await inviteBox.sendKeys(username3);
await clickXpath('//div[@class="studio-adder-row"]/button');
let inviteAlert = await findByXpath('//div[@class="alert-msg"]'); // the confirm alert
let alertText = await inviteAlert.getText();
let successText = await `Curator invite sent to "${username3}"`;
await expect(alertText).toMatch(successText);
const inviteAlert = await findByXpath('//div[@class="alert-msg"]'); // the confirm alert
const alertText = await inviteAlert.getText();
const successText = `Curator invite sent to "${username3}"`;
expect(alertText).toMatch(successText);
});
test('accept curator invite', async () => {
// Sign in user3
await signIn(username3, password);
await findByXpath('//span[contains(@class, "profile-name")]');
await navigate(curatorTab);
// accept the curator invite
await clickXpath('//button[@class="studio-invitation-button button"]');
let acceptSuccess = await findByXpath('//div[contains(@class,"studio-info-box-success")]');
let acceptSuccessVisible = await acceptSuccess.isDisplayed();
await expect(acceptSuccessVisible).toBe(true);
const acceptSuccess = await findByXpath('//div[contains(@class,"studio-info-box-success")]');
const acceptSuccessVisible = await acceptSuccess.isDisplayed();
expect(acceptSuccessVisible).toBe(true);
});
test('promote to manager', async () => {
@ -125,23 +126,22 @@ describe('studio management', () => {
await findByXpath('//span[contains(@class, "profile-name")]');
// for some reason the user isn't showing up without waiting and reloading the page
await driver.sleep(2000);
await driver.get(curatorTab);
await navigate(curatorTab);
// promote user3
let user3href = await '/users/' + username3;
const user3href = `/users/${username3}`;
// click kebab menu on the user tile
let kebabMenuXpath = await `//a[@href = "${user3href}"]/` +
'following-sibling::div[@class="overflow-menu-container"]';
await clickXpath(kebabMenuXpath + '/button[@class="overflow-menu-trigger"]');
const kebabMenuXpath = `//a[@href = "${user3href}"]/following-sibling::div[@class="overflow-menu-container"]`;
await clickXpath(`${kebabMenuXpath}/button[@class="overflow-menu-trigger"]`);
// click promote
// await clickXpath('//button[@class="promote-menu-button"]'); //<-- I think this will do it
await clickXpath(kebabMenuXpath + '/ul/li/button/span[contains(text(), "Promote")]/..');
await clickXpath(`${kebabMenuXpath}/ul/li/button/span[contains(text(), "Promote")]/..`);
await findByXpath('//div[@class="promote-content"]');
// await clickXpath(//button[contains(@class="promote-button")]) <-- add this selector to the button
await clickXpath('//div[@class="promote-button-row"]/button/span[contains(text(),"Promote")]/..');
let promoteSuccess = await findByXpath('//div[contains(@class, "alert-success")]');
let promoteSuccessVisible = await promoteSuccess.isDisplayed();
await expect(promoteSuccessVisible).toBe(true);
const promoteSuccess = await findByXpath('//div[contains(@class, "alert-success")]');
const promoteSuccessVisible = await promoteSuccess.isDisplayed();
expect(promoteSuccessVisible).toBe(true);
});
test('transfer studio host', async () => {
@ -149,14 +149,13 @@ describe('studio management', () => {
await signIn(username2, password);
await findByXpath('//span[contains(@class, "profile-name")]');
// for some reason the user isn't showing up without reloading the page
await driver.get(curatorTab);
await navigate(curatorTab);
// open kebab menu
let user2href = await '/users/' + username2;
const user2href = `/users/${username2}`;
// click kebab menu on the user tile
let kebabMenuXpath = await `//a[@href = "${user2href}"]/` +
'following-sibling::div[@class="overflow-menu-container"]';
await clickXpath(kebabMenuXpath + '/button[@class="overflow-menu-trigger"]');
const kebabMenuXpath = `//a[@href = "${user2href}"]/following-sibling::div[@class="overflow-menu-container"]`;
await clickXpath(`${kebabMenuXpath}/button[@class="overflow-menu-trigger"]`);
// click transfer in dropdown
await clickXpath('//button[@class="studio-member-tile-menu-wide"]');
@ -175,15 +174,15 @@ describe('studio management', () => {
await findByXpath('//div[@class="transfer-outcome"]');
// enter password
let passwordInput = await findByXpath('//input[@class="transfer-password-input"]');
const passwordInput = await findByXpath('//input[@class="transfer-password-input"]');
await passwordInput.sendKeys(password);
await findByXpath(`//input[@value="${password}"]`);
// click confirm
// await clickXpath('//button[contains(@class, "confirm-transfer-button")]')
await clickXpath('//span[contains(text(), "Confirm")]/..');
let transferSuccess = await findByXpath('//div[contains(@class, "alert-success")]');
let successVisible = await transferSuccess.isDisplayed();
await expect(successVisible).toBe(true);
// findByXpath checks for both presence and visibility
const transferSuccess = await findByXpath('//div[contains(@class, "alert-success")]');
expect(transferSuccess).toBeTruthy();
});
});

View file

@ -9,16 +9,16 @@
* with languages as keys and the missing IDs as values
*/
var path = require('path');
var fs = require('fs');
var tap = require('tap');
const path = require('path');
const fs = require('fs');
const tap = require('tap');
/**
* To get the files (containing message IDs and localized strings for each page in www)
* from the intl directory
*/
var intlDirPath = path.resolve(__dirname, '../../intl/');
var intlFiles = fs.readdirSync(intlDirPath);
const intlDirPath = path.resolve(__dirname, '../../intl/');
const intlFiles = fs.readdirSync(intlDirPath);
/*
* Tells tap whether the test should pass or fail for a given file.
@ -30,36 +30,36 @@ const noMissingStrings = (fileName, missingMessageId, pagesMissingIds) => {
if (Object.keys(missingMessageId).length === 0) {
tap.pass();
} else {
tap.fail(fileName + ' is missing string IDs');
tap.fail(`${fileName} is missing string IDs`);
pagesMissingIds[fileName] = [];
pagesMissingIds[fileName].push(missingMessageId);
}
};
var pagesWithLanguagesMissingIds = {};
const pagesWithLanguagesMissingIds = {};
for (var i in intlFiles) {
var file = intlFiles[i];
var filePath = path.resolve(__dirname, '../../intl/' + file);
var pageMessagesString = fs.readFileSync(filePath, 'utf8');
for (const i in intlFiles) {
const file = intlFiles[i];
const filePath = path.resolve(__dirname, `../../intl/${file}`);
const pageMessagesString = fs.readFileSync(filePath, 'utf8');
/**
* To make the string of the file of the page.intl.js back into useable objects
*/
var window = {};
var pageMessages = eval(pageMessagesString); // eslint-disable-line no-eval
const window = {};
const pageMessages = eval(pageMessagesString); // eslint-disable-line no-eval
/**
* The goal is to compare the IDs for each language to the IDs for English,
* so we need the list of IDs for the given page in English
*/
var englishIdList = window._messages.en;
const englishIdList = window._messages.en;
var messageIdNotInLanguage = {};
const messageIdNotInLanguage = {};
for (var languageKey in pageMessages) {
var currentLanguageObject = pageMessages[languageKey];
for (var messageId in englishIdList) {
for (const languageKey in pageMessages) {
const currentLanguageObject = pageMessages[languageKey];
for (const messageId in englishIdList) {
if (!(messageId in currentLanguageObject)) {
if (typeof messageIdNotInLanguage[languageKey] === 'undefined') {
messageIdNotInLanguage[languageKey] = [];

View file

@ -1,20 +1,20 @@
var fs = require('fs');
var glob = require('glob');
var tap = require('tap');
const fs = require('fs');
const glob = require('glob');
const tap = require('tap');
var TRANSLATIONS_PATTERN = './node_modules/scratch-l10n/www/**/*.json';
var files = glob.sync(TRANSLATIONS_PATTERN);
const TRANSLATIONS_PATTERN = './node_modules/scratch-l10n/www/**/*.json';
const files = glob.sync(TRANSLATIONS_PATTERN);
const checkJson = (data, name) => {
try {
JSON.parse(data);
} catch (e) {
tap.fail(name + ' has invalid Json.\n');
tap.fail(`${name} has invalid Json.\n`);
}
};
tap.test('check valid json', function (t) {
files.forEach(function (f) {
tap.test('check valid json', t => {
files.forEach(f => {
const data = fs.readFileSync(f);
checkJson(data, f);
});

View file

@ -1,9 +1,9 @@
var defaults = require('lodash.defaults');
var fastlyConfig = require('../../bin/lib/fastly-config-methods');
var routeJson = require('../../src/routes.json');
var tap = require('tap');
const defaults = require('lodash.defaults');
const fastlyConfig = require('../../bin/lib/fastly-config-methods');
const routeJson = require('../../src/routes.json');
const tap = require('tap');
var testRoutes = [
const testRoutes = [
{
name: 'less-traveled',
pattern: '^/?$',
@ -20,10 +20,10 @@ var testRoutes = [
}
];
var routes = routeJson.map(function (route) {
return defaults({}, {pattern: fastlyConfig.expressPatternToRegex(route.pattern)}, route);
});
var extraAppRoutes = [
const routes = routeJson.map(route =>
defaults({}, {pattern: fastlyConfig.expressPatternToRegex(route.pattern)}, route)
);
const extraAppRoutes = [
// Homepage with querystring.
// TODO: Should this be added for every route?
'/\\?',
@ -32,35 +32,35 @@ var extraAppRoutes = [
];
tap.test('getStaticPaths', function (t) {
var staticPaths = fastlyConfig.getStaticPaths(__dirname, '../../build/*');
tap.test('getStaticPaths', t => {
const staticPaths = fastlyConfig.getStaticPaths(__dirname, '../../build/*');
t.type(staticPaths, 'object');
t.end();
});
tap.test('getViewPaths', function (t) {
var viewPaths = fastlyConfig.getViewPaths(testRoutes);
tap.test('getViewPaths', t => {
const viewPaths = fastlyConfig.getViewPaths(testRoutes);
t.type(viewPaths, 'object');
t.equal(viewPaths[0], '/?$');
t.equal(viewPaths[1], '/more?$');
t.end();
});
tap.test('pathsToCondition', function (t) {
var condition = fastlyConfig.pathsToCondition(['/?$', '/more?$']);
tap.test('pathsToCondition', t => {
const condition = fastlyConfig.pathsToCondition(['/?$', '/more?$']);
t.type(condition, 'string');
t.equal(condition, 'req.url~"^(/?$|/more?$)"');
t.end();
});
tap.test('getAppRouteCondition', function (t) {
var condition = fastlyConfig.getAppRouteCondition('../../build/*', routes, extraAppRoutes, __dirname);
tap.test('getAppRouteCondition', t => {
const condition = fastlyConfig.getAppRouteCondition('../../build/*', routes, extraAppRoutes, __dirname);
t.type(condition, 'string');
t.end();
});
tap.test('testSetTTL', function (t) {
var ttl = fastlyConfig.setResponseTTL('itsactuallyttyl');
tap.test('testSetTTL', t => {
const ttl = fastlyConfig.setResponseTTL('itsactuallyttyl');
t.equal(ttl, '' +
'if (itsactuallyttyl) {\n' +
' if (req.url ~ "^(/projects/|/fragment/account-nav.json|/session/)" && ' +

View file

@ -33,18 +33,6 @@ describe('Captcha test', () => {
expect(global.grecaptcha.execute).toHaveBeenCalled();
});
test('Captcha load calls props captchaOnLoad', () => {
const props = {
onCaptchaLoad: jest.fn()
};
const wrapper = enzyme.shallow(<Captcha
{...props}
/>);
wrapper.instance().onCaptchaLoad();
expect(global.grecaptcha.render).toHaveBeenCalled();
expect(props.onCaptchaLoad).toHaveBeenCalled();
});
test('Captcha renders the div google wants', () => {
const props = {
onCaptchaLoad: jest.fn()

View file

@ -7,7 +7,7 @@ import configureStore from 'redux-mock-store';
describe('Compose Comment test', () => {
const mockStore = configureStore();
let _mockFormat;
const defaultProps = () =>({
const defaultProps = () => ({
user: {
thumbnailUrl: 'scratch.mit.edu',
username: 'auser'
@ -51,7 +51,7 @@ describe('Compose Comment test', () => {
return wrapper;
};
test('status is EDITING when props do not contain a muteStatus ', () => {
test('status is EDITING when props do not contain a muteStatus', () => {
const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.state.status).toBe('EDITING');
});
@ -81,7 +81,7 @@ describe('Compose Comment test', () => {
global.Date.now = realDateNow;
});
test('Modal & Comment status do not show ', () => {
test('Modal & Comment status do not show', () => {
const component = getComposeCommentWrapper({});
// Comment compsoe box is there
expect(component.find('FlexRow.compose-comment').exists()).toEqual(true);
@ -95,7 +95,7 @@ describe('Compose Comment test', () => {
});
test('Error messages shows when comment rejected ', () => {
test('Error messages shows when comment rejected', () => {
const component = getComposeCommentWrapper({});
const commentInstance = component.instance();
commentInstance.setState({
@ -109,7 +109,7 @@ describe('Compose Comment test', () => {
expect(component.find('Button.compose-cancel').props().disabled).toBe(false);
});
test('No error message shows when comment rejected because user is already muted ', () => {
test('No error message shows when comment rejected because user is already muted', () => {
const component = getComposeCommentWrapper({});
const commentInstance = component.instance();
commentInstance.setState({
@ -278,7 +278,7 @@ describe('Compose Comment test', () => {
expect(component.find('Button.compose-cancel').props().disabled).toBe(false);
});
test('Mute Modal shows when muteOpen is true ', () => {
test('Mute Modal shows when muteOpen is true', () => {
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
const store = mockStore({
@ -390,18 +390,18 @@ describe('Compose Comment test', () => {
expect(component.find('MuteModal').exists()).toEqual(true);
expect(component.find('MuteModal').props().showFeedback).toBe(false);
});
test('shouldShowMuteModal is false when muteStatus is undefined ', () => {
test('shouldShowMuteModal is false when muteStatus is undefined', () => {
const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.shouldShowMuteModal()).toBe(false);
});
test('shouldShowMuteModal is false when list is undefined ', () => {
test('shouldShowMuteModal is false when list is undefined', () => {
const muteStatus = {};
const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.shouldShowMuteModal(muteStatus)).toBe(false);
});
test('shouldShowMuteModal is false when list empty ', () => {
test('shouldShowMuteModal is false when list empty', () => {
const muteStatus = {
offenses: []
};
@ -409,7 +409,7 @@ describe('Compose Comment test', () => {
expect(commentInstance.shouldShowMuteModal(muteStatus)).toBe(false);
});
test('shouldShowMuteModal is true when only 1 recent offesnse ', () => {
test('shouldShowMuteModal is true when only 1 recent offesnse', () => {
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
// Since Date.now mocked to 0 above, we just need a small number to make
@ -426,13 +426,13 @@ describe('Compose Comment test', () => {
global.Date.now = realDateNow;
});
test('shouldShowMuteModal is false when multiple offenses, even if 1 is recent ', () => {
test('shouldShowMuteModal is false when multiple offenses, even if 1 is recent', () => {
const offenses = [];
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
// Since Date.now mocked to 0 above, we just need a small number to make
// it look like it was created more than 2 minutes ago.
let offense = {
const offense = {
expiresAt: '1000',
createdAt: '-119' // just shy of two min ago
};
@ -453,7 +453,7 @@ describe('Compose Comment test', () => {
global.Date.now = () => 0;
// Since Date.now mocked to 0 above, we just need a small number to make
// it look like it was created more than 2 minutes ago.
let offense = {
const offense = {
expiresAt: '1000',
createdAt: '-119' // just shy of two min ago
};
@ -469,7 +469,7 @@ describe('Compose Comment test', () => {
global.Date.now = realDateNow;
});
test('shouldShowMuteModal is false when the user is already muted, even when only 1 recent offesnse ', () => {
test('shouldShowMuteModal is false when the user is already muted, even when only 1 recent offesnse', () => {
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
// Since Date.now mocked to 0 above, we just need a small number to make
@ -510,7 +510,7 @@ describe('Compose Comment test', () => {
expect(commentInstance.getMuteModalStartStep()).toBe(0);
});
test('getMuteModalStartStep: A reply that got them muted ', () => {
test('getMuteModalStartStep: A reply that got them muted', () => {
const commentInstance = getComposeCommentWrapper({isReply: true}).instance();
commentInstance.setState({
status: 'REJECTED_MUTE'
@ -518,7 +518,7 @@ describe('Compose Comment test', () => {
expect(commentInstance.getMuteModalStartStep()).toBe(0);
});
test('getMuteModalStartStep: A reply click when already muted ', () => {
test('getMuteModalStartStep: A reply click when already muted', () => {
const commentInstance = getComposeCommentWrapper({isReply: true}).instance();
commentInstance.setState({
status: 'COMPOSE_DISALLOWED'
@ -526,7 +526,7 @@ describe('Compose Comment test', () => {
expect(commentInstance.getMuteModalStartStep()).toBe(1);
});
test('isMuted: expiration is in the future ', () => {
test('isMuted: expiration is in the future', () => {
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0; // Set "now" to 0 for easier testing.
@ -536,7 +536,7 @@ describe('Compose Comment test', () => {
global.Date.now = realDateNow;
});
test('isMuted: expiration is in the past ', () => {
test('isMuted: expiration is in the past', () => {
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
@ -546,7 +546,7 @@ describe('Compose Comment test', () => {
global.Date.now = realDateNow;
});
test('isMuted: expiration is not set ', () => {
test('isMuted: expiration is not set', () => {
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;

View file

@ -23,7 +23,7 @@ describe('DonateBannerTest', () => {
expect(component.find('FormattedMessage[id="donatebanner.askSupport"]').exists()).toEqual(false);
});
test('testing default message comes back after May 21 ', () => {
test('testing default message comes back after May 21', () => {
// Date after Scratch week
global.Date.now = () => new Date(2022, 4, 22).getTime();
const component = mountWithIntl(

View file

@ -19,7 +19,7 @@ const requestFailureResponse = {
// lets us change where remoteRequestResponse points later, without actually changing
// mockedValidateEmailRemotely.
let remoteRequestResponse = requestSuccessResponse;
let mockedValidateEmailRemotely = jest.fn(() => (
const mockedValidateEmailRemotely = jest.fn(() => (
/* eslint-disable no-undef */
Promise.resolve(remoteRequestResponse)
/* eslint-enable no-undef */

View file

@ -1,8 +1,7 @@
import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import FormikSelect from '../../../src/components/formik-forms/formik-select.jsx';
import {Formik} from 'formik';
import {Field} from 'formik';
import {Field, Formik} from 'formik';
describe('FormikSelect', () => {
test('No validation message without an error', () => {

View file

@ -30,7 +30,7 @@ describe('InfoButton', () => {
// mouseOver info button
component.find('div.info-button').simulate('mouseOver');
setTimeout(function () { // necessary because mouseover uses debounce
setTimeout(() => { // necessary because mouseover uses debounce
// crucial: if we don't call update(), then find() below looks through an OLD
// version of the DOM! see https://github.com/airbnb/enzyme/issues/1233#issuecomment-358915200
component.update();
@ -70,7 +70,7 @@ describe('InfoButton', () => {
// mouseLeave from info button
component.find('div.info-button').simulate('mouseLeave');
setTimeout(function () { // necessary because mouseover uses debounce
setTimeout(() => { // necessary because mouseover uses debounce
component.update();
expect(component.find('div.info-button-message').exists()).toEqual(true);
done();

View file

@ -1,6 +1,5 @@
import React from 'react';
import {shallowWithIntl} from '../../helpers/intl-helpers.jsx';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import {mountWithIntl, shallowWithIntl} from '../../helpers/intl-helpers.jsx';
import MuteModal from '../../../src/components/modal/mute/modal';
import Modal from '../../../src/components/modal/base/modal';

View file

@ -3,7 +3,7 @@ import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import MuteStep from '../../../src/components/modal/mute/mute-step';
describe('MuteStepTest', () => {
test('Mute Step with no images ', () => {
test('Mute Step with no images', () => {
const component = mountWithIntl(
<MuteStep
header="header text"
@ -19,7 +19,7 @@ describe('MuteStepTest', () => {
});
test('Mute Step with side image ', () => {
test('Mute Step with side image', () => {
const component = mountWithIntl(
<MuteStep
sideImg="/path/to/img.png"
@ -34,7 +34,7 @@ describe('MuteStepTest', () => {
});
test('Mute Step with bottom image ', () => {
test('Mute Step with bottom image', () => {
const component = mountWithIntl(
<MuteStep
bottomImg="/path/to/img.png"

View file

@ -1,6 +1,5 @@
import React from 'react';
import {shallowWithIntl} from '../../helpers/intl-helpers.jsx';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import {mountWithIntl, shallowWithIntl} from '../../helpers/intl-helpers.jsx';
import JoinFlowStep from '../../../src/components/join-flow/join-flow-step';
import RegistrationErrorStep from '../../../src/components/join-flow/registration-error-step';

View file

@ -54,7 +54,7 @@ describe('Studio comments', () => {
});
test('calling onOpen sets a class on the #viewEl and records in local storage', () => {
const component = mountWithIntl(<StudioAdminPanel showAdminPanel />);
let child = component.find(AdminPanel);
const child = component.find(AdminPanel);
expect(viewEl.classList.contains(adminPanelOpenClass)).toBe(false);
// `act` is a test-util function for making react state updates sync
act(child.prop('onOpen'));
@ -69,7 +69,7 @@ describe('Studio comments', () => {
showAdminPanel
/>
);
let child = component.find('iframe');
const child = component.find('iframe');
expect(child.getDOMNode().src).toMatch('/scratch2-studios/123/adminpanel');
});
test('responds to closePanel MessageEvent from the iframe', () => {

View file

@ -17,7 +17,7 @@ const requestFailureResponse = {
// lets us change where remoteRequestResponse points later, without actually changing
// mockedValidateUsernameRemotely.
let remoteRequestResponse = requestSuccessResponse;
let mockedValidateUsernameRemotely = jest.fn(() => (
const mockedValidateUsernameRemotely = jest.fn(() => (
/* eslint-disable no-undef */
Promise.resolve(remoteRequestResponse)
/* eslint-enable no-undef */

View file

@ -6,7 +6,7 @@ describe('unit test lib/format-time.js', () => {
const mockFormatExpression = {
format: jest.fn()
};
beforeEach(() =>{
beforeEach(() => {
realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
_mockFormat = Intl.RelativeTimeFormat = jest
@ -14,68 +14,68 @@ describe('unit test lib/format-time.js', () => {
.mockImplementation(() => mockFormatExpression);
});
afterEach(()=>{
afterEach(() => {
global.Date.now = realDateNow;
jest.resetAllMocks();
});
test('test timestamp that is 2 minutes in the future', () => {
test('timestamp that is 2 minutes in the future', () => {
const twoMin = 2 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 2 minutes');
format.formatRelativeTime(twoMin, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(2, 'minute');
});
test('test timestamp that is 15 seconds in the future displays 1', () => {
test('timestamp that is 15 seconds in the future displays 1', () => {
const fifteenSec = 15 * 1000;
mockFormatExpression.format.mockReturnValue('in 1 minute');
format.formatRelativeTime(fifteenSec, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(1, 'minute');
});
test('test rounding timestamp that is 4.4 minutes rounds to 4', () => {
test('rounding timestamp that is 4.4 minutes rounds to 4', () => {
const fourPlusMin = 4.4 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 4 minutes');
format.formatRelativeTime(fourPlusMin, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(4, 'minute');
});
test('test timestamp that is 95.25 minutes in the future', () => {
test('timestamp that is 95.25 minutes in the future', () => {
const ninetyFiveMin = 95.25 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 95 minutes');
format.formatRelativeTime(ninetyFiveMin, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(95, 'minute');
});
test('test timestamp that is 119 minutes in the future', () => {
test('timestamp that is 119 minutes in the future', () => {
const ninetyFiveMin = 119 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 199 minutes');
format.formatRelativeTime(ninetyFiveMin, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(119, 'minute');
});
test('test timestamp that is 48 hours in the future', () => {
test('timestamp that is 48 hours in the future', () => {
const fortyEightHrs = 48 * 60 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 48 hours');
format.formatRelativeTime(fortyEightHrs, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(48, 'hour');
});
test('test timestamp that is 2.6 hours rounds to 3', () => {
test('timestamp that is 2.6 hours rounds to 3', () => {
const twoPlusHours = 2.6 * 60 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 3 hours');
format.formatRelativeTime(twoPlusHours, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(3, 'hour');
});
test('test timestamp that is 4.2 hours in the future rounds to 4', () => {
test('timestamp that is 4.2 hours in the future rounds to 4', () => {
const fourPlusHours = 4.2 * 60 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 4 hours');
format.formatRelativeTime(fourPlusHours, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(4, 'hour');
});
test('test timestamp that is 2 hours in the future is in hours', () => {
test('timestamp that is 2 hours in the future is in hours', () => {
const twoHours = 2 * 60 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 2 hours');
format.formatRelativeTime(twoHours, 'en');

View file

@ -14,7 +14,7 @@ describe('unit test lib/jar.js', () => {
expires: expect.anything() // not specifically matching the date because it is hard to mock
}));
});
test('test with opts', () => {
test('with opts', () => {
jar.set('a', 'b', {option: 'one'});
expect(cookie.serialize).toHaveBeenCalled();
expect(cookie.serialize).toHaveBeenCalledWith('a', 'b',

View file

@ -7,20 +7,17 @@ describe('unit test lib/route.js', () => {
});
test('getURIClassroomToken parses URI paths like /classes/21/register/r9n5f5xk', () => {
let response;
response = route.getURIClassroomToken('/classes/21/register/r9n5f5xk');
const response = route.getURIClassroomToken('/classes/21/register/r9n5f5xk');
expect(response).toEqual('r9n5f5xk');
});
test('getURIClassroomToken parses URI paths like /signup/e2dcfkx95', () => {
let response;
response = route.getURIClassroomToken('/signup/e2dcfkx95');
const response = route.getURIClassroomToken('/signup/e2dcfkx95');
expect(response).toEqual('e2dcfkx95');
});
test('getURIClassroomToken works with trailing slash', () => {
let response;
response = route.getURIClassroomToken('/signup/r9n5f5xk/');
const response = route.getURIClassroomToken('/signup/r9n5f5xk/');
expect(response).toEqual('r9n5f5xk');
});
});

View file

@ -1,16 +1,16 @@
describe('session library', () => {
// respond to session requests with empty session object
let sessionNoUser = jest.fn((opts, callback) => {
const sessionNoUser = jest.fn((opts, callback) => {
callback(null, {}, {statusCode: 200});
});
// respond to session requests with session object that indicates
// successfully logged-in user
let sessionYesUser = jest.fn((opts, callback) => {
const sessionYesUser = jest.fn((opts, callback) => {
callback(null, {user: {username: 'test_username'}}, {statusCode: 200});
});
// respond to first two requests with empty session object; after that,
// respond with user in object
let sessionNoThenYes = jest.fn((opts, callback) => {
const sessionNoThenYes = jest.fn((opts, callback) => {
if (sessionNoThenYes.mock.calls.length <= 2) {
callback(null, {}, {statusCode: 200});
} else {
@ -19,24 +19,22 @@ describe('session library', () => {
});
// respond to session requests with response code 404, indicating no session
// found for that user
let sessionNotFound = jest.fn((opts, callback) => {
const sessionNotFound = jest.fn((opts, callback) => {
callback(null, null, {statusCode: 404});
});
// respond to session requests with response code 503, indicating connection failure
let sessionConnectFailure = jest.fn((opts, callback) => {
const sessionConnectFailure = jest.fn((opts, callback) => {
callback(null, null, {statusCode: 503});
});
// by changing whichMockAPIRequest, we can simulate different api responses
let whichMockAPIRequest = null;
let mockAPIRequest = (opts, callback) => {
const mockAPIRequest = (opts, callback) => {
whichMockAPIRequest(opts, callback);
};
// mock lib/api.js, and include our mocked version in lib/session.js
jest.mock('../../../src/lib/api', () => {
return mockAPIRequest;
});
jest.mock('../../../src/lib/api', () => mockAPIRequest);
const sessionLib = require('../../../src/lib/session'); // eslint-disable-line global-require
afterEach(() => {

View file

@ -173,7 +173,7 @@ describe('unit test lib/validate.js', () => {
});
test('responseErrorMsg is null in case where there is no dedicated string for that case', () => {
let response = validate.responseErrorMsg('username', 'some error that is not covered');
const response = validate.responseErrorMsg('username', 'some error that is not covered');
expect(response).toEqual(null);
});
});

View file

@ -121,7 +121,7 @@ describe('Infinite List redux module', () => {
describe('ERROR', () => {
let action;
let error = new Error();
const error = new Error();
beforeEach(() => {
action = module.actions.error(error);
});
@ -145,7 +145,7 @@ describe('Infinite List redux module', () => {
describe('action creators', () => {
test('module contains actions creators', () => {
// The actual action creators are tested above in the reducer tests
for (let key in module.actions) {
for (const key in module.actions) {
expect(typeof module.actions[key]).toBe('function');
}
});

View file

@ -14,7 +14,7 @@ beforeEach(() => {
});
describe('getTopLevelComments', () => {
test('replies are only loaded for comments with a reply_count > 0', async () => {
test('replies are only loaded for comments with a reply_count > 0', () => {
api.mockImplementationOnce((opts, callback) => {
expect(opts.uri).toBe('/users/u/projects/123123/comments');
const body = [
@ -43,8 +43,8 @@ describe('getTopLevelComments', () => {
expect(state.comments.replies[1]).toBeUndefined();
expect(state.comments.replies[60]).toBeUndefined();
});
test('admin route is used correctly', async () => {
api.mockImplementationOnce((opts) => {
test('admin route is used correctly', () => {
api.mockImplementationOnce(opts => {
// NB: this route doesn't include the owner username
expect(opts.uri).toBe('/admin/projects/123123/comments');
expect(opts.authentication).toBe('a-token');
@ -54,7 +54,7 @@ describe('getTopLevelComments', () => {
});
describe('getCommentById', () => {
test('getting a top level comment will not load replies if there arent any', async () => {
test('getting a top level comment will not load replies if there arent any', () => {
api.mockImplementationOnce((opts, callback) => {
expect(opts.uri).toBe('/users/u/projects/123123/comments/111');
const body = {id: 111, parent_id: null, reply_count: 0};
@ -66,8 +66,8 @@ describe('getCommentById', () => {
expect(state.comments.replies[111]).toBeUndefined();
});
test('admin route is used correctly', async () => {
api.mockImplementationOnce((opts) => {
test('admin route is used correctly', () => {
api.mockImplementationOnce(opts => {
// NB: this route doesn't include the owner username
expect(opts.uri).toBe('/admin/projects/123123/comments/111');
expect(opts.authentication).toBe('a-token');
@ -75,7 +75,7 @@ describe('getCommentById', () => {
store.dispatch(actions.getCommentById(123123, 111, 'u', true, 'a-token'));
});
test('getting a top level comment will load replies', async () => {
test('getting a top level comment will load replies', () => {
api.mockImplementationOnce((opts, callback) => {
expect(opts.uri).toBe('/users/u/projects/123123/comments/111');
const body = {id: 111, parent_id: null, reply_count: 2};
@ -91,7 +91,7 @@ describe('getCommentById', () => {
expect(state.comments.replies[111].length).toBe(1);
});
test('getting a reply comment will load the parent comment and its other replies', async () => {
test('getting a reply comment will load the parent comment and its other replies', () => {
// Expect 3 requests. First 111, which is a reply comment, maybe linked to from messages
// Second is for 111's parent, which is 555.
// Third is for 555's replies, which returns 111 and 112
@ -121,5 +121,4 @@ describe.skip('addNewComment', () => { });
describe.skip('deleteComment', () => { });
describe.skip('reportComment', () => { });
describe.skip('resetComments', () => { });
describe.skip('reportComment', () => { });
describe.skip('getReplies', () => { });

View file

@ -17,7 +17,7 @@ describe('session selectors', () => {
});
test('user data', () => {
let state = {session: getInitialState()};
const state = {session: getInitialState()};
const newSession = sessions.user1.session;
state.session = sessionReducer(state.session, setSession(newSession));
expect(selectUserId(state)).toBe(1);
@ -28,7 +28,7 @@ describe('session selectors', () => {
describe('permissions', () => {
test('selectIsAdmin', () => {
let state = {session: getInitialState()};
const state = {session: getInitialState()};
const newSession = sessions.user1Admin.session;
state.session = sessionReducer(state.session, setSession(newSession));
expect(selectIsAdmin(state)).toBe(true);
@ -37,7 +37,7 @@ describe('session selectors', () => {
});
test('selectIsSocial', () => {
let state = {session: getInitialState()};
const state = {session: getInitialState()};
const newSession = sessions.user1Social.session;
state.session = sessionReducer(state.session, setSession(newSession));
expect(selectIsSocial(state)).toBe(true);

View file

@ -12,7 +12,7 @@ beforeEach(() => {
});
describe('getTopLevelComments', () => {
test('replies are only loaded for comments with a reply_count > 0', async () => {
test('replies are only loaded for comments with a reply_count > 0', () => {
store = configureStore(reducers, {
...initialState,
studio: {id: 123123}
@ -45,7 +45,7 @@ describe('getTopLevelComments', () => {
expect(state.comments.replies[1]).toBeUndefined();
expect(state.comments.replies[60]).toBeUndefined();
});
test('admin route is used when the session shows the user is an admin', async () => {
test('admin route is used when the session shows the user is an admin', () => {
store = configureStore(reducers, {
...initialState,
studio: {id: 123123},
@ -56,7 +56,7 @@ describe('getTopLevelComments', () => {
}
}
});
api.mockImplementationOnce((opts) => {
api.mockImplementationOnce(opts => {
expect(opts.uri).toBe('/admin/studios/123123/comments');
expect(opts.authentication).toBe('a-token');
});
@ -65,7 +65,7 @@ describe('getTopLevelComments', () => {
});
describe('getCommentById', () => {
test('getting a top level comment will not load replies if there arent any', async () => {
test('getting a top level comment will not load replies if there arent any', () => {
store = configureStore(reducers, {
...initialState,
studio: {id: 123123}
@ -81,7 +81,7 @@ describe('getCommentById', () => {
expect(state.comments.replies[111]).toBeUndefined();
});
test('getting a top level comment will load replies', async () => {
test('getting a top level comment will load replies', () => {
store = configureStore(reducers, {
...initialState,
studio: {id: 123123}
@ -101,7 +101,7 @@ describe('getCommentById', () => {
expect(state.comments.replies[111].length).toBe(1);
});
test('getting a reply comment will load the parent comment and its other replies', async () => {
test('getting a reply comment will load the parent comment and its other replies', () => {
store = configureStore(reducers, {
...initialState,
studio: {id: 123123}
@ -135,5 +135,4 @@ describe.skip('addNewComment', () => { });
describe.skip('deleteComment', () => { });
describe.skip('reportComment', () => { });
describe.skip('resetComments', () => { });
describe.skip('reportComment', () => { });
describe.skip('getReplies', () => { });

View file

@ -64,7 +64,7 @@ describe('loadManagers', () => {
}
}
});
api.mockImplementation((opts) => {
api.mockImplementation(opts => {
expect(opts.uri).toBe('/admin/studios/123123/managers/');
expect(opts.authentication).toBe('a-token');
});
@ -120,7 +120,7 @@ describe('loadCurators', () => {
}
}
});
api.mockImplementation((opts) => {
api.mockImplementation(opts => {
expect(opts.uri).toBe('/admin/studios/123123/curators/');
expect(opts.authentication).toBe('a-token');
});

View file

@ -30,7 +30,7 @@ import {sessions, studios} from '../../helpers/state-fixtures.json';
let state;
const setStateByRole = (role) => {
const setStateByRole = role => {
switch (role) {
case 'admin':
state.session = sessions.user1Admin;
@ -74,7 +74,7 @@ const setStateByRole = (role) => {
state.session = sessions.user1Muted;
break;
default:
throw new Error('Unknown user role in test: ' + role);
throw new Error(`Unknown user role in test: ${role}`);
}
};
@ -588,7 +588,7 @@ describe('studio mute errors', () => {
state.session = thisSession;
expect(selectShowCommentsGloballyOffError(state)).toBe(true);
});
test('Do not show comments off error because feature flag is on ', () => {
test('Do not show comments off error because feature flag is on', () => {
const thisSession = {
status: Status.FETCHED,
session: {
@ -600,7 +600,7 @@ describe('studio mute errors', () => {
state.session = thisSession;
expect(selectShowCommentsGloballyOffError(state)).toBe(false);
});
test('Do not show comments off error because session not fetched ', () => {
test('Do not show comments off error because session not fetched', () => {
const thisSession = {
status: Status.NOT_FETCHED,
session: {

View file

@ -52,7 +52,7 @@ describe('loadProjects', () => {
}
}
});
api.mockImplementation((opts) => {
api.mockImplementation(opts => {
expect(opts.uri).toBe('/admin/studios/123123/projects/');
expect(opts.authentication).toBe('a-token');
});