From b4dfbc0ecf656081ee53809fb6200499e57e321a Mon Sep 17 00:00:00 2001 From: BryceLTaylor Date: Tue, 20 Oct 2020 10:11:23 -0400 Subject: [PATCH 001/121] Move Project Rows tests from Tap to Jest --- .../smoke-testing/test_project_rows.js | 93 ------------------- test/integration/project-rows.test.js | 59 ++++++++++++ 2 files changed, 59 insertions(+), 93 deletions(-) delete mode 100644 test/integration-legacy/smoke-testing/test_project_rows.js create mode 100644 test/integration/project-rows.test.js diff --git a/test/integration-legacy/smoke-testing/test_project_rows.js b/test/integration-legacy/smoke-testing/test_project_rows.js deleted file mode 100644 index 029125b3a..000000000 --- a/test/integration-legacy/smoke-testing/test_project_rows.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Checks that the some of the homepage rows on the homepage are displayed and - * contents have the right URLs to redirect to - * - * Test cases: https://github.com/LLK/scratch-www/wiki/Most-Important-Workflows - */ - -const SeleniumHelper = require('../selenium-helpers.js'); -const helper = new SeleniumHelper(); - -var tap = require('tap'); - -const webdriver = require('selenium-webdriver'); -const driver = helper.buildDriver('www-smoke test_project_rows'); - -var rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; - -// number of tests in the plan -tap.plan(4); - -tap.tearDown(function () { - // quit the instance of the browser - driver.quit(); -}); - -tap.beforeEach(function () { - // load the page with the driver - return driver.get(rootUrl); -}); - -// checks that the title of the first row is Featured Projects -tap.test('checkFeaturedProjectsRowTitleWhenSignedOut', function (t) { - var xPathLink = '//div[@class="box"]/div[@class="box-header"]/h4'; - driver.findElement(webdriver.By.xpath(xPathLink)) - .then(function (element) { - element.getText('h4') - .then(function (text) { - // expected value of the title text - var expectedText = 'Featured Projects'; - t.equal(text, expectedText); - t.end(); - }); - }); -}); - -// checks that the link for a project makes sense -tap.test('checkFeaturedProjectsRowLinkWhenSignedOut', function (t) { - var xPathLink = '//div[contains(@class, "thumbnail") ' + - 'and contains(@class, "project") and contains(@class, "slick-slide") ' + - 'and contains(@class, "slick-active")]/a[@class="thumbnail-image"]'; - driver.wait(webdriver.until - .elementLocated(webdriver.By.xpath(xPathLink))) - .then(function (element) { - element.getAttribute('href') - .then(function (url) { - // expected pattern for the project URL - // since I don't know the length of the project ID number - var expectedUrlRegExp = new RegExp('/projects/.*[0-9].*/?'); - t.match(url, expectedUrlRegExp); - t.end(); - }); - }); -}); - -// checks that the title of the 2nd row is Featured Studios -tap.test('checkFeaturedStudiosRowWhenSignedOut', function (t) { - var xPathLink = '//div[@class="box"][2]/div[@class="box-header"]/h4'; - driver.findElement(webdriver.By.xpath(xPathLink)) - .then(function (element) { - element.getText('h4') - .then(function (text) { - var expectedText = 'Featured Studios'; - t.equal(text, expectedText); - t.end(); - }); - }); -}); - -// checks that the link for a studio makes sense -tap.test('checkFeaturedStudiosRowLinkWhenSignedOut', function (t) { - var xPathLink = '//div[contains(@class, "thumbnail") and contains(@class, "gallery") ' + - 'and contains(@class, "slick-slide") ' + - 'and contains(@class, "slick-active")]/a[@class="thumbnail-image"]'; - driver.findElement(webdriver.By.xpath(xPathLink)) - .then(function (element) { - element.getAttribute('href') - .then(function (url) { - var expectedUrlRegExp = new RegExp('/studios/.*[0-9].*/?'); - t.match(url, expectedUrlRegExp); - t.end(); - }); - }); -}); diff --git a/test/integration/project-rows.test.js b/test/integration/project-rows.test.js new file mode 100644 index 000000000..3910c44d9 --- /dev/null +++ b/test/integration/project-rows.test.js @@ -0,0 +1,59 @@ +const SeleniumHelper = require('./selenium-helpers.js'); + +const { + clickXpath, + findByXpath, + buildDriver +} = new SeleniumHelper(); + +let remote = process.env.SMOKE_REMOTE || false; +let rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; + +if (remote) { + jest.setTimeout(60000); +} else { + jest.setTimeout(10000); +} + +let driver; + +describe('www-integration project rows', () => { + beforeAll(async () => { + driver = await buildDriver('www-integration project rows'); + // driver.get(rootUrl); + }); + + beforeEach(async () => { + await driver.get(rootUrl); + }); + + afterAll(async () => await 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'); + }); + + test('Featured Project link', async () => { + await clickXpath('//div[@class="box"][descendant::text()="Featured Projects"]' + + '//div[contains(@class, "thumbnail")][1]/a[@class="thumbnail-image"]'); + let gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]'); + let gfOverlayDisplayed = await gfOverlay.isDisplayed(); + await expect(gfOverlayDisplayed).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'); + }); + + test('Featured Studios link', async () => { + await clickXpath('//div[@class="box"][descendant::text()="Featured Studios"]' + + '//div[contains(@class, "thumbnail")][1]/a[@class="thumbnail-image"]'); + let galleryInfo = await findByXpath('//div[contains(@class, "gallery-info")]'); + let galleryInfoDisplayed = await galleryInfo.isDisplayed(); + await expect(galleryInfoDisplayed).toBe(true); + }); +}); From 7edb0b2cb8650230019149639368dc37bb5f8621 Mon Sep 17 00:00:00 2001 From: BryceLTaylor Date: Mon, 19 Oct 2020 10:58:36 -0400 Subject: [PATCH 002/121] Move Navbar integration tests from Tap to Jest --- .../smoke-testing/test_navbar_links.js | 123 ------------------ test/integration/navbar.test.js | 107 +++++++++++++++ 2 files changed, 107 insertions(+), 123 deletions(-) delete mode 100644 test/integration-legacy/smoke-testing/test_navbar_links.js create mode 100644 test/integration/navbar.test.js diff --git a/test/integration-legacy/smoke-testing/test_navbar_links.js b/test/integration-legacy/smoke-testing/test_navbar_links.js deleted file mode 100644 index 632c6d9e9..000000000 --- a/test/integration-legacy/smoke-testing/test_navbar_links.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Checks that the links in the navbar on the homepage have the right URLs to redirect to - * - * Test cases: https://github.com/LLK/scratch-www/wiki/Most-Important-Workflows - */ - -const SeleniumHelper = require('../selenium-helpers.js'); -const helper = new SeleniumHelper(); - -var tap = require('tap'); - -const webdriver = require('selenium-webdriver'); -const driver = helper.buildDriver('www-smoke test_navbar_links'); - -// Set test url through environment variable -var rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; - -// number of tests in the plan -tap.plan(7); - -tap.tearDown(function () { - // quit the instance of the browser - driver.quit(); -}); - -tap.beforeEach(function () { - // load the page with the driver - return driver.get(rootUrl); -}); - -// ==== Links in navbar ==== - -// the create link changes depending on whether the user is signed in or not (tips window opens) -tap.test('checkCreateLinkWhenSignedOut', function (t) { - var xPathLink = '//li[contains(@class, "link") and contains(@class, "create")]/a'; - var expectedHref = '/projects/editor/?tutorial=getStarted'; - driver.findElement(webdriver.By.xpath(xPathLink)) - .then(function (element) { - return element.getAttribute('href'); - }) - .then(function (url) { - t.equal(url.substr(-expectedHref.length), expectedHref); - t.end(); - }); -}); - -tap.test('checkExploreLinkWhenSignedOut', function (t) { - var xPathLink = '//li[contains(@class, "link") and contains(@class, "explore")]/a'; - var expectedHref = '/explore/projects/all'; - driver.findElement(webdriver.By.xpath(xPathLink)) - .then(function (element) { - return element.getAttribute('href'); - }) - .then(function (url) { - t.equal(url.substr(-expectedHref.length), expectedHref); - t.end(); - }); -}); - -tap.test('checkIdeasLinkWhenSignedOut', function (t) { - var xPathLink = '//li[contains(@class, "link") and contains(@class, "ideas")]/a'; - var expectedHref = '/ideas'; - driver.findElement(webdriver.By.xpath(xPathLink)) - .then(function (element) { - return element.getAttribute('href'); - }) - .then(function (url) { - t.equal(url.substr(-expectedHref.length), expectedHref); - t.end(); - }); -}); - -tap.test('checkAboutLinkWhenSignedOut', function (t) { - var xPathLink = '//li[contains(@class, "link") and contains(@class, "about")]/a'; - var expectedHref = '/about'; - driver.findElement(webdriver.By.xpath(xPathLink)) - .then(function (element) { - return element.getAttribute('href'); - }) - .then(function (url) { - t.equal(url.substr(-expectedHref.length), expectedHref); - t.end(); - }); -}); - -// ==== Search bar ==== - -tap.test('checkSearchBar', function (t) { - var xPathLink = '//input[@id="frc-q-1088"]'; - // search bar should exist - driver.findElement(webdriver.By.xpath(xPathLink)).then(function (element) { - t.ok(element); - t.end(); - }); -}); - -// ==== Join Scratch & Sign In ==== - -tap.test('checkJoinScratchLinkWhenSignedOut', function (t) { - var xPathLink = '//li[contains(@class, "link") and contains(@class, "right") and contains(@class, "join")]/a'; - var expectedText = 'Join Scratch'; - driver.findElement(webdriver.By.xpath(xPathLink)) - .then(function (element) { - return element.getText('a'); - }) - .then(function (text) { - t.equal(text, expectedText); - t.end(); - }); -}); - -tap.test('checkSignInLinkWhenSignedOut', function (t) { - var xPathLink = '//li[contains(@class, "link") and contains(@class, "right") and contains(@class, "login-item")]/a'; - var expectedText = 'Sign in'; - driver.findElement(webdriver.By.xpath(xPathLink)) - .then(function (element) { - return element.getText('a'); - }) - .then(function (text) { - t.equal(text, expectedText); - t.end(); - }); -}); diff --git a/test/integration/navbar.test.js b/test/integration/navbar.test.js new file mode 100644 index 000000000..424bdb6f3 --- /dev/null +++ b/test/integration/navbar.test.js @@ -0,0 +1,107 @@ +const SeleniumHelper = require('./selenium-helpers.js'); + +const { + clickXpath, + findByXpath, + buildDriver +} = new SeleniumHelper(); + +let remote = process.env.SMOKE_REMOTE || false; +let rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; + +if (remote) { + jest.setTimeout(60000); +} else { + jest.setTimeout(10000); +} + +let driver; + +describe('www-integration navbar links', () => { + beforeAll(async () => { + driver = await buildDriver('www-integration navbar links'); + }); + + beforeEach(async () => { + await driver.get(rootUrl); + }); + + afterAll(async () => await 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'); + + let explore = await findByXpath('//li[@class="link explore"]'); + let exploreText = await explore.getText(); + await expect(exploreText).toEqual('Explore'); + + let ideas = await findByXpath('//li[@class="link ideas"]'); + let ideasText = await ideas.getText(); + await expect(ideasText).toEqual('Ideas'); + + let about = await findByXpath('//li[@class="link about"]'); + let aboutText = await about.getText(); + await expect(aboutText).toEqual('About'); + + let join = await findByXpath('//a[@class="registrationLink"]'); + let joinText = await join.getText(); + await 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'); + }); + + test('create when signed out', async () => { + await clickXpath('//li[@class="link create"]'); + let infoArea = await findByXpath('//div[@class="sprite-info_sprite-info_3EyZh box_box_2jjDp"]'); + let areaVisible = await infoArea.isDisplayed(); + await expect(areaVisible).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'); + }); + + 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); + }); + + 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); + }); + + test('Search Bar', async () => { + let searchBar = await findByXpath('//input[@id="frc-q-1088"]'); + // await searchBar.click(); + 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'); + }); + + 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(); + expect(splashVisible).toBe(true); + }); + + // Sign In is tested in sign-in-and-out tests + // Create Account is tested in Join tests +}); From 6bea2b3a2c69004deda1556684a742510c6fb5d0 Mon Sep 17 00:00:00 2001 From: BryceLTaylor Date: Tue, 20 Oct 2020 10:54:14 -0400 Subject: [PATCH 003/121] Remove leftover comment in navbar test --- test/integration/navbar.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration/navbar.test.js b/test/integration/navbar.test.js index 424bdb6f3..fca59c230 100644 --- a/test/integration/navbar.test.js +++ b/test/integration/navbar.test.js @@ -84,7 +84,6 @@ describe('www-integration navbar links', () => { test('Search Bar', async () => { let searchBar = await findByXpath('//input[@id="frc-q-1088"]'); - // await searchBar.click(); await searchBar.sendKeys('cat'); await driver.sleep(500); // without it sends an empty string on submit await searchBar.submit(); From 37d279f6e5b925ed388a78ae3fbfca0fac4b46da Mon Sep 17 00:00:00 2001 From: BryceLTaylor Date: Thu, 22 Oct 2020 14:54:58 -0400 Subject: [PATCH 004/121] Search for the gui component in navbar create test --- test/integration/navbar.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/navbar.test.js b/test/integration/navbar.test.js index fca59c230..403082645 100644 --- a/test/integration/navbar.test.js +++ b/test/integration/navbar.test.js @@ -56,9 +56,9 @@ describe('www-integration navbar links', () => { test('create when signed out', async () => { await clickXpath('//li[@class="link create"]'); - let infoArea = await findByXpath('//div[@class="sprite-info_sprite-info_3EyZh box_box_2jjDp"]'); - let areaVisible = await infoArea.isDisplayed(); - await expect(areaVisible).toBe(true); + let gui = await findByXpath('//div[contains(@class, "gui")]'); + let guiVisible = await gui.isDisplayed(); + await expect(guiVisible).toBe(true); }); test('Explore link when signed out', async () => { From 0cd1f6e6bee478436c537785c37dbe4204bdb04d Mon Sep 17 00:00:00 2001 From: BryceLTaylor Date: Fri, 23 Oct 2020 14:00:11 -0400 Subject: [PATCH 005/121] Add class to searchbar for automated tests --- src/components/navigation/www/navigation.jsx | 1 + test/integration/navbar.test.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/navigation/www/navigation.jsx b/src/components/navigation/www/navigation.jsx index 39e69c242..0f25c79da 100644 --- a/src/components/navigation/www/navigation.jsx +++ b/src/components/navigation/www/navigation.jsx @@ -135,6 +135,7 @@ class Navigation extends React.Component { /> { }); test('Search Bar', async () => { - let searchBar = await findByXpath('//input[@id="frc-q-1088"]'); + let searchBar = await findByXpath('//div[contains(@class, "search-input")]/div/input'); await searchBar.sendKeys('cat'); await driver.sleep(500); // without it sends an empty string on submit await searchBar.submit(); From 7a5bdc040064ff56eff4fe41207e03777ef3b525 Mon Sep 17 00:00:00 2001 From: BryceLTaylor Date: Tue, 27 Oct 2020 11:47:59 -0400 Subject: [PATCH 006/121] Change class for search input wrapper to search-wrapper --- src/components/navigation/www/navigation.jsx | 2 +- test/integration/navbar.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/navigation/www/navigation.jsx b/src/components/navigation/www/navigation.jsx index 0f25c79da..14a6d412b 100644 --- a/src/components/navigation/www/navigation.jsx +++ b/src/components/navigation/www/navigation.jsx @@ -135,7 +135,7 @@ class Navigation extends React.Component { /> { }); test('Search Bar', async () => { - let searchBar = await findByXpath('//div[contains(@class, "search-input")]/div/input'); + let 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(); From c8445561b30af14c191f0d2c15d0ac2da6a240ca Mon Sep 17 00:00:00 2001 From: Ben Wheeler Date: Tue, 22 Dec 2020 14:18:29 -0500 Subject: [PATCH 007/121] set, purge static_assets surrogate key on deploy --- bin/configure-fastly.js | 11 +++++------ package.json | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/bin/configure-fastly.js b/bin/configure-fastly.js index c3eb4cca2..2d3b0c189 100644 --- a/bin/configure-fastly.js +++ b/bin/configure-fastly.js @@ -270,12 +270,11 @@ async.auto({ fastly.activateVersion(results.version, function (e, resp) { if (e) throw new Error(e); process.stdout.write('Successfully configured and activated version ' + resp.number + '\n'); - if (process.env.FASTLY_PURGE_ALL) { - fastly.purgeAll(FASTLY_SERVICE_ID, function (error) { - if (error) throw new Error(error); - process.stdout.write('Purged all.\n'); - }); - } + // purge static_assets using surrogate key + fastly.purgeKey(FASTLY_SERVICE_ID, 'static_assets', function (error) { + if (error) throw new Error(error); + process.stdout.write('Purged static assets.\n'); + }); }); } }); diff --git a/package.json b/package.json index da40221bd..2023d6a8c 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "deploy": "npm run deploy:s3 && npm run deploy:fastly", "deploy:fastly": "node ./bin/configure-fastly.js", "deploy:s3": "npm run deploy:s3:all && npm run deploy:s3:svg && npm run deploy:s3:js", - "deploy:s3cmd": "s3cmd sync -P --delete-removed --add-header=Cache-Control:no-cache,public,max-age=3600", + "deploy:s3cmd": "s3cmd sync -P --delete-removed --add-header=Cache-Control:no-cache,public,max-age=3600 --add-header=Surrogate-Key:static_assets", "deploy:s3:all": "npm run deploy:s3cmd -- --exclude '.DS_Store' --exclude '*.svg' --exclude '*.js' ./build/ s3://$S3_BUCKET_NAME/", "deploy:s3:svg": "npm run deploy:s3cmd -- --exclude '*' --include '*.svg' --mime-type 'image/svg+xml' ./build/ s3://$S3_BUCKET_NAME/", "deploy:s3:js": "npm run deploy:s3cmd -- --exclude '*' --include '*.js' --mime-type 'application/javascript' ./build/ s3://$S3_BUCKET_NAME/", From 177c02824c9f00dd31514fa2139ea5996510c89d Mon Sep 17 00:00:00 2001 From: Ben Wheeler Date: Wed, 23 Dec 2020 14:25:01 -0500 Subject: [PATCH 008/121] use x-amz-meta-surrogate-key to get around S3 header goodlist --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2023d6a8c..2707f12b1 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "deploy": "npm run deploy:s3 && npm run deploy:fastly", "deploy:fastly": "node ./bin/configure-fastly.js", "deploy:s3": "npm run deploy:s3:all && npm run deploy:s3:svg && npm run deploy:s3:js", - "deploy:s3cmd": "s3cmd sync -P --delete-removed --add-header=Cache-Control:no-cache,public,max-age=3600 --add-header=Surrogate-Key:static_assets", + "deploy:s3cmd": "s3cmd sync -P --delete-removed --add-header=Cache-Control:no-cache,public,max-age=3600 --add-header=x-amz-meta-surrogate-key:static_assets", "deploy:s3:all": "npm run deploy:s3cmd -- --exclude '.DS_Store' --exclude '*.svg' --exclude '*.js' ./build/ s3://$S3_BUCKET_NAME/", "deploy:s3:svg": "npm run deploy:s3cmd -- --exclude '*' --include '*.svg' --mime-type 'image/svg+xml' ./build/ s3://$S3_BUCKET_NAME/", "deploy:s3:js": "npm run deploy:s3cmd -- --exclude '*' --include '*.js' --mime-type 'application/javascript' ./build/ s3://$S3_BUCKET_NAME/", From 37a5dd93652e6550d0989a86ad2228426061308c Mon Sep 17 00:00:00 2001 From: Ben Wheeler Date: Fri, 14 May 2021 19:17:22 -0400 Subject: [PATCH 009/121] use hyphen instead of underscore --- bin/configure-fastly.js | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/configure-fastly.js b/bin/configure-fastly.js index 2d3b0c189..d98c01b68 100644 --- a/bin/configure-fastly.js +++ b/bin/configure-fastly.js @@ -270,8 +270,8 @@ async.auto({ fastly.activateVersion(results.version, function (e, resp) { if (e) throw new Error(e); process.stdout.write('Successfully configured and activated version ' + resp.number + '\n'); - // purge static_assets using surrogate key - fastly.purgeKey(FASTLY_SERVICE_ID, 'static_assets', function (error) { + // purge static-assets using surrogate key + fastly.purgeKey(FASTLY_SERVICE_ID, 'static-assets', function (error) { if (error) throw new Error(error); process.stdout.write('Purged static assets.\n'); }); diff --git a/package.json b/package.json index 2707f12b1..4986ce2a0 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "deploy": "npm run deploy:s3 && npm run deploy:fastly", "deploy:fastly": "node ./bin/configure-fastly.js", "deploy:s3": "npm run deploy:s3:all && npm run deploy:s3:svg && npm run deploy:s3:js", - "deploy:s3cmd": "s3cmd sync -P --delete-removed --add-header=Cache-Control:no-cache,public,max-age=3600 --add-header=x-amz-meta-surrogate-key:static_assets", + "deploy:s3cmd": "s3cmd sync -P --delete-removed --add-header=Cache-Control:no-cache,public,max-age=3600 --add-header=x-amz-meta-surrogate-key:static-assets", "deploy:s3:all": "npm run deploy:s3cmd -- --exclude '.DS_Store' --exclude '*.svg' --exclude '*.js' ./build/ s3://$S3_BUCKET_NAME/", "deploy:s3:svg": "npm run deploy:s3cmd -- --exclude '*' --include '*.svg' --mime-type 'image/svg+xml' ./build/ s3://$S3_BUCKET_NAME/", "deploy:s3:js": "npm run deploy:s3cmd -- --exclude '*' --include '*.js' --mime-type 'application/javascript' ./build/ s3://$S3_BUCKET_NAME/", From 168cd5a01b99edbd1b61157921094b4d29bdebf2 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 17 May 2021 11:02:48 -0400 Subject: [PATCH 010/121] Update the curator invitation component --- src/views/studio/l10n.json | 3 ++ src/views/studio/lib/studio-member-actions.js | 4 +- src/views/studio/studio-curator-invite.jsx | 45 ++++++++++++++++--- src/views/studio/studio-curators.jsx | 10 ++--- src/views/studio/studio.jsx | 2 + src/views/studio/studio.scss | 34 ++++++++++++++ 6 files changed, 84 insertions(+), 14 deletions(-) diff --git a/src/views/studio/l10n.json b/src/views/studio/l10n.json index e8e934ae3..e14b9cea7 100644 --- a/src/views/studio/l10n.json +++ b/src/views/studio/l10n.json @@ -39,7 +39,10 @@ "studio.inviteCuratorsHeader": "Invite Curators", "studio.inviteCurator": "Invite", "studio.inviteCuratorPlaceholder": "Scratch Username", + "studio.curatorInvitationAccepted": "Congratulations! You are now a curator of this studio.", + "studio.curatorInvitation": "You’ve been invited to become a curator of this studio.", "studio.curatorAcceptInvite": "Accept Invite", + "studio.curatorInvitationError": "Something went wrong, try again later.", "studio.curatorsEmptyCanAdd1": "You don’t have curators right now.", "studio.curatorsEmptyCanAdd2": "Add some curators to collaborate with!", "studio.curatorsEmpty1": "This studio has no curators right now.", diff --git a/src/views/studio/lib/studio-member-actions.js b/src/views/studio/lib/studio-member-actions.js index 894cb5354..5e4b7a8b7 100644 --- a/src/views/studio/lib/studio-member-actions.js +++ b/src/views/studio/lib/studio-member-actions.js @@ -168,7 +168,9 @@ const acceptInvitation = () => ((dispatch, getState) => new Promise((resolve, re // Note: this assumes that the user items from the curator endpoint // are the same structure as the single user data returned from /users/:username dispatch(curators.actions.create(userBody, true)); - dispatch(setRoles({invited: false, curator: true})); + setTimeout(() => { + dispatch(setRoles({invited: false, curator: true})); + }, 5 * 1000); return resolve(); }); }); diff --git a/src/views/studio/studio-curator-invite.jsx b/src/views/studio/studio-curator-invite.jsx index 7fe1a129a..9995367c7 100644 --- a/src/views/studio/studio-curator-invite.jsx +++ b/src/views/studio/studio-curator-invite.jsx @@ -6,15 +6,42 @@ import classNames from 'classnames'; import {FormattedMessage} from 'react-intl'; import {acceptInvitation} from './lib/studio-member-actions'; +import {selectShowCuratorInvite} from '../../redux/studio-permissions'; -const StudioCuratorInvite = ({onSubmit}) => { +const StudioCuratorInvite = ({showCuratorInvite, onSubmit}) => { const [submitting, setSubmitting] = useState(false); - const [error, setError] = useState(null); + const [accepted, setAccepted] = useState(false); + const [error, setError] = useState(false); + + if (!showCuratorInvite) return null; + + if (error) { + return ( +
+
+ +
+
+ ); + } + + if (accepted) { + return ( +
+
+ +
+
+ ); + } return ( -
+
+
+ +
- {error &&
{error}
}
); }; StudioCuratorInvite.propTypes = { + showCuratorInvite: PropTypes.func, onSubmit: PropTypes.func }; -const mapStateToProps = () => ({}); +const mapStateToProps = state => ({ + showCuratorInvite: selectShowCuratorInvite(state) +}); const mapDispatchToProps = ({ onSubmit: acceptInvitation diff --git a/src/views/studio/studio-curators.jsx b/src/views/studio/studio-curators.jsx index 089eb5549..1d77e2319 100644 --- a/src/views/studio/studio-curators.jsx +++ b/src/views/studio/studio-curators.jsx @@ -8,12 +8,11 @@ import {curators} from './lib/redux-modules'; import Debug from './debug.jsx'; import {CuratorTile} from './studio-member-tile.jsx'; import CuratorInviter from './studio-curator-inviter.jsx'; -import CuratorInvite from './studio-curator-invite.jsx'; import {loadCurators} from './lib/studio-member-actions'; -import {selectCanInviteCurators, selectShowCuratorInvite} from '../../redux/studio-permissions'; +import {selectCanInviteCurators} from '../../redux/studio-permissions'; const StudioCurators = ({ - canInviteCurators, showCuratorInvite, items, error, loading, moreToLoad, onLoadMore + canInviteCurators, items, error, loading, moreToLoad, onLoadMore }) => { useEffect(() => { if (items.length === 0) onLoadMore(); @@ -24,7 +23,6 @@ const StudioCurators = ({

{canInviteCurators && } - {showCuratorInvite && } {error && ({ ...curators.selector(state), - canInviteCurators: selectCanInviteCurators(state), - showCuratorInvite: selectShowCuratorInvite(state) + canInviteCurators: selectCanInviteCurators(state) }), { onLoadMore: loadCurators diff --git a/src/views/studio/studio.jsx b/src/views/studio/studio.jsx index 2d4bd65c4..dddd41ff2 100644 --- a/src/views/studio/studio.jsx +++ b/src/views/studio/studio.jsx @@ -22,6 +22,7 @@ import StudioManagers from './studio-managers.jsx'; import StudioCurators from './studio-curators.jsx'; import StudioComments from './studio-comments.jsx'; import StudioActivity from './studio-activity.jsx'; +import StudioCuratorInvite from './studio-curator-invite.jsx'; import { projects, @@ -53,6 +54,7 @@ const StudioShell = ({studioLoadFailed}) => {
+ diff --git a/src/views/studio/studio.scss b/src/views/studio/studio.scss index d48a2bfbb..e2082e3ac 100644 --- a/src/views/studio/studio.scss +++ b/src/views/studio/studio.scss @@ -349,6 +349,40 @@ $radius: 8px; } } +.studio-invitation { + margin-top: 1rem; + padding: 1rem; + box-sizing: border-box; + min-height: 85px; /* So the box doesn't change height after being accepted */ + + display: flex; + justify-content: space-between; + align-items: center; + + @media #{$intermediate-and-smaller} { + flex-direction: column; + .studio-invitation-msg { + margin-top: .5rem; + margin-bottom: 1rem; + } + } +} + +.studio-info-box { + border-radius: 4px; + background: $ui-blue-10percent; + border: 1px solid $ui-blue-25percent; + + &.studio-info-box-success { + background: #CEF2E8; + border: 1px solid rgba(15, 189, 140, 0.5); + } + &.studio-info-box-error { + background: #FFF0DF; + border: 1px solid $ui-dark-orange; + } +} + /* Modification classes for different interaction states */ .mod-fetching { /* When a field has no content to display yet */ position: relative; From cc663412522bd602b87d115d8b00a96b28deb6f0 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 17 May 2021 14:53:02 -0400 Subject: [PATCH 011/121] Wait for session to be fetched before loading comments --- src/redux/session.js | 1 + src/views/studio/studio-comments.jsx | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/redux/session.js b/src/redux/session.js index 36ce89927..b6ed97cc4 100644 --- a/src/redux/session.js +++ b/src/redux/session.js @@ -128,6 +128,7 @@ module.exports.selectToken = state => get(state, ['session', 'session', 'user', module.exports.selectIsAdmin = state => get(state, ['session', 'session', 'permissions', 'admin'], false); module.exports.selectIsSocial = state => get(state, ['session', 'session', 'permissions', 'social'], false); module.exports.selectIsEducator = state => get(state, ['session', 'session', 'permissions', 'educator'], false); +module.exports.selectHasFetchedSession = state => state.session.status === module.exports.Status.FETCHED; // NB logged out user id as NaN so that it can never be used in equality testing since NaN !== NaN module.exports.selectUserId = state => get(state, ['session', 'session', 'user', 'id'], NaN); diff --git a/src/views/studio/studio-comments.jsx b/src/views/studio/studio-comments.jsx index b6ac7c15d..b477f0d94 100644 --- a/src/views/studio/studio-comments.jsx +++ b/src/views/studio/studio-comments.jsx @@ -9,7 +9,7 @@ import TopLevelComment from '../preview/comment/top-level-comment.jsx'; import studioCommentActions from '../../redux/studio-comment-actions.js'; import StudioCommentsAllowed from './studio-comments-allowed.jsx'; -import {selectIsAdmin} from '../../redux/session'; +import {selectIsAdmin, selectHasFetchedSession} from '../../redux/session'; import { selectShowCommentComposer, selectCanDeleteComment, @@ -24,6 +24,7 @@ const StudioComments = ({ comments, commentsAllowed, isAdmin, + hasFetchedSession, handleLoadMoreComments, handleNewComment, moreCommentsToLoad, @@ -42,8 +43,8 @@ const StudioComments = ({ handleLoadMoreReplies }) => { useEffect(() => { - if (comments.length === 0) handleLoadMoreComments(); - }, [comments.length === 0]); + if (comments.length === 0 && hasFetchedSession) handleLoadMoreComments(); + }, [comments.length === 0, hasFetchedSession]); // The comments you see depend on your admin status // so reset them if isAdmin changes. @@ -108,6 +109,7 @@ StudioComments.propTypes = { comments: PropTypes.arrayOf(PropTypes.shape({})), commentsAllowed: PropTypes.bool, isAdmin: PropTypes.bool, + hasFetchedSession: PropTypes.bool, handleLoadMoreComments: PropTypes.func, handleNewComment: PropTypes.func, moreCommentsToLoad: PropTypes.bool, @@ -133,6 +135,7 @@ export { export default connect( state => ({ comments: state.comments.comments, + hasFetchedSession: selectHasFetchedSession(state), isAdmin: selectIsAdmin(state), moreCommentsToLoad: state.comments.moreCommentsToLoad, replies: state.comments.replies, From efe961e10310bad4ffd8ef63e3bf1b6799144d9b Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 17 May 2021 15:24:51 -0400 Subject: [PATCH 012/121] Handle more types of project add errors --- src/views/studio/l10n.json | 6 +++++- src/views/studio/studio-project-adder.jsx | 16 ++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/views/studio/l10n.json b/src/views/studio/l10n.json index e8e934ae3..a95952604 100644 --- a/src/views/studio/l10n.json +++ b/src/views/studio/l10n.json @@ -26,7 +26,11 @@ "studio.projectsEmpty1": "This studio has no projects yet.", "studio.projectsEmpty2": "Suggest projects you want to add in the comments!", "studio.browseProjects": "Browse Projects", - "studio.projectErrors.checkUrl": "Could not add project. Check the URL and try again.", + "studio.projectErrors.checkUrl": "Could not find that project. Check the URL and try again.", + "studio.projectErrors.generic": "Could not add project", + "studio.projectErrors.tooFast": "You are adding projects too quickly", + "studio.projectErrors.permission": "You do not have permission to add that project", + "studio.projectErrors.duplicate": "That project is already in this studio", "studio.creatorRole": "Studio Creator", diff --git a/src/views/studio/studio-project-adder.jsx b/src/views/studio/studio-project-adder.jsx index abae09a92..7271cd4b2 100644 --- a/src/views/studio/studio-project-adder.jsx +++ b/src/views/studio/studio-project-adder.jsx @@ -5,10 +5,22 @@ import {connect} from 'react-redux'; import classNames from 'classnames'; import {FormattedMessage, intlShape, injectIntl} from 'react-intl'; -import {addProject} from './lib/studio-project-actions'; +import {Errors, addProject} from './lib/studio-project-actions'; import UserProjectsModal from './modals/user-projects-modal.jsx'; import ValidationMessage from '../../components/forms/validation-message.jsx'; +const errorToMessageId = error => { + switch (error) { + case Errors.NETWORK: return 'studio.projectErrors.generic'; + case Errors.SERVER: return 'studio.projectErrors.generic'; + case Errors.PERMISSION: return 'studio.projectErrors.permission'; + case Errors.DUPLICATE: return 'studio.projectErrors.duplicate'; + case Errors.RATE_LIMIT: return 'studio.projectErrors.tooFast'; + case Errors.UNKNOWN_PROJECT: return 'studio.projectErrors.checkUrl'; + default: return 'studio.projectErrors.generic'; + } +}; + const StudioProjectAdder = ({intl, onSubmit}) => { const [value, setValue] = useState(''); const [submitting, setSubmitting] = useState(false); @@ -30,7 +42,7 @@ const StudioProjectAdder = ({intl, onSubmit}) => { } + message={} />
} Date: Mon, 17 May 2021 15:31:08 -0400 Subject: [PATCH 013/121] Update tests --- test/unit/components/studio-comments.test.jsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/unit/components/studio-comments.test.jsx b/test/unit/components/studio-comments.test.jsx index 1f628fc66..1985d370f 100644 --- a/test/unit/components/studio-comments.test.jsx +++ b/test/unit/components/studio-comments.test.jsx @@ -7,10 +7,14 @@ describe('Studio comments', () => { const loadComments = jest.fn(); const component = mountWithIntl( ); + expect(loadComments).not.toHaveBeenCalled(); + component.setProps({hasFetchedSession: true}); + component.update(); expect(loadComments).toHaveBeenCalled(); // When updated to have comments, load is not called again @@ -30,6 +34,7 @@ describe('Studio comments', () => { const resetComments = jest.fn(); const component = mountWithIntl( { mountWithIntl( From b0eae61d5b6eb55f4e06ae94504959ee7acb66b0 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 17 May 2021 15:32:16 -0400 Subject: [PATCH 014/121] Fix periods --- src/views/studio/l10n.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/views/studio/l10n.json b/src/views/studio/l10n.json index a95952604..87cdfb8c3 100644 --- a/src/views/studio/l10n.json +++ b/src/views/studio/l10n.json @@ -27,10 +27,10 @@ "studio.projectsEmpty2": "Suggest projects you want to add in the comments!", "studio.browseProjects": "Browse Projects", "studio.projectErrors.checkUrl": "Could not find that project. Check the URL and try again.", - "studio.projectErrors.generic": "Could not add project", - "studio.projectErrors.tooFast": "You are adding projects too quickly", - "studio.projectErrors.permission": "You do not have permission to add that project", - "studio.projectErrors.duplicate": "That project is already in this studio", + "studio.projectErrors.generic": "Could not add project.", + "studio.projectErrors.tooFast": "You are adding projects too quickly.", + "studio.projectErrors.permission": "You do not have permission to add that project.", + "studio.projectErrors.duplicate": "That project is already in this studio.", "studio.creatorRole": "Studio Creator", From ff2e321067b523368d17d7a384c45759244e317c Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 17 May 2021 15:47:40 -0400 Subject: [PATCH 015/121] Add dynamic meta tags for studio pages --- src/routes.json | 3 ++- src/views/studio/studio-meta.jsx | 43 ++++++++++++++++++++++++++++++++ src/views/studio/studio.jsx | 2 ++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/views/studio/studio-meta.jsx diff --git a/src/routes.json b/src/routes.json index 97976d6bb..48832a30b 100644 --- a/src/routes.json +++ b/src/routes.json @@ -301,7 +301,8 @@ "pattern": "^/studios-playground/\\d+(/projects|/curators|/activity|/comments)?/?(\\?.*)?$", "routeAlias": "/studios-playground/?$", "view": "studio/studio", - "title": "Studio Playground" + "title": "Studio Playground", + "dynamicMetaTags": true }, { "name": "teacher-faq", diff --git a/src/views/studio/studio-meta.jsx b/src/views/studio/studio-meta.jsx new file mode 100644 index 000000000..540c5411a --- /dev/null +++ b/src/views/studio/studio-meta.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import Helmet from 'react-helmet'; +import PropTypes from 'prop-types'; +import {connect} from 'react-redux'; + +import {selectStudioDescription, selectStudioId, selectStudioTitle} from '../../redux/studio'; + +const StudioMeta = ({id, description, title}) => ( + + {`${title} - Scratch Studio`} + + + + + +); + +StudioMeta.propTypes = { + description: PropTypes.string, + id: PropTypes.string, + title: PropTypes.string +}; + +export default connect( + state => ({ + description: selectStudioDescription(state), + id: selectStudioId(state), + title: selectStudioTitle(state) + }) +)(StudioMeta); diff --git a/src/views/studio/studio.jsx b/src/views/studio/studio.jsx index 2d4bd65c4..17aaf67dc 100644 --- a/src/views/studio/studio.jsx +++ b/src/views/studio/studio.jsx @@ -22,6 +22,7 @@ import StudioManagers from './studio-managers.jsx'; import StudioCurators from './studio-curators.jsx'; import StudioComments from './studio-comments.jsx'; import StudioActivity from './studio-activity.jsx'; +import StudioMeta from './studio-meta.jsx'; import { projects, @@ -45,6 +46,7 @@ const StudioShell = ({studioLoadFailed}) => { studioLoadFailed ? :
+
From 536caf994a2593d63891d7463b1c2f2e9376022a Mon Sep 17 00:00:00 2001 From: apple502j <33279053+apple502j@users.noreply.github.com> Date: Tue, 18 May 2021 05:22:02 +0900 Subject: [PATCH 016/121] Linkify studio descriptions --- src/views/studio/studio-description.jsx | 35 +++++++++++++++++-------- src/views/studio/studio.scss | 6 +++++ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/views/studio/studio-description.jsx b/src/views/studio/studio-description.jsx index 3f3765302..be78232ef 100644 --- a/src/views/studio/studio-description.jsx +++ b/src/views/studio/studio-description.jsx @@ -12,6 +12,7 @@ import { } from '../../redux/studio-mutations'; import ValidationMessage from '../../components/forms/validation-message.jsx'; +import decorateText from '../../lib/decorate-text.jsx'; const errorToMessageId = error => { switch (error) { @@ -32,18 +33,30 @@ const StudioDescription = ({ }); return (
-