From d161998acccf05b2429318c7ac89eda167c59710 Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Tue, 24 Aug 2021 20:26:48 +0200 Subject: [PATCH] test: check that JS projects can be submitted. (#43222) * test: check that JS projects can be submitted. * refactor: remove stale workflow comments * refactor: remove redundant build * chore: update Cypress version in CI * test: create separate electron-only workflow * test: put data-cy on correct button * test: drop mailhog from electron CI Co-authored-by: Shaun Hamilton * refactor: update differences comment * test: separate toggling from logging in * test: check solutions can be viewed on cert Co-authored-by: Shaun Hamilton --- .github/workflows/cypress-electron.yml | 83 ++++++++++++++++++ .github/workflows/cypress.yml | 13 +-- .../client-only-routes/show-project-links.tsx | 6 +- .../src/components/settings/Certification.js | 1 + .../integration/learn/challenges/projects.js | 87 ++++++++++++++++++- .../claim-cert-from-learn.js | 1 + cypress/plugins/index.js | 11 ++- cypress/support/commands.js | 1 - 8 files changed, 185 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/cypress-electron.yml diff --git a/.github/workflows/cypress-electron.yml b/.github/workflows/cypress-electron.yml new file mode 100644 index 0000000000..076c01563b --- /dev/null +++ b/.github/workflows/cypress-electron.yml @@ -0,0 +1,83 @@ +# Mirrors the Cypress workflow with the following changes: +# - Cypress run has a spec +# - We create the stats.json file here since the workflow only runs once. +# - Mailhog is missing since none of the tests use it. +name: Cypress Electron +on: + push: + branches-ignore: + - 'renovate/**' + pull_request: + +jobs: + cypress-run: + name: Test + runs-on: ubuntu-18.04 + strategy: + matrix: + browsers: [electron] + node-version: [14.x] + services: + mongodb: + image: mongo:3.6.19 + ports: + - 27017:27017 + + steps: + # We use .npmrc to set the default version to 0, and prevents download during development. + # This installs it specifically in the CI runs. + - name: Set Action Environment Variables + run: | + echo "CYPRESS_RECORD_KEY=${{ secrets.CYPRESS_RECORD_KEY }}" >> $GITHUB_ENV + echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV + echo "CYPRESS_INSTALL_BINARY=7.7.0" >> $GITHUB_ENV + + - name: Checkout Source Files + uses: actions/checkout@v2 + + - name: Checkout client config + uses: actions/checkout@v2 + with: + repository: freeCodeCamp/client-config + path: client-config + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Set freeCodeCamp Environment Variables + run: cp sample.env .env + + - name: Install Dependencies + run: | + npm ci + npm run ensure-env + + - name: Move serve.json to client + run: | + mkdir -p client/public + cp client-config/serve.json client/public/serve.json + + - name: Seed Database + run: npm run seed + + - name: Cypress run + uses: cypress-io/github-action@v2 + with: + record: ${{ env.CYPRESS_RECORD_KEY != 0 }} + build: npm run build + start: npm run start-ci + wait-on: http://localhost:8000 + wait-on-timeout: 1200 + config: baseUrl=http://localhost:8000 + browser: ${{ matrix.browsers }} + spec: 'cypress/integration/learn/challenges/projects.js' + headless: true + + - name: Upload Webpack stats + uses: actions/upload-artifact@v2 + with: + name: webpack-stats + path: client/public/stats.json diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index d2f2f63fa1..d77bb9b894 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -31,7 +31,7 @@ jobs: run: | echo "CYPRESS_RECORD_KEY=${{ secrets.CYPRESS_RECORD_KEY }}" >> $GITHUB_ENV echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV - echo "CYPRESS_INSTALL_BINARY=7.1.0" >> $GITHUB_ENV + echo "CYPRESS_INSTALL_BINARY=7.7.0" >> $GITHUB_ENV - name: Checkout Source Files uses: actions/checkout@v2 @@ -55,7 +55,6 @@ jobs: run: | npm ci npm run ensure-env - npm run build:curriculum - name: Move serve.json to client run: | @@ -70,19 +69,9 @@ jobs: with: record: ${{ env.CYPRESS_RECORD_KEY != 0 }} build: npm run build - # this should mirror the production build, but for now we're using gatsby - # serve instead (the npm script serve:client needs updating!) start: npm run start-ci wait-on: http://localhost:8000 - # the site builds in about 8 minutes, so there is currently 12 minutes of time - # left for testing. wait-on-timeout: 1200 config: baseUrl=http://localhost:8000 browser: ${{ matrix.browsers }} headless: true - - - name: Upload Webpack stats - uses: actions/upload-artifact@v2 - with: - name: webpack-stats - path: client/public/stats.json diff --git a/client/src/client-only-routes/show-project-links.tsx b/client/src/client-only-routes/show-project-links.tsx index 9b08ac561f..1522691d60 100644 --- a/client/src/client-only-routes/show-project-links.tsx +++ b/client/src/client-only-routes/show-project-links.tsx @@ -101,7 +101,11 @@ const ShowProjectLinks = (props: IShowProjectLinksProps): JSX.Element => { ); } return ( - ); diff --git a/client/src/components/settings/Certification.js b/client/src/components/settings/Certification.js index 3ff38c1b6b..84786b9c05 100644 --- a/client/src/components/settings/Certification.js +++ b/client/src/components/settings/Certification.js @@ -183,6 +183,7 @@ export class CertificationSettings extends Component { block={true} bsStyle='primary' className='btn-invert' + data-cy={projectTitle} id={`btn-for-${projectId}`} onClick={onClickHandler} > diff --git a/cypress/integration/learn/challenges/projects.js b/cypress/integration/learn/challenges/projects.js index 36bfc8d51f..9ccad60918 100644 --- a/cypress/integration/learn/challenges/projects.js +++ b/cypress/integration/learn/challenges/projects.js @@ -1,4 +1,10 @@ -const projects = { +/* global cy */ + +const selectors = { + editor: '.react-monaco-editor-container' +}; + +const pythonProjects = { superBlock: 'machine-learning-with-python', block: 'machine-learning-with-python-projects', challenges: [ @@ -24,10 +30,14 @@ const projects = { } ] }; + describe('project submission', () => { + beforeEach(() => { + cy.exec('npm run seed'); + }); // NOTE: this will fail once challenge tests are added. it('Should be possible to submit Python projects', () => { - const { superBlock, block, challenges } = projects; + const { superBlock, block, challenges } = pythonProjects; challenges.forEach(({ slug }) => { const url = `/learn/${superBlock}/${block}/${slug}`; cy.visit(url); @@ -47,4 +57,77 @@ describe('project submission', () => { // cy.url().should('not.have.string', url); }); }); + it( + 'JavaScript projects can be submitted and then viewed in /settings and on the certifications', + { browser: 'electron' }, + () => { + cy.login(); + cy.fixture('../../config/curriculum.json').then(curriculum => { + const { challenges, meta } = + curriculum['javascript-algorithms-and-data-structures'].blocks[ + 'javascript-algorithms-and-data-structures-projects' + ]; + + const projectTitles = meta.challengeOrder.map(([, title]) => title); + const projectsInOrder = projectTitles.map(projectTitle => { + return challenges.find(({ title }) => title === projectTitle); + }); + + // We need to wait for everything to finish loading and hydrating, so we + // use this text as a proxy for that. + const textInNextPage = projectTitles.slice(1); + textInNextPage.push('Claim Your Certification'); + + projectsInOrder.forEach( + ({ block, superBlock, dashedName, solutions }, i) => { + const url = `/learn/${superBlock}/${block}/${dashedName}`; + cy.visit(url); + + solutions.forEach(files => { + files.forEach(({ contents }) => { + cy.get(selectors.editor).as('editor'); + cy.get('@editor').click().focused().type('{ctrl+a}{del}'); + // NOTE: clipboard operations are flaky in watch mode, because + // the document can lose focus + cy.window() + .its('navigator.clipboard') + .invoke('writeText', contents); + cy.document().invoke('execCommand', 'paste'); + cy.contains('Run the Tests').click(); + cy.contains('Submit and go to next challenge', { + timeout: 8000 + }).click(); + cy.contains(textInNextPage[i]); + }); + }); + } + ); + + cy.visit('/settings'); + + projectTitles.forEach(title => { + cy.get(`[data-cy="${title}"]`).click(); + // TODO: if we write a test to check that the solution is visible + // before reloading, we should include that here. + cy.contains('Solution for'); + cy.contains('Close').click(); + }); + + // Claim and view solutions on certification page + + cy.toggleAll(); + cy.visit('/learn/javascript-algorithms-and-data-structures'); + cy.contains('Claim Certification').click(); + cy.contains('Show Certification').click(); + + projectTitles.forEach(title => { + cy.get(`[data-cy="${title} solution"]`).click(); + // TODO: if we write a test to check that the solution is visible + // before reloading, we should include that here. + cy.contains('Solution for'); + cy.contains('Close').click(); + }); + }); + } + ); }); diff --git a/cypress/integration/learn/responsive-web-design/claim-cert-from-learn.js b/cypress/integration/learn/responsive-web-design/claim-cert-from-learn.js index 26460d0963..fad120e588 100644 --- a/cypress/integration/learn/responsive-web-design/claim-cert-from-learn.js +++ b/cypress/integration/learn/responsive-web-design/claim-cert-from-learn.js @@ -49,6 +49,7 @@ describe('Responsive Web Design Superblock', () => { }); describe('After submitting all 5 projects', () => { before(() => { + cy.login(); cy.toggleAll(); const { superBlock, block, challenges } = projects; challenges.forEach(({ slug, solution }) => { diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index 6b713888e4..721b36beb6 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -11,7 +11,14 @@ // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) /* eslint-disable no-unused-vars */ + +const { execSync } = require('child_process'); +const { existsSync } = require('fs'); + module.exports = (on, config) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config + on('before:run', () => { + if (!existsSync('../../config/curriculum.json')) { + execSync('npm run build:curriculum'); + } + }); }; diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 3031c9c555..50a139436c 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -45,7 +45,6 @@ Cypress.Commands.add('login', () => { }); Cypress.Commands.add('toggleAll', () => { - cy.login(); cy.visit('/settings'); // cy.get('input[name="isLocked"]').click(); // cy.get('input[name="name"]').click();