feat(test, e2e) test suit for cypress (#42138)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Sem Bauke
2021-06-14 18:37:52 +02:00
committed by GitHub
parent 08fc4014c7
commit 22b45761a7
35 changed files with 367 additions and 55 deletions

View File

@ -1,8 +1,5 @@
name: Cypress
name: Cypress - Pull-request
on:
push:
branches-ignore:
- 'renovate/**'
pull_request:
jobs:

91
.github/workflows/cypress-push.yml vendored Normal file
View File

@ -0,0 +1,91 @@
name: Cypress - Push
on:
push:
branches-ignore:
- 'renovate/**'
jobs:
cypress-run:
name: Test
runs-on: ubuntu-18.04
strategy:
fail-fast: false
matrix:
containers: [1, 2, 3, 4, 5, 6]
browsers: [chrome, firefox]
node-version: [14.x]
services:
mongodb:
image: mongo:3.6.19
ports:
- 27017:27017
mailhog:
image: mailhog/mailhog
ports:
- 1025:1025
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.1.0" >> $GITHUB_ENV
- name: Checkout Source Files
uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set freeCodeCamp Environment Variables
run: cp sample.env .env
- name: Install Dependencies
run: |
npm ci
npm run ensure-env
npm run build:curriculum
- name: Seed Database
run: npm run seed
- name: Generate fixture data
run: npm run precypress:gen:fixtures
- name: Generate Specfiles for challenges
run: npm run precypress:gen:test
- name: Cypress run
uses: cypress-io/github-action@v2
with:
parallel: ${{ env.CYPRESS_RECORD_KEY != 0 }}
group: ${{ matrix.browsers }}
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

5
.gitignore vendored
View File

@ -116,6 +116,10 @@ coverage
cypress/videos
cypress/screenshots
### Cypress generated fixtures and tests ###
cypress/fixtures/path-data
cypress/integration/challenge-tests/blocks
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
@ -179,3 +183,4 @@ api-server/lib/*
curriculum/dist
curriculum/build
client/static/_redirects

View File

@ -1,5 +1,6 @@
{
"projectId": "ke77ns",
"baseUrl": "http://localhost:8000",
"retries": 4
"retries": 4,
"videoUploadOnPasses": false
}

View File

@ -1,17 +1,5 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// 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 */
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
module.exports = function (on, config) {
// configure plugins here
};

View File

@ -1,37 +1,4 @@
/* global cy Cypress */
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => {});
//
//
// -- This is a child command --
// Cypress.Commands.add(
// 'drag',
// { prevSubject: 'element' },
// (subject, options) => {}
// );
//
//
// -- This is a dual command --
// Cypress.Commands.add(
// 'dismiss',
// { prevSubject: 'optional' },
// (subject, options) => {}
// );
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => {});
Cypress.Commands.add('login', () => {
cy.visit('/');
@ -52,3 +19,50 @@ Cypress.Commands.add('resetUsername', () => {
cy.contains('Account Settings for developmentuser').should('be.visible');
});
Cypress.Commands.add('testChallenges', () => {
// Test Meta tags
cy.get('head meta[charset=utf-8]');
cy.get('head meta[name=description]').should('have.attr', 'content');
// Test breadcrumbs
cy.get('.breadcrumb-right').should('have.attr', 'href');
cy.get('.ellipsis').should('be.visible');
cy.get('.breadcrumb-left').should('have.attr', 'href');
cy.get('.breadcrumb-left').should('be.visible', 'href');
cy.get('body').should('be.visible');
// Challenge content
cy.get('.challenge-title').should('be.visible');
cy.get('#description').should('be.visible');
// Monaco editor
cy.get('.react-monaco-editor-container')
.click()
.focused()
.type('<h1> Hello world! </h1>');
// Ensure that there are test
cy.get('.challenge-test-suite').children().its('length').should('be.gt', 0);
});
// This command can be used to test projects and back-end challenges
Cypress.Commands.add('testProjectsAndBackend', () => {
// Test breadcrumbs
cy.get('.breadcrumb-right').should('have.attr', 'href');
cy.get('.ellipsis').should('be.visible');
cy.get('.breadcrumb-left').should('have.attr', 'href');
cy.get('.breadcrumb-left').should('be.visible', 'href');
// Challenge content
cy.get('.challenge-title').should('be.visible');
cy.get('#description').should('be.visible');
// Shoud be possible to submit solution
cy.get('input[name=solution]')
.click()
.type('https://codepen.io/foobar/full/RKRbwL');
cy.get('button[type=submit]').first().click();
});

122
generate-fixture-data.js Normal file
View File

@ -0,0 +1,122 @@
const { writeFileSync, mkdirSync } = require('fs');
const getChallenge = require('./curriculum/getChallenges');
const { challengeTypes } = require('./client/utils/challengeTypes');
const path = require('path');
function getCurriculum() {
return getChallenge.getChallengesForLang('english');
}
function initCurriculum() {
const superblocks = [
'apis-and-microservices',
'data-visualization',
'front-end-libraries',
'javascript-algorithms-and-data-structures',
'responsive-web-design'
];
const init = getCurriculum();
init.then(curriculum => {
superblocks.forEach(superblock => {
console.log(`creating pathdata for ${superblock} now`);
const blocks = Object.keys(curriculum[superblock]['blocks']);
createPaths(curriculum, superblock, blocks);
});
});
}
function createDirs() {
mkdirSync(path.join(__dirname, '/cypress/fixtures/path-data'));
mkdirSync(path.join(__dirname, '/cypress/fixtures/path-data/challenges'));
mkdirSync(
path.join(
__dirname,
'/cypress/fixtures/path-data/projects-and-back-challenges'
)
);
}
createDirs();
initCurriculum();
function createPaths(curriculum, superblock, blocks) {
let challengeObj = { blocks: {} };
let challengeObj2 = { blocks: {} };
let challengePaths;
// Specifies which challenge type has an editor
const typeHasEditor = [
challengeTypes.html,
challengeTypes.js,
challengeTypes.bonfire,
challengeTypes.modern
];
blocks.forEach(block => {
const challengeArr = curriculum[superblock]['blocks'][block]['challenges'];
challengePaths = challengeArr.map(challengePath => [
`/learn/${superblock}/${block}/${challengePath['dashedName']}`,
challengePath['challengeType']
]);
// Make variables defined before accessing them when checking for challenge type
challengeObj['blocks'][block] = {};
challengeObj2['blocks'][block] = {};
challengePaths.forEach(challengePath => {
const challengeName = challengePath[0].split('/');
if (typeHasEditor.includes(challengePath[1])) {
challengeObj['blocks'][block][challengeName[challengeName.length - 1]] =
challengePath[0];
} else {
challengeObj2['blocks'][block][
challengeName[challengeName.length - 1]
] = challengePath[0];
}
});
});
// Remove the objects if they are empty
function cleanEmptyObjects(obj) {
const getSize = function (obj) {
let size = 0;
for (let key in obj) {
if (obj.hasOwnProperty(key)) size++;
}
return size;
};
for (let block in obj['blocks']) {
if (getSize(obj['blocks'][block]) === 0) {
delete obj['blocks'][block];
}
}
return JSON.stringify(obj, null, 4);
}
writeFileSync(
path.join(
__dirname,
`/cypress/fixtures/path-data/challenges/${superblock}.json`
),
cleanEmptyObjects(challengeObj)
);
writeFileSync(
path.join(
__dirname,
`/cypress/fixtures/path-data/projects-and-back-challenges/${superblock}.json`
),
cleanEmptyObjects(challengeObj2)
);
}

91
generate-spec-files.js Normal file
View File

@ -0,0 +1,91 @@
const { readdirSync, readFileSync, writeFileSync } = require('fs');
const path = require('path');
console.log('Creating challenge specfiles...');
function createSpecFiles() {
// Get blocks in directory
const challengesFiles = readdirSync(
path.join(__dirname, '/cypress/fixtures/path-data/challenges')
);
const projectsFiles = readdirSync(
path.join(
__dirname,
'/cypress/fixtures/path-data/projects-and-back-challenges'
)
);
const blockExist = readdirSync(
path.join(__dirname, '/cypress/integration/challenge-tests/blocks')
);
// Split the extensions
let blockInDir = [];
blockExist.forEach(block => {
blockInDir.push(block.split('.')[0]);
});
function divider(files, project) {
files.forEach(file => {
let files = JSON.parse(
readFileSync(
path.join(
__dirname,
`/cypress/fixtures/path-data/${
project ? 'projects-and-back-challenges' : 'challenges'
}/${file}`
),
'utf-8'
)
);
let challengeBlocks = Object.keys(files['blocks']);
challengeBlocks.forEach(block => {
if (!blockInDir.includes(block)) {
writeFileSync(
path.join(
__dirname,
`/cypress/integration/challenge-tests/blocks/${block}.js`
),
`/* global cy */
const superBlockPath = require('../../../fixtures/path-data/${
project ? 'projects-and-back-challenges' : 'challenges'
}/${file}');
const blocks = Object.entries(superBlockPath['blocks']['${block}'])
for(const [challengeName , challengePath] of blocks){
describe('loading challenge', () => {
before(() => {
cy.visit(challengePath)
})
it('Challenge ' + challengeName + ' should work correctly', () => {
${
project
? 'cy.testProjectsAndBackend(challengePath)'
: 'cy.testChallenges(challengePath)'
}
})
});
}
`
);
}
});
});
}
divider(challengesFiles, false);
divider(projectsFiles, true);
return null;
}
createSpecFiles();
console.log('specfiles generated!');

View File

@ -38,8 +38,11 @@
"clean:root-deps": "shx rm -rf node_modules",
"clean:server": "shx rm -rf ./api-server/lib",
"precypress": "node ./cypress-install.js",
"precypress:gen:test": "node ./generate-spec-files.js",
"precypress:gen:fixtures": "node ./generate-fixture-data.js",
"cypress": "cypress",
"cypress:dev:run": "npm run cypress -- run",
"cypress:dev:run": "npm run cypress -- run --spec \"cypress/integration/main-tests/**/*\"",
"cypress:dev:run:full": "npm run cypress -- run --spec \"cypress/integration/**/*\"",
"cypress:dev:watch": "npm run cypress -- open",
"cypress:install": "cypress install && echo 'for use with ./cypress-install.js'",
"cypress:install-build-tools": "sh ./cypress-install.sh",