Merge pull request #7943 from scratchfoundation/gha

Gha
This commit is contained in:
Christopher Willis-Ford 2023-10-31 08:57:58 -07:00 committed by GitHub
commit 2f788186d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 474 additions and 349 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

View file

@ -7,8 +7,8 @@
"test": "npm run test:lint && npm run build && npm run test:unit", "test": "npm run test:lint && npm run build && npm run test:unit",
"test:lint": "eslint . --ext .js,.jsx,.json", "test:lint": "eslint . --ext .js,.jsx,.json",
"test:lint:ci": "eslint . --ext .js,.jsx,.json --format junit -o ./test/results/lint-results.xml", "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": "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": "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": "npm run test:unit:jest:unit && npm run test:unit:jest:localization",
"test:unit:jest:unit": "jest ./test/unit/ --reporters=default", "test:unit:jest:unit": "jest ./test/unit/ --reporters=default",

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

@ -8,6 +8,7 @@ const {
clickXpath, clickXpath,
containsClass, containsClass,
findByXpath, findByXpath,
navigate,
signIn signIn
} = new SeleniumHelper(); } = new SeleniumHelper();
@ -29,13 +30,12 @@ const studioId = process.env.COMMENT_STUDIO_ID || 10005646;
const studioUrl = `${rootUrl}/studios/${studioId}/comments`; const studioUrl = `${rootUrl}/studios/${studioId}/comments`;
// setup comments to leave // setup comments to leave
// 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 date = new Date();
const dateString = `Y:${date.getFullYear()} - M:${date.getMonth() + 1} - D:${date.getDate()} ` + const dateString = date.toISOString();
`: ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; const projectComment = `${dateString} project`;
const buildNumber = process.env.CIRCLE_BUILD_NUM || dateString; const profileComment = `${dateString} profile`;
const projectComment = `${buildNumber} project`; const studioComment = `${dateString} studio`;
const profileComment = `${buildNumber} profile`;
const studioComment = `${buildNumber} studio`;
const projectReply = `${projectComment} reply`; const projectReply = `${projectComment} reply`;
const profileReply = `${profileComment} reply`; const profileReply = `${profileComment} reply`;
@ -48,7 +48,7 @@ let driver;
describe('comment tests', () => { describe('comment tests', () => {
beforeAll(async () => { beforeAll(async () => {
driver = await buildDriver('www-integration project comments'); driver = await buildDriver('www-integration project comments');
await driver.get(rootUrl); await navigate(rootUrl);
}); });
afterAll(() => driver.quit()); afterAll(() => driver.quit());
@ -60,13 +60,13 @@ describe('comment tests', () => {
}); });
afterAll(async () => { afterAll(async () => {
await driver.get(rootUrl); await navigate(rootUrl);
await clickXpath('//a[contains(@class, "user-info")]'); await clickXpath('//a[contains(@class, "user-info")]');
await clickText('Sign out'); await clickText('Sign out');
}); });
test('leave comment on project', async () => { test('leave comment on project', async () => {
await driver.get(projectUrl); await navigate(projectUrl);
// leave the comment // leave the comment
const commentBox = await findByXpath('//textArea[@name="compose-comment"]'); const commentBox = await findByXpath('//textArea[@name="compose-comment"]');
@ -82,7 +82,7 @@ describe('comment tests', () => {
}); });
test('leave comment on a profile', async () => { test('leave comment on a profile', async () => {
await driver.get(profileUrl); await navigate(profileUrl);
// leave the comment // leave the comment
const commentXpath = '//form[@id="main-post-form"]/div/textArea'; const commentXpath = '//form[@id="main-post-form"]/div/textArea';
@ -97,11 +97,11 @@ describe('comment tests', () => {
expect(commentVisible).toBe(true); expect(commentVisible).toBe(true);
// return to homepage to sign out with www // return to homepage to sign out with www
await driver.get(rootUrl); await navigate(rootUrl);
}); });
test('leave comment on studio', async () => { test('leave comment on studio', async () => {
await driver.get(studioUrl); await navigate(studioUrl);
// leave the comment // leave the comment
const commentBox = await findByXpath('//textArea[@name="compose-comment"]'); const commentBox = await findByXpath('//textArea[@name="compose-comment"]');
@ -138,7 +138,7 @@ describe('comment tests', () => {
}); });
test('project comment message visible', async () => { test('project comment message visible', async () => {
await driver.get(`${rootUrl}/messages`); await navigate(`${rootUrl}/messages`);
const projectMessageXpath = '//p[@class="emoji-text mod-comment" ' + const projectMessageXpath = '//p[@class="emoji-text mod-comment" ' +
`and contains(text(), "${projectComment}")]`; `and contains(text(), "${projectComment}")]`;
@ -148,7 +148,7 @@ describe('comment tests', () => {
}); });
test('profile comment message visible', async () => { test('profile comment message visible', async () => {
await driver.get(`${rootUrl}/messages`); await navigate(`${rootUrl}/messages`);
const profileMessageXpath = '//p[@class="emoji-text mod-comment" ' + const profileMessageXpath = '//p[@class="emoji-text mod-comment" ' +
`and contains(text(), "${profileComment}")]`; `and contains(text(), "${profileComment}")]`;
@ -163,7 +163,7 @@ describe('comment tests', () => {
const 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]`; `and contains(text(), "${projectComment}")]/../../../p[@class = "comment-message-info"]/span/a[2]`;
await driver.get(`${rootUrl}/messages`); await navigate(`${rootUrl}/messages`);
await clickXpath(projectLinkXpath); await clickXpath(projectLinkXpath);
// find green flag overlay // find green flag overlay
@ -175,7 +175,7 @@ describe('comment tests', () => {
const 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]`; `and contains(text(), "${projectComment}")]/../../../p[@class = "comment-message-info"]/span/a[2]`;
await driver.get(`${rootUrl}/messages`); await navigate(`${rootUrl}/messages`);
await clickXpath(projectLinkXpath); await clickXpath(projectLinkXpath);
const commentXpath = `//span[contains(text(), "${projectComment}")]`; const commentXpath = `//span[contains(text(), "${projectComment}")]`;
@ -189,7 +189,7 @@ describe('comment tests', () => {
`and contains(text(), "${projectComment}")]/../../../p[@class = "comment-message-info"]/span/a[2]`; `and contains(text(), "${projectComment}")]/../../../p[@class = "comment-message-info"]/span/a[2]`;
const containerXpath = `//span[contains(text(), "${projectComment}")]/../../../..`; const containerXpath = `//span[contains(text(), "${projectComment}")]/../../../..`;
await driver.get(`${rootUrl}/messages`); await navigate(`${rootUrl}/messages`);
await clickXpath(projectLinkXpath); await clickXpath(projectLinkXpath);
const commentContainer = await findByXpath(containerXpath); const commentContainer = await findByXpath(containerXpath);
@ -201,7 +201,7 @@ describe('comment tests', () => {
const profileLinkXpath = '//p[@class="emoji-text mod-comment" ' + const profileLinkXpath = '//p[@class="emoji-text mod-comment" ' +
`and contains(text(), "${profileComment}")]/../../../` + `and contains(text(), "${profileComment}")]/../../../` +
'p[@class = "comment-message-info"]/span/a[2]'; 'p[@class = "comment-message-info"]/span/a[2]';
await driver.get(`${rootUrl}/messages`); await navigate(`${rootUrl}/messages`);
await clickXpath(profileLinkXpath); await clickXpath(profileLinkXpath);
// find profile data // find profile data
@ -218,7 +218,7 @@ describe('comment tests', () => {
const profileLinkXpath = '//p[@class="emoji-text mod-comment" ' + const profileLinkXpath = '//p[@class="emoji-text mod-comment" ' +
`and contains(text(), "${profileComment}")]/../../../` + `and contains(text(), "${profileComment}")]/../../../` +
'p[@class = "comment-message-info"]/span/a[2]'; 'p[@class = "comment-message-info"]/span/a[2]';
await driver.get(`${rootUrl}/messages`); await navigate(`${rootUrl}/messages`);
await clickXpath(profileLinkXpath); await clickXpath(profileLinkXpath);
// find comment // find comment
@ -233,7 +233,7 @@ describe('comment tests', () => {
const profileLinkXpath = '//p[@class="emoji-text mod-comment" ' + const profileLinkXpath = '//p[@class="emoji-text mod-comment" ' +
`and contains(text(), "${profileComment}")]/../../../` + `and contains(text(), "${profileComment}")]/../../../` +
'p[@class = "comment-message-info"]/span/a[2]'; 'p[@class = "comment-message-info"]/span/a[2]';
await driver.get(`${rootUrl}/messages`); await navigate(`${rootUrl}/messages`);
await clickXpath(profileLinkXpath); await clickXpath(profileLinkXpath);
// comment highlighted? // comment highlighted?
@ -244,7 +244,7 @@ describe('comment tests', () => {
}); });
test('project: reply to comment', async () => { test('project: reply to comment', async () => {
await driver.get(projectUrl); await navigate(projectUrl);
const commentXpath = `//span[contains(text(), "${projectComment}")]/../..`; const commentXpath = `//span[contains(text(), "${projectComment}")]/../..`;
const replyXpath = `${commentXpath}//span[@class = "comment-reply"]`; const replyXpath = `${commentXpath}//span[@class = "comment-reply"]`;
await clickXpath(replyXpath); await clickXpath(replyXpath);
@ -256,8 +256,7 @@ describe('comment tests', () => {
await composeBox.sendKeys(projectReply); await composeBox.sendKeys(projectReply);
// click post // click post
const postButton = await findByXpath(`${replyRow}//button[@class = "button compose-post"]`); await clickXpath(`${replyRow}//button[@class = "button compose-post"]`);
await postButton.click();
const postedReply = await findByXpath(`//span[contains(text(), "${projectReply}")]`); const postedReply = await findByXpath(`//span[contains(text(), "${projectReply}")]`);
const commentVisible = await postedReply.isDisplayed(); const commentVisible = await postedReply.isDisplayed();
@ -265,7 +264,7 @@ describe('comment tests', () => {
}); });
test('profile reply to comment', async () => { test('profile reply to comment', async () => {
await driver.get(profileUrl); await navigate(profileUrl);
// find the comment and click reply // find the comment and click reply
const commentXpath = `//div[contains(text(), "${profileComment}")]/..`; const commentXpath = `//div[contains(text(), "${profileComment}")]/..`;
await clickXpath(`${commentXpath}//a[@class = "reply"]`); await clickXpath(`${commentXpath}//a[@class = "reply"]`);
@ -282,7 +281,7 @@ describe('comment tests', () => {
}); });
test('studio: reply to comment', async () => { test('studio: reply to comment', async () => {
await driver.get(studioUrl); await navigate(studioUrl);
// find the comment and click reply // find the comment and click reply
const commentXpath = `//span[contains(text(), "${studioComment}")]/../..`; const commentXpath = `//span[contains(text(), "${studioComment}")]/../..`;

View file

@ -5,7 +5,9 @@ const SeleniumHelper = require('./selenium-helpers.js');
const { const {
clickText, clickText,
buildDriver, buildDriver,
findText findText,
navigate,
waitUntilDocumentReady
} = new SeleniumHelper(); } = new SeleniumHelper();
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
@ -20,7 +22,7 @@ describe('www-integration footer links', () => {
}); });
beforeEach(async () => { beforeEach(async () => {
await driver.get(rootUrl); await navigate(rootUrl);
await findText('Create stories, games, and animations'); await findText('Create stories, games, and animations');
}); });
@ -30,6 +32,7 @@ describe('www-integration footer links', () => {
test('click About Scratch link', async () => { test('click About Scratch link', async () => {
await clickText('About Scratch'); await clickText('About Scratch');
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl(); const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname; const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/about\/?$/); expect(pathname).toMatch(/^\/about\/?$/);
@ -37,6 +40,7 @@ describe('www-integration footer links', () => {
test('click For Parents link', async () => { test('click For Parents link', async () => {
await clickText('For Parents'); await clickText('For Parents');
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl(); const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname; const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/parents\/?$/); expect(pathname).toMatch(/^\/parents\/?$/);
@ -44,6 +48,7 @@ describe('www-integration footer links', () => {
test('click For Educators link', async () => { test('click For Educators link', async () => {
await clickText('For Educators'); await clickText('For Educators');
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl(); const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname; const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/educators\/?$/); expect(pathname).toMatch(/^\/educators\/?$/);
@ -51,6 +56,7 @@ describe('www-integration footer links', () => {
test('click For Developers link', async () => { test('click For Developers link', async () => {
await clickText('For Developers'); await clickText('For Developers');
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl(); const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname; const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/developers\/?$/); expect(pathname).toMatch(/^\/developers\/?$/);
@ -60,6 +66,7 @@ describe('www-integration footer links', () => {
test('click Community Guidelines link', async () => { test('click Community Guidelines link', async () => {
await clickText('Community Guidelines'); await clickText('Community Guidelines');
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl(); const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname; const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/community_guidelines\/?$/); expect(pathname).toMatch(/^\/community_guidelines\/?$/);
@ -67,6 +74,7 @@ describe('www-integration footer links', () => {
test('click Discussion Forums link', async () => { test('click Discussion Forums link', async () => {
await clickText('Discussion Forums'); await clickText('Discussion Forums');
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl(); const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname; const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/discuss\/?$/); expect(pathname).toMatch(/^\/discuss\/?$/);
@ -74,6 +82,7 @@ describe('www-integration footer links', () => {
test('click Statistics link', async () => { test('click Statistics link', async () => {
await clickText('Statistics'); await clickText('Statistics');
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl(); const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname; const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/statistics\/?$/); expect(pathname).toMatch(/^\/statistics\/?$/);
@ -83,6 +92,7 @@ describe('www-integration footer links', () => {
test('click Ideas link', async () => { test('click Ideas link', async () => {
await clickText('Ideas'); await clickText('Ideas');
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl(); const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname; const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/ideas\/?$/); expect(pathname).toMatch(/^\/ideas\/?$/);
@ -90,6 +100,7 @@ describe('www-integration footer links', () => {
test('click FAQ link', async () => { test('click FAQ link', async () => {
await clickText('FAQ'); await clickText('FAQ');
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl(); const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname; const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/faq\/?$/); expect(pathname).toMatch(/^\/faq\/?$/);
@ -97,6 +108,7 @@ describe('www-integration footer links', () => {
test('click Download link', async () => { test('click Download link', async () => {
await clickText('Download'); await clickText('Download');
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl(); const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname; const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/download\/?$/); expect(pathname).toMatch(/^\/download\/?$/);
@ -104,6 +116,7 @@ describe('www-integration footer links', () => {
test('click Contact Us link', async () => { test('click Contact Us link', async () => {
await clickText('Contact Us'); await clickText('Contact Us');
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl(); const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname; const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/contact-us\/?$/); expect(pathname).toMatch(/^\/contact-us\/?$/);
@ -113,6 +126,7 @@ describe('www-integration footer links', () => {
test('click Terms of Use link', async () => { test('click Terms of Use link', async () => {
await clickText('Terms of Use'); await clickText('Terms of Use');
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl(); const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname; const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/terms_of_use\/?$/); expect(pathname).toMatch(/^\/terms_of_use\/?$/);
@ -120,6 +134,7 @@ describe('www-integration footer links', () => {
test('click Privacy Policy link', async () => { test('click Privacy Policy link', async () => {
await clickText('Privacy Policy'); await clickText('Privacy Policy');
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl(); const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname; const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/privacy_policy\/?$/); expect(pathname).toMatch(/^\/privacy_policy\/?$/);
@ -127,6 +142,7 @@ describe('www-integration footer links', () => {
test('click Cookies link', async () => { test('click Cookies link', async () => {
await clickText('Cookies'); await clickText('Cookies');
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl(); const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname; const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/cookies\/?$/); expect(pathname).toMatch(/^\/cookies\/?$/);
@ -139,23 +155,14 @@ describe('www-integration footer links', () => {
test('click DMCA link', async () => { test('click DMCA link', async () => {
await clickText('DMCA'); await clickText('DMCA');
await waitUntilDocumentReady();
const url = await driver.getCurrentUrl(); const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname; const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/DMCA\/?$/); expect(pathname).toMatch(/^\/DMCA\/?$/);
}); });
// ==== SCRATCH FAMILY column ====
test('click Scratch Conference link', async () => {
await clickText('Scratch Conference');
const url = await driver.getCurrentUrl();
const pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/scratch-conference\/?$/);
}); });
}); // The following links in the footer are skipped because they are not part of scratch-www
// The following links in are skipped because they are not on scratch.mit.edu
// Jobs // Jobs
// Press // Press
@ -166,3 +173,4 @@ describe('www-integration footer links', () => {
// SCRATCH JR (SCRATCHJR) // SCRATCH JR (SCRATCHJR)
// SCRATCH DAY // SCRATCH DAY
// SCRATCH FOUNDATION // SCRATCH FOUNDATION
// Scratch Conference

View file

@ -3,9 +3,11 @@
const SeleniumHelper = require('./selenium-helpers.js'); const SeleniumHelper = require('./selenium-helpers.js');
const { const {
buildDriver,
clickXpath, clickXpath,
findByXpath, findByXpath,
buildDriver navigate,
waitUntilDocumentReady
} = new SeleniumHelper(); } = new SeleniumHelper();
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
@ -17,11 +19,11 @@ let driver;
describe('www-integration project rows', () => { describe('www-integration project rows', () => {
beforeAll(async () => { beforeAll(async () => {
driver = await buildDriver('www-integration project rows'); driver = await buildDriver('www-integration project rows');
// driver.get(rootUrl); // navigate(rootUrl);
}); });
beforeEach(async () => { beforeEach(async () => {
await driver.get(rootUrl); await navigate(rootUrl);
}); });
afterAll(() => driver.quit()); afterAll(() => driver.quit());
@ -49,6 +51,7 @@ describe('www-integration project rows', () => {
test('Featured Studios link', async () => { test('Featured Studios link', async () => {
await clickXpath('//div[@class="box"][descendant::text()="Featured Studios"]' + await clickXpath('//div[@class="box"][descendant::text()="Featured Studios"]' +
'//div[contains(@class, "thumbnail")][1]/a[@class="thumbnail-image"]'); '//div[contains(@class, "thumbnail")][1]/a[@class="thumbnail-image"]');
await waitUntilDocumentReady();
const studioInfo = await findByXpath('//div[contains(@class, "studio-info")]'); const studioInfo = await findByXpath('//div[contains(@class, "studio-info")]');
const studioInfoDisplayed = await studioInfo.isDisplayed(); const studioInfoDisplayed = await studioInfo.isDisplayed();
expect(studioInfoDisplayed).toBe(true); expect(studioInfoDisplayed).toBe(true);

View file

@ -3,9 +3,11 @@
const SeleniumHelper = require('./selenium-helpers.js'); const SeleniumHelper = require('./selenium-helpers.js');
const { const {
findByXpath, buildDriver,
clickXpath, clickXpath,
buildDriver findByXpath,
navigate,
waitUntilDocumentReady
} = new SeleniumHelper(); } = new SeleniumHelper();
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
@ -18,14 +20,14 @@ let driver;
describe('www-integration join flow', () => { describe('www-integration join flow', () => {
beforeAll(async () => { beforeAll(async () => {
driver = await buildDriver('www-integration join flow'); driver = await buildDriver('www-integration join flow');
await driver.get(rootUrl);
}); });
afterAll(() => driver.quit()); afterAll(() => driver.quit());
beforeEach(async () => { beforeEach(async () => {
await driver.get(rootUrl); await navigate(rootUrl); // navigate to home page
await clickXpath('//a[@class="registrationLink"]'); await clickXpath('//a[@class="registrationLink"]'); // navigate to join page
await waitUntilDocumentReady();
}); });
test('click Join opens join modal', async () => { test('click Join opens join modal', async () => {
@ -35,22 +37,24 @@ describe('www-integration join flow', () => {
}); });
test('username validation message appears', async () => { test('username validation message appears', async () => {
await clickXpath('//input[contains(@name, "username")]'); 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 message = await findByXpath('//div[contains(@class, "validation-message")]');
const messageText = await message.getText(); const messageText = await message.getText();
expect(messageText).toEqual('Don\'t use your real name'); expect(messageText).toEqual('Don\'t use your real name');
}); });
test('password validation message appears', async () => { test('password validation message appears', async () => {
await clickXpath('//input[contains(@name, "password")]'); 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 message = await findByXpath('//div[contains(@class, "validation-message")]');
const messageText = await message.getText(); const messageText = await message.getText();
expect(messageText).toContain('Write it down so you remember.'); expect(messageText).toContain('Write it down so you remember.');
}); });
test('password confirmation validation message appears', async () => { test('password confirmation validation message appears', async () => {
await clickXpath('//input[contains(@name, "passwordConfirm")]'); 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 message = await findByXpath('//div[contains(@class, "validation-message")]');
const messageText = await message.getText(); const messageText = await message.getText();
expect(messageText).toEqual('Type password again'); expect(messageText).toEqual('Type password again');
@ -59,6 +63,7 @@ describe('www-integration join flow', () => {
test('username validation: too short', async () => { test('username validation: too short', async () => {
const textInput = await findByXpath('//input[contains(@name, "username")]'); const textInput = await findByXpath('//input[contains(@name, "username")]');
await textInput.click(); await textInput.click();
await driver.wait(() => driver.executeScript('return document.activeElement == arguments[0]', textInput));
await textInput.sendKeys('ab'); await textInput.sendKeys('ab');
await clickXpath('//div[@class = "join-flow-outer-content"]'); await clickXpath('//div[@class = "join-flow-outer-content"]');
const message = await findByXpath('//div[contains(@class, "validation-error")]'); const message = await findByXpath('//div[contains(@class, "validation-error")]');
@ -69,6 +74,7 @@ describe('www-integration join flow', () => {
test('username validation: username taken', async () => { test('username validation: username taken', async () => {
const textInput = await findByXpath('//input[contains(@name, "username")]'); const textInput = await findByXpath('//input[contains(@name, "username")]');
await textInput.click(); await textInput.click();
await driver.wait(() => driver.executeScript('return document.activeElement == arguments[0]', textInput));
await textInput.sendKeys(takenUsername); await textInput.sendKeys(takenUsername);
await clickXpath('//div[@class = "join-flow-outer-content"]'); await clickXpath('//div[@class = "join-flow-outer-content"]');
const message = await findByXpath('//div[contains(@class, "validation-error")]'); const message = await findByXpath('//div[contains(@class, "validation-error")]');
@ -79,6 +85,7 @@ describe('www-integration join flow', () => {
test('username validation: bad word', async () => { test('username validation: bad word', async () => {
const textInput = await findByXpath('//input[contains(@name, "username")]'); const textInput = await findByXpath('//input[contains(@name, "username")]');
await textInput.click(); await textInput.click();
await driver.wait(() => driver.executeScript('return document.activeElement == arguments[0]', textInput));
// Should be caught by the filter // Should be caught by the filter
await textInput.sendKeys('xxxxxxxxx'); await textInput.sendKeys('xxxxxxxxx');
await clickXpath('//div[@class = "join-flow-outer-content"]'); await clickXpath('//div[@class = "join-flow-outer-content"]');

View file

@ -7,7 +7,10 @@ const {
clickText, clickText,
clickXpath, clickXpath,
findByXpath, findByXpath,
signIn navigate,
signIn,
urlMatches,
waitUntilDocumentReady
} = new SeleniumHelper(); } = new SeleniumHelper();
const username = `${process.env.SMOKE_USERNAME}1`; const username = `${process.env.SMOKE_USERNAME}1`;
@ -24,8 +27,7 @@ let driver;
describe('www-integration my_stuff', () => { describe('www-integration my_stuff', () => {
beforeAll(async () => { beforeAll(async () => {
driver = await buildDriver('www-integration my_stuff'); driver = await buildDriver('www-integration my_stuff');
await driver.get(rootUrl); await navigate(rootUrl);
await driver.sleep(1000);
await signIn(username, password); await signIn(username, password);
await findByXpath('//span[contains(@class, "profile-name")]'); await findByXpath('//span[contains(@class, "profile-name")]');
}); });
@ -33,7 +35,7 @@ describe('www-integration my_stuff', () => {
afterAll(() => driver.quit()); afterAll(() => driver.quit());
test('verify My Stuff structure (tabs, title)', async () => { test('verify My Stuff structure (tabs, title)', async () => {
await driver.get(myStuffURL); await navigate(myStuffURL);
const header = await findByXpath('//div[@class="box-head"]/h2'); const header = await findByXpath('//div[@class="box-head"]/h2');
const headerVisible = await header.isDisplayed(); const headerVisible = await header.isDisplayed();
expect(headerVisible).toBe(true); expect(headerVisible).toBe(true);
@ -55,24 +57,25 @@ describe('www-integration my_stuff', () => {
}); });
test('clicking a project title should take you to the project page', async () => { 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 clickXpath('//span[@class="media-info-item title"]');
await driver.sleep(6000); await waitUntilDocumentReady();
const gui = await findByXpath('//div[@class="guiPlayer"]'); const gui = await findByXpath('//div[@class="guiPlayer"]');
const guiVisible = await gui.isDisplayed(); const guiVisible = await gui.isDisplayed();
expect(guiVisible).toBe(true); expect(guiVisible).toBe(true);
}); });
test('clicking "see inside" should take you to the editor', async () => { test('clicking "see inside" should take you to the editor', async () => {
await driver.get(myStuffURL); await navigate(myStuffURL);
await clickXpath('//a[@data-control="edit"]'); await clickXpath('//a[@data-control="edit"]');
await waitUntilDocumentReady();
const gf = await findByXpath('//img[@class="green-flag_green-flag_1kiAo"]'); const gf = await findByXpath('//img[@class="green-flag_green-flag_1kiAo"]');
const gfVisible = await gf.isDisplayed(); const gfVisible = await gf.isDisplayed();
expect(gfVisible).toBe(true); expect(gfVisible).toBe(true);
}); });
test('Add To button should bring up a list of studios', async () => { test('Add To button should bring up a list of studios', async () => {
await driver.get(myStuffURL); await navigate(myStuffURL);
await clickXpath('//div[@id="sidebar"]/ul/li[@data-tab="shared"]'); await clickXpath('//div[@id="sidebar"]/ul/li[@data-tab="shared"]');
await clickXpath('//div[@data-control="add-to"]'); await clickXpath('//div[@data-control="add-to"]');
const dropDown = await findByXpath('//div[@class="dropdown-menu"]/ul/li'); const dropDown = await findByXpath('//div[@class="dropdown-menu"]/ul/li');
@ -81,52 +84,53 @@ describe('www-integration my_stuff', () => {
}); });
test('+ New Project button should open the editor', async () => { test('+ New Project button should open the editor', async () => {
await driver.get(myStuffURL); await navigate(myStuffURL);
await clickText('+ New Project'); await clickText('+ New Project');
await waitUntilDocumentReady();
const gf = await findByXpath('//img[@class="green-flag_green-flag_1kiAo"]'); const gf = await findByXpath('//img[@class="green-flag_green-flag_1kiAo"]');
const gfVisible = await gf.isDisplayed(); const gfVisible = await gf.isDisplayed();
expect(gfVisible).toBe(true); expect(gfVisible).toBe(true);
}); });
test('+ New Studio button should take you to the studio page', async () => { test('+ New Studio button should take you to the studio page', async () => {
await driver.get(rateLimitCheck); await navigate(rateLimitCheck);
await driver.get(myStuffURL); await navigate(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]'); await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
await waitUntilDocumentReady();
const tabs = await findByXpath('//div[@class="studio-tabs"]'); const tabs = await findByXpath('//div[@class="studio-tabs"]');
const tabsVisible = await tabs.isDisplayed(); const tabsVisible = await tabs.isDisplayed();
expect(tabsVisible).toBe(true); expect(tabsVisible).toBe(true);
}); });
test('New studio rate limited to five', async () => { test('New studio rate limited to five', async () => {
await driver.get(rateLimitCheck); await navigate(rateLimitCheck);
// 1st studio // 1st studio
await driver.get(myStuffURL); await navigate(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]'); await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
await findByXpath('//div[@class="studio-tabs"]'); await urlMatches(/\/studios\//);
// 2nd studio // 2nd studio
await driver.get(myStuffURL); await navigate(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]'); await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
await findByXpath('//div[@class="studio-tabs"]'); await urlMatches(/\/studios\//);
// 3rd studio // 3rd studio
await driver.get(myStuffURL); await navigate(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]'); await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
await findByXpath('//div[@class="studio-tabs"]'); await urlMatches(/\/studios\//);
// 4th studio // 4th studio
await driver.get(myStuffURL); await navigate(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]'); await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
await findByXpath('//div[@class="studio-tabs"]'); await urlMatches(/\/studios\//);
// 5th studio // 5th studio
await driver.get(myStuffURL); await navigate(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]'); await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
await findByXpath('//div[@class="studio-tabs"]'); await urlMatches(/\/studios\//);
// 6th studio should fail // 6th studio should fail
await driver.get(myStuffURL); await navigate(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]'); await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
// findByXpath checks for both presence and visibility
const alertMessage = await findByXpath('//div[contains(@class, "alert-error")]'); const alertMessage = await findByXpath('//div[contains(@class, "alert-error")]');
const errVisible = await alertMessage.isDisplayed(); expect(alertMessage).toBeTruthy();
expect(errVisible).toBe(true);
await driver.get(rateLimitCheck); await navigate(rateLimitCheck);
}); });
}); });

View file

@ -4,8 +4,9 @@ const SeleniumHelper = require('./selenium-helpers.js');
const { const {
clickXpath, clickXpath,
buildDriver,
findByXpath, findByXpath,
buildDriver navigate
} = new SeleniumHelper(); } = new SeleniumHelper();
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
@ -20,7 +21,7 @@ describe('www-integration navbar links', () => {
}); });
beforeEach(async () => { beforeEach(async () => {
await driver.get(rootUrl); await navigate(rootUrl);
}); });
afterAll(() => driver.quit()); afterAll(() => driver.quit());

View file

@ -3,6 +3,7 @@
// some tests use projects owned by user #2 // some tests use projects owned by user #2
const SeleniumHelper = require('./selenium-helpers.js'); const SeleniumHelper = require('./selenium-helpers.js');
const {until} = require('selenium-webdriver');
import path from 'path'; import path from 'path';
const { const {
@ -11,7 +12,9 @@ const {
clickXpath, clickXpath,
findText, findText,
findByXpath, findByXpath,
isSignedIn,
signIn, signIn,
navigate,
waitUntilVisible waitUntilVisible
} = new SeleniumHelper(); } = new SeleniumHelper();
@ -52,11 +55,11 @@ describe('www-integration project-page signed out', () => {
beforeAll(async () => { beforeAll(async () => {
// expect(projectUrl).toBe(defined); // expect(projectUrl).toBe(defined);
driver = await buildDriver('www-integration project-page signed out'); driver = await buildDriver('www-integration project-page signed out');
await driver.get(rootUrl); await navigate(rootUrl);
}); });
beforeEach(async () => { beforeEach(async () => {
await driver.get(unownedSharedUrl); await navigate(unownedSharedUrl);
const 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 waitUntilVisible(gfOverlay, driver); await waitUntilVisible(gfOverlay, driver);
}); });
@ -103,7 +106,7 @@ describe('www-integration project-page signed out', () => {
// Load an unshared project while signed out, get error // Load an unshared project while signed out, get error
test('Load an ushared project you do not own (error)', async () => { test('Load an ushared project you do not own (error)', async () => {
await driver.get(unownedUnsharedUrl); await navigate(unownedUnsharedUrl);
const unavailableImage = await findByXpath('//img[@class="not-available-image"]'); const unavailableImage = await findByXpath('//img[@class="not-available-image"]');
await waitUntilVisible(unavailableImage, driver); await waitUntilVisible(unavailableImage, driver);
const unavailableVisible = await unavailableImage.isDisplayed(); const unavailableVisible = await unavailableImage.isDisplayed();
@ -117,14 +120,14 @@ describe('www-integration project-page signed in', () => {
beforeAll(async () => { beforeAll(async () => {
// expect(projectUrl).toBe(defined); // expect(projectUrl).toBe(defined);
driver = await buildDriver('www-integration project-page signed in'); 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 () => { 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(() => driver.quit()); afterAll(() => driver.quit());
@ -133,7 +136,7 @@ describe('www-integration project-page signed in', () => {
// Load a shared project you own // Load a shared project you own
test('Load a shared project you own', async () => { test('Load a shared project you own', async () => {
await driver.get(ownedSharedUrl); await navigate(ownedSharedUrl);
const 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 waitUntilVisible(gfOverlay, driver); await waitUntilVisible(gfOverlay, driver);
const gfVisible = await gfOverlay.isDisplayed(); const gfVisible = await gfOverlay.isDisplayed();
@ -142,7 +145,7 @@ describe('www-integration project-page signed in', () => {
// Load a shared project you don't own // Load a shared project you don't own
test('Load a shared project you do not own', async () => { test('Load a shared project you do not own', async () => {
await driver.get(unownedSharedUrl); await navigate(unownedSharedUrl);
const 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 waitUntilVisible(gfOverlay, driver); await waitUntilVisible(gfOverlay, driver);
const gfVisible = await gfOverlay.isDisplayed(); const gfVisible = await gfOverlay.isDisplayed();
@ -151,7 +154,7 @@ describe('www-integration project-page signed in', () => {
// Load an unshared project you own // Load an unshared project you own
test('Load an unshared project you own', async () => { test('Load an unshared project you own', async () => {
await driver.get(ownedUnsharedUrl); await navigate(ownedUnsharedUrl);
const 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 waitUntilVisible(gfOverlay, driver); await waitUntilVisible(gfOverlay, driver);
const gfVisible = await gfOverlay.isDisplayed(); const gfVisible = await gfOverlay.isDisplayed();
@ -160,7 +163,7 @@ describe('www-integration project-page signed in', () => {
// Load an unshared project you don't own, get error // Load an unshared project you don't own, get error
test('Load an ushared project you do not own (error)', async () => { test('Load an ushared project you do not own (error)', async () => {
await driver.get(unownedUnsharedUrl); await navigate(unownedUnsharedUrl);
const unavailableImage = await findByXpath('//img[@class="not-available-image"]'); const unavailableImage = await findByXpath('//img[@class="not-available-image"]');
await waitUntilVisible(unavailableImage, driver); await waitUntilVisible(unavailableImage, driver);
const unavailableVisible = await unavailableImage.isDisplayed(); const unavailableVisible = await unavailableImage.isDisplayed();
@ -169,7 +172,7 @@ describe('www-integration project-page signed in', () => {
// Load a shared scratch 2 project you don't own // Load a shared scratch 2 project you don't own
test('Load a shared scratch 2 project you do not own', async () => { test('Load a shared scratch 2 project you do not own', async () => {
await driver.get(unownedSharedScratch2Url); await navigate(unownedSharedScratch2Url);
const 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 waitUntilVisible(gfOverlay, driver); await waitUntilVisible(gfOverlay, driver);
const gfVisible = await gfOverlay.isDisplayed(); const gfVisible = await gfOverlay.isDisplayed();
@ -178,7 +181,7 @@ describe('www-integration project-page signed in', () => {
// Load an unshared scratch 2 project you own // Load an unshared scratch 2 project you own
test('Load an unshared scratch 2 project you own', async () => { test('Load an unshared scratch 2 project you own', async () => {
await driver.get(ownedUnsharedScratch2Url); await navigate(ownedUnsharedScratch2Url);
const 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 waitUntilVisible(gfOverlay, driver); await waitUntilVisible(gfOverlay, driver);
const gfVisible = await gfOverlay.isDisplayed(); const gfVisible = await gfOverlay.isDisplayed();
@ -188,32 +191,29 @@ describe('www-integration project-page signed in', () => {
describe('www-integration project-creation signed in', () => { describe('www-integration project-creation signed in', () => {
beforeAll(async () => { beforeAll(async () => {
// expect(projectUrl).toBe(defined);
driver = await buildDriver('www-integration project-creation signed in'); 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 // 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 // https://support.saucelabs.com/hc/en-us/articles/115003685593-Uploading-Files-to-a-Sauce-Labs-Virtual-Machine-during-a-Test
if (remote) { if (remote) {
await driver.get('https://github.com/scratchfoundation/scratch-www/blob/develop/test/fixtures/project1.sb3'); await navigate('https://github.com/scratchfoundation/scratch-www/blob/develop/test/fixtures/project1.sb3');
await clickXpath('//Button[@data-testid="download-raw-button"]'); await clickXpath('//button[@data-testid="download-raw-button"]');
await driver.sleep(3000); await driver.sleep(3000);
} }
}); });
beforeEach(async () => { 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(() => driver.quit()); afterAll(() => driver.quit());
test('make a copy of a project', async () => { test('make a copy of a project', async () => {
await driver.get(`${ownedUnsharedUrl}/editor`); await navigate(`${ownedUnsharedUrl}/editor`);
const gf = await findByXpath('//img[@class="green-flag_green-flag_1kiAo"]');
await gf.isDisplayed();
await clickXpath(FILE_MENU_XPATH); await clickXpath(FILE_MENU_XPATH);
await clickText('Save as a copy'); await clickText('Save as a copy');
const successAlert = await findText('Project saved as a copy.'); const successAlert = await findText('Project saved as a copy.');
@ -226,7 +226,7 @@ describe('www-integration project-creation signed in', () => {
}); });
test('remix a project', async () => { test('remix a project', async () => {
await driver.get(unownedSharedUrl); await navigate(unownedSharedUrl);
const 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 waitUntilVisible(gfOverlay, driver); await waitUntilVisible(gfOverlay, driver);
await clickXpath('//button[@class="button remix-button"]'); await clickXpath('//button[@class="button remix-button"]');
@ -245,17 +245,17 @@ describe('www-integration project-creation signed in', () => {
'/Users/chef/Downloads/project1.sb3' : '/Users/chef/Downloads/project1.sb3' :
path.resolve(__dirname, '../fixtures/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"]'); await clickXpath('//li[@class="link create"]');
const gf = await findByXpath('//img[@class="green-flag_green-flag_1kiAo"]');
await gf.isDisplayed(); // upload file
await clickXpath(FILE_MENU_XPATH); await clickXpath(FILE_MENU_XPATH);
await clickText('Load from your computer'); await clickText('Load from your computer');
await driver.sleep(1000);
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]'); const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
await input.sendKeys(projectPath); await input.sendKeys(projectPath);
// accept alert // accept alert
await driver.wait(until.alertIsPresent());
const alert = await driver.switchTo().alert(); const alert = await driver.switchTo().alert();
await alert.accept(); await alert.accept();

View file

@ -6,7 +6,8 @@ const {
buildDriver, buildDriver,
clickXpath, clickXpath,
findByXpath, findByXpath,
getKey getKey,
navigate
} = new SeleniumHelper(); } = new SeleniumHelper();
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
@ -21,7 +22,7 @@ describe('www-integration search', () => {
}); });
beforeEach(async () => { beforeEach(async () => {
await driver.get(rootUrl); await navigate(rootUrl);
}); });
afterAll(() => driver.quit()); afterAll(() => driver.quit());

View file

@ -8,9 +8,9 @@ const chromedriverVersion = require('chromedriver').version;
const headless = process.env.SMOKE_HEADLESS || false; const headless = process.env.SMOKE_HEADLESS || false;
const remote = process.env.SMOKE_REMOTE || false; const remote = process.env.SMOKE_REMOTE || false;
const ci = process.env.CI || false; const ciBuildPrefix = process.env.CI ?
const usingCircle = process.env.CIRCLECI || false; `CI #${process.env.GITHUB_RUN_ID}/${process.env.GITHUB_RUN_ATTEMPT}` :
const buildID = process.env.CIRCLE_BUILD_NUM || '0000'; ''; // no prefix if not in CI
const {SAUCE_USERNAME, SAUCE_ACCESS_KEY} = process.env; const {SAUCE_USERNAME, SAUCE_ACCESS_KEY} = process.env;
const {By, Key, until} = webdriver; const {By, Key, until} = webdriver;
@ -135,8 +135,11 @@ class SeleniumHelper {
'getDriver', 'getDriver',
'getLogs', 'getLogs',
'getSauceDriver', 'getSauceDriver',
'isSignedIn',
'navigate',
'signIn', 'signIn',
'urlMatches', 'urlMatches',
'waitUntilDocumentReady',
'waitUntilGone' 'waitUntilGone'
]); ]);
@ -157,9 +160,8 @@ class SeleniumHelper {
buildDriver (name) { buildDriver (name) {
if (remote === 'true'){ if (remote === 'true'){
let nameToUse; let nameToUse;
if (ci === 'true'){ if (ciBuildPrefix){
const ciName = usingCircle ? 'circleCi ' : 'unknown '; nameToUse = `${ciBuildPrefix}: ${name}`;
nameToUse = `${ciName + buildID} : ${name}`;
} else { } else {
nameToUse = name; nameToUse = name;
} }
@ -230,8 +232,7 @@ class SeleniumHelper {
accessKey: accessKey, accessKey: accessKey,
name: name name: name
}) })
.usingServer(`http://${username}:${accessKey .usingServer(`http://${username}:${accessKey}@ondemand.saucelabs.com:80/wd/hub`)
}@ondemand.saucelabs.com:80/wd/hub`)
.build(); .build();
return driver; return driver;
} }
@ -247,6 +248,40 @@ class SeleniumHelper {
return Key[keyName]; return Key[keyName];
} }
/**
* 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);
}
}
/**
* 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);
}
}
/** /**
* Find an element by xpath. * Find an element by xpath.
* @param {string} xpath The xpath to search for. * @param {string} xpath The xpath to search for.
@ -284,8 +319,27 @@ class SeleniumHelper {
async clickXpath (xpath) { async clickXpath (xpath) {
const outerError = new SeleniumHelperError('clickXpath failed', [{xpath}]); const outerError = new SeleniumHelperError('clickXpath failed', [{xpath}]);
try { try {
const el = await this.findByXpath(xpath); return await this.driver.wait(new webdriver.WebElementCondition(
await el.click(); '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;
}
throw e;
}
}
), DEFAULT_TIMEOUT_MILLISECONDS);
} catch (cause) { } catch (cause) {
throw await outerError.chain(cause, this.driver); throw await outerError.chain(cause, this.driver);
} }
@ -384,6 +438,57 @@ class SeleniumHelper {
} }
} }
/**
* @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. * Sign in on a `scratch-www` page.
* @param {string} username The username to sign in with. * @param {string} username The username to sign in with.
@ -396,12 +501,13 @@ class SeleniumHelper {
{password: password ? 'provided' : 'absent'} {password: password ? 'provided' : 'absent'}
]); ]);
try { try {
await this.clickXpath('//li[@class="link right login-item"]/a'); await this.clickXpath(this.getPathForLogin());
const name = await this.findByXpath('//input[@id="frc-username-1088"]'); const name = await this.findByXpath('//input[@id="frc-username-1088"]');
await name.sendKeys(username); await name.sendKeys(username);
const word = await this.findByXpath('//input[@id="frc-password-1088"]'); const word = await this.findByXpath('//input[@id="frc-password-1088"]');
await word.sendKeys(password + this.getKey('ENTER')); await word.sendKeys(password + this.getKey('ENTER'));
await this.findByXpath('//span[contains(@class, "profile-name")]'); await this.waitUntilDocumentReady();
await this.findByXpath(this.getPathForProfileName());
} catch (cause) { } catch (cause) {
throw await outerError.chain(cause, this.driver); throw await outerError.chain(cause, this.driver);
} }

View file

@ -9,6 +9,7 @@ const {
clickXpath, clickXpath,
findByXpath, findByXpath,
getKey, getKey,
navigate,
signIn, signIn,
waitUntilVisible waitUntilVisible
} = new SeleniumHelper(); } = new SeleniumHelper();
@ -34,13 +35,13 @@ describe('www-integration sign-in-and-out', () => {
describe('sign in', () => { describe('sign in', () => {
afterEach(async () => { afterEach(async () => {
await driver.get(wwwURL); await navigate(wwwURL);
await clickXpath('//div[@class="account-nav"]'); await clickXpath('//div[@class="account-nav"]');
await clickText('Sign out'); await clickText('Sign out');
}); });
test('sign in on www', async () => { test('sign in on www', async () => {
await driver.get(wwwURL); await navigate(wwwURL);
await driver.sleep(1000); await driver.sleep(1000);
await clickXpath('//li[@class="link right login-item"]/a'); await clickXpath('//li[@class="link right login-item"]/a');
const name = await findByXpath('//input[@id="frc-username-1088"]'); const name = await findByXpath('//input[@id="frc-username-1088"]');
@ -57,7 +58,7 @@ describe('www-integration sign-in-and-out', () => {
}); });
test('sign in on scratchr2', async () => { test('sign in on scratchr2', async () => {
await driver.get(scratchr2url); await navigate(scratchr2url);
await clickXpath('//li[@class="sign-in dropdown"]/span'); await clickXpath('//li[@class="sign-in dropdown"]/span');
const name = await findByXpath('//input[@id="login_dropdown_username"]'); const name = await findByXpath('//input[@id="login_dropdown_username"]');
await name.sendKeys(username); await name.sendKeys(username);
@ -72,7 +73,7 @@ describe('www-integration sign-in-and-out', () => {
describe('sign out', () => { describe('sign out', () => {
beforeEach(async () => { beforeEach(async () => {
await driver.get(wwwURL); await navigate(wwwURL);
await signIn(username, password); await signIn(username, password);
await driver.sleep(500); await driver.sleep(500);
}); });
@ -86,7 +87,7 @@ describe('www-integration sign-in-and-out', () => {
}); });
test('sign out on scratchr2', async () => { test('sign out on scratchr2', async () => {
await driver.get(scratchr2url); await navigate(scratchr2url);
await clickXpath('//span[@class="user-name dropdown-toggle"]'); await clickXpath('//span[@class="user-name dropdown-toggle"]');
await clickXpath('//li[@id="logout"]'); await clickXpath('//li[@id="logout"]');
const element = await findByXpath('//li[@class="link right login-item"]/a/span'); const element = await findByXpath('//li[@class="link right login-item"]/a/span');
@ -101,7 +102,7 @@ describe('www-integration sign-in-and-out', () => {
const nonsenseUsername = Math.random().toString(36) const nonsenseUsername = Math.random().toString(36)
.replace(/[^a-z]+/g, '') .replace(/[^a-z]+/g, '')
.substr(0, 5); .substr(0, 5);
await driver.get(scratchr2url); await navigate(scratchr2url);
await clickXpath('//li[@class="sign-in dropdown"]/span'); await clickXpath('//li[@class="sign-in dropdown"]/span');
const name = await findByXpath('//input[@id="login_dropdown_username"]'); const name = await findByXpath('//input[@id="login_dropdown_username"]');
await name.sendKeys(nonsenseUsername + getKey('ENTER')); await name.sendKeys(nonsenseUsername + getKey('ENTER'));
@ -117,7 +118,7 @@ describe('www-integration sign-in-and-out', () => {
const nonsenseUsername = Math.random().toString(36) const nonsenseUsername = Math.random().toString(36)
.replace(/[^a-z]+/g, '') .replace(/[^a-z]+/g, '')
.substr(0, 5); .substr(0, 5);
await driver.get(scratchr2url); await navigate(scratchr2url);
await clickXpath('//li[@class="sign-in dropdown"]/span'); await clickXpath('//li[@class="sign-in dropdown"]/span');
const name = await findByXpath('//input[@id="login_dropdown_username"]'); const name = await findByXpath('//input[@id="login_dropdown_username"]');
await name.sendKeys(nonsenseUsername); await name.sendKeys(nonsenseUsername);
@ -135,7 +136,7 @@ describe('www-integration sign-in-and-out', () => {
const nonsensePassword = Math.random().toString(36) const nonsensePassword = Math.random().toString(36)
.replace(/[^a-z]+/g, '') .replace(/[^a-z]+/g, '')
.substr(0, 5); .substr(0, 5);
await driver.get(scratchr2url); await navigate(scratchr2url);
await clickXpath('//li[@class="sign-in dropdown"]/span'); await clickXpath('//li[@class="sign-in dropdown"]/span');
const name = await findByXpath('//input[@id="login_dropdown_username"]'); const name = await findByXpath('//input[@id="login_dropdown_username"]');
await name.sendKeys(username); await name.sendKeys(username);

View file

@ -6,7 +6,8 @@ const {
buildDriver, buildDriver,
clickText, clickText,
containsClass, containsClass,
findByXpath findByXpath,
navigate
} = new SeleniumHelper(); } = new SeleniumHelper();
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
@ -22,7 +23,7 @@ describe('www-integration statistics page', () => {
}); });
beforeEach(async () => { beforeEach(async () => {
await driver.get(statisticsPage); await navigate(statisticsPage);
}); });
afterAll(() => driver.quit()); afterAll(() => driver.quit());

View file

@ -3,10 +3,12 @@
import SeleniumHelper from './selenium-helpers.js'; import SeleniumHelper from './selenium-helpers.js';
const { const {
findByXpath,
buildDriver, buildDriver,
clickXpath,
clickText, clickText,
clickXpath,
findByXpath,
isSignedIn,
navigate,
signIn signIn
} = new SeleniumHelper(); } = new SeleniumHelper();
@ -33,11 +35,10 @@ describe('studio page while signed out', () => {
beforeAll(async () => { beforeAll(async () => {
// expect(projectUrl).toBe(defined); // expect(projectUrl).toBe(defined);
driver = await buildDriver('www-integration studio-page signed out'); driver = await buildDriver('www-integration studio-page signed out');
await driver.get(rootUrl);
}); });
beforeEach(async () => { beforeEach(async () => {
await driver.get(studioUrl); await navigate(studioUrl);
const studioNav = await findByXpath('//div[@class="studio-tabs"]'); const studioNav = await findByXpath('//div[@class="studio-tabs"]');
await studioNav.isDisplayed(); await studioNav.isDisplayed();
}); });
@ -45,7 +46,7 @@ describe('studio page while signed out', () => {
afterAll(() => driver.quit()); afterAll(() => driver.quit());
test('land on projects tab', async () => { test('land on projects tab', async () => {
await driver.get(studioUrl); await navigate(studioUrl);
const projectGrid = await findByXpath('//div[@class="studio-projects-grid"]'); const projectGrid = await findByXpath('//div[@class="studio-projects-grid"]');
const projectGridDisplayed = await projectGrid.isDisplayed(); const projectGridDisplayed = await projectGrid.isDisplayed();
expect(projectGridDisplayed).toBe(true); expect(projectGridDisplayed).toBe(true);
@ -70,13 +71,13 @@ describe('studio management', () => {
beforeAll(async () => { beforeAll(async () => {
driver = await buildDriver('www-integration studio management'); driver = await buildDriver('www-integration studio management');
await driver.get(rootUrl); await navigate(rootUrl);
// create a studio for tests // create a studio for tests
await signIn(username2, password); await signIn(username2, password);
await findByXpath('//span[contains(@class, "profile-name")]'); await findByXpath('//span[contains(@class, "profile-name")]');
await driver.get(rateLimitCheck); await navigate(rateLimitCheck);
await driver.get(myStuffURL); await navigate(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]'); await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
await findByXpath('//div[@class="studio-tabs"]'); await findByXpath('//div[@class="studio-tabs"]');
promoteStudioURL = await driver.getCurrentUrl(); promoteStudioURL = await driver.getCurrentUrl();
@ -84,10 +85,10 @@ describe('studio management', () => {
}); });
beforeEach(async () => { beforeEach(async () => {
if (await isSignedIn()) {
await clickXpath('//a[contains(@class, "user-info")]'); await clickXpath('//a[contains(@class, "user-info")]');
await clickText('Sign out'); await clickText('Sign out');
await driver.get(curatorTab); }
await findByXpath('//div[@class="studio-tabs"]');
}); });
afterAll(() => driver.quit()); afterAll(() => driver.quit());
@ -95,7 +96,7 @@ describe('studio management', () => {
test('invite a curator', async () => { test('invite a curator', async () => {
// sign in as user2 // sign in as user2
await signIn(username2, password); await signIn(username2, password);
await findByXpath('//span[contains(@class, "profile-name")]'); await navigate(curatorTab);
// invite user3 to curate // invite user3 to curate
const inviteBox = await findByXpath('//div[@class="studio-adder-row"]/input'); const inviteBox = await findByXpath('//div[@class="studio-adder-row"]/input');
@ -110,7 +111,7 @@ describe('studio management', () => {
test('accept curator invite', async () => { test('accept curator invite', async () => {
// Sign in user3 // Sign in user3
await signIn(username3, password); await signIn(username3, password);
await findByXpath('//span[contains(@class, "profile-name")]'); await navigate(curatorTab);
// accept the curator invite // accept the curator invite
await clickXpath('//button[@class="studio-invitation-button button"]'); await clickXpath('//button[@class="studio-invitation-button button"]');
@ -125,7 +126,7 @@ describe('studio management', () => {
await findByXpath('//span[contains(@class, "profile-name")]'); await findByXpath('//span[contains(@class, "profile-name")]');
// for some reason the user isn't showing up without waiting and reloading the page // for some reason the user isn't showing up without waiting and reloading the page
await driver.sleep(2000); await driver.sleep(2000);
await driver.get(curatorTab); await navigate(curatorTab);
// promote user3 // promote user3
const user3href = `/users/${username3}`; const user3href = `/users/${username3}`;
@ -148,7 +149,7 @@ describe('studio management', () => {
await signIn(username2, password); await signIn(username2, password);
await findByXpath('//span[contains(@class, "profile-name")]'); await findByXpath('//span[contains(@class, "profile-name")]');
// for some reason the user isn't showing up without reloading the page // for some reason the user isn't showing up without reloading the page
await driver.get(curatorTab); await navigate(curatorTab);
// open kebab menu // open kebab menu
const user2href = `/users/${username2}`; const user2href = `/users/${username2}`;
@ -180,8 +181,8 @@ describe('studio management', () => {
// click confirm // click confirm
// await clickXpath('//button[contains(@class, "confirm-transfer-button")]') // await clickXpath('//button[contains(@class, "confirm-transfer-button")]')
await clickXpath('//span[contains(text(), "Confirm")]/..'); await clickXpath('//span[contains(text(), "Confirm")]/..');
// findByXpath checks for both presence and visibility
const transferSuccess = await findByXpath('//div[contains(@class, "alert-success")]'); const transferSuccess = await findByXpath('//div[contains(@class, "alert-success")]');
const successVisible = await transferSuccess.isDisplayed(); expect(transferSuccess).toBeTruthy();
expect(successVisible).toBe(true);
}); });
}); });