feat(test, e2e) test suit for cypress (#42138)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@ -1,8 +1,5 @@
|
|||||||
name: Cypress
|
name: Cypress - Pull-request
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches-ignore:
|
|
||||||
- 'renovate/**'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
91
.github/workflows/cypress-push.yml
vendored
Normal file
91
.github/workflows/cypress-push.yml
vendored
Normal 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
5
.gitignore
vendored
@ -116,6 +116,10 @@ coverage
|
|||||||
cypress/videos
|
cypress/videos
|
||||||
cypress/screenshots
|
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 intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
.grunt
|
.grunt
|
||||||
|
|
||||||
@ -179,3 +183,4 @@ api-server/lib/*
|
|||||||
curriculum/dist
|
curriculum/dist
|
||||||
curriculum/build
|
curriculum/build
|
||||||
client/static/_redirects
|
client/static/_redirects
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"projectId": "ke77ns",
|
"projectId": "ke77ns",
|
||||||
"baseUrl": "http://localhost:8000",
|
"baseUrl": "http://localhost:8000",
|
||||||
"retries": 4
|
"retries": 4,
|
||||||
|
"videoUploadOnPasses": false
|
||||||
}
|
}
|
||||||
|
0
cypress/integration/challenge-tests/blocks/.gitkeep
Normal file
0
cypress/integration/challenge-tests/blocks/.gitkeep
Normal 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 */
|
/* eslint-disable no-unused-vars */
|
||||||
module.exports = (on, config) => {
|
|
||||||
// `on` is used to hook into various events Cypress emits
|
module.exports = function (on, config) {
|
||||||
// `config` is the resolved Cypress config
|
// configure plugins here
|
||||||
};
|
};
|
||||||
|
@ -1,37 +1,4 @@
|
|||||||
/* global cy Cypress*/
|
/* 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', () => {
|
Cypress.Commands.add('login', () => {
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
@ -52,3 +19,50 @@ Cypress.Commands.add('resetUsername', () => {
|
|||||||
|
|
||||||
cy.contains('Account Settings for developmentuser').should('be.visible');
|
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
122
generate-fixture-data.js
Normal 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
91
generate-spec-files.js
Normal 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!');
|
@ -38,8 +38,11 @@
|
|||||||
"clean:root-deps": "shx rm -rf node_modules",
|
"clean:root-deps": "shx rm -rf node_modules",
|
||||||
"clean:server": "shx rm -rf ./api-server/lib",
|
"clean:server": "shx rm -rf ./api-server/lib",
|
||||||
"precypress": "node ./cypress-install.js",
|
"precypress": "node ./cypress-install.js",
|
||||||
|
"precypress:gen:test": "node ./generate-spec-files.js",
|
||||||
|
"precypress:gen:fixtures": "node ./generate-fixture-data.js",
|
||||||
"cypress": "cypress",
|
"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:dev:watch": "npm run cypress -- open",
|
||||||
"cypress:install": "cypress install && echo 'for use with ./cypress-install.js'",
|
"cypress:install": "cypress install && echo 'for use with ./cypress-install.js'",
|
||||||
"cypress:install-build-tools": "sh ./cypress-install.sh",
|
"cypress:install-build-tools": "sh ./cypress-install.sh",
|
||||||
|
Reference in New Issue
Block a user