diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 73006ca31..9f0a206c0 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -87,25 +87,39 @@ jobs: GIT_DIR: ${{ github.workspace }}/.git GIT_WORK_TREE: ${{ github.workspace }} with: + browser: chrome env: STUDIO_CONTAINER=,STUDIO_SSH= working-directory: ${{ github.workspace }}.cypress-workspace + - name: Open access to crash log + if: always() + run: | + sudo chmod ugo+rX /hab/svc/php-runtime/{var,var/logs} || true + sudo chmod ugo+r /hab/svc/php-runtime/var/logs/crash.log || true + + - name: Upload crash log + uses: actions/upload-artifact@v2 + if: always() + with: + name: crash-log + path: /hab/svc/php-runtime/var/logs/crash.log + - name: Upload supervisor log - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v2 if: always() with: name: supervisor-log path: /hab/sup/default/sup.log - name: Upload Cypress screenshots - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v2 if: failure() with: name: cypress-screenshots path: ${{ github.workspace }}.cypress-workspace/cypress/screenshots - name: Upload Cypress videos - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v2 if: always() with: name: cypress-videos diff --git a/.holo/sources/slate.toml b/.holo/sources/slate.toml index def93d5f8..56aedb742 100644 --- a/.holo/sources/slate.toml +++ b/.holo/sources/slate.toml @@ -1,3 +1,3 @@ [holosource] url = "https://github.com/SlateFoundation/slate" -ref = "refs/tags/v2.16.8" +ref = "refs/tags/v2.16.12" diff --git a/cypress.json b/cypress.json index 15ff9514f..5f2cba867 100644 --- a/cypress.json +++ b/cypress.json @@ -3,5 +3,6 @@ "env": { "STUDIO_CONTAINER": "slate-cbl-studio", "TEST_USER": "teacher" - } + }, + "redirectionLimit":35 } diff --git a/cypress/fixtures/precise-rounding.json b/cypress/fixtures/precise-rounding.json new file mode 100644 index 000000000..653335457 --- /dev/null +++ b/cypress/fixtures/precise-rounding.json @@ -0,0 +1,255 @@ +{ + "student": { + "ELA": { + "1": { + "baseline": "10", + "test_doc_growth": "-0.7", + "growth": "-1.5", + "progress": "33", + "performanceLevel": "9.3" + }, + + "2": { + "baseline": "10", + "growth": "1", + "test_doc_growth": "0.8", + "progress": "27", + "performanceLevel": "10.8" + }, + + "6": { + "baseline": "9.5", + "test_doc_growth": "0.5", + "growth": "1.5", + "progress": "67", + "performanceLevel": "10" + } + } + }, + "student2": { + "ELA": { + "2": { + "baseline": "9", + "test_doc_growth": null, + "growth": "0", + "progress": "0", + "performanceLevel": null + }, + + "3": { + "baseline": "9", + "growth": "0", + "progress": "33", + "performanceLevel": "9" + } + } + }, + "student3": { + "SCI": { + "1": { + "baseline": "9.1", + "test_doc_growth": "0.3", + "growth": "0.4", + "progress": "38", + "performanceLevel": "9.4" + }, + "2": { + "baseline": "9.3", + "growth": "-1.3", + "progress": "25", + "performanceLevel": "8" + }, + "3": { + "baseline": "9.7", + "test_doc_growth": "-2.7", + "growth": "0", + "progress": "33", + "performanceLevel": "7" + }, + "4": { + "competency": "4", + "baseline": "9.7", + "growth": "0.3", + "progress": "67", + "performanceLevel": "10" + } + }, + "HOS": { + "4": { + "test_doc_baseline": null, + "baseline": "9", + "test_doc_growth": "0.3", + "growth": "0", + "progress": "100", + "test_doc_progress": "0", + "performanceLevel": "9.3", + "test_doc_performanceLevel": null + } + } + }, + "student4": { + "ELA": { + "1": { + "test_doc_baseline": "6", + "baseline": null, + "growth": "0", + "progress": "33", + "performanceLevel": "6" + }, + + "2": { + "baseline": null, + "test_doc_growth": null, + "growth": "0", + "progress": "40", + "performanceLevel": "6" + }, + + "3": { + "test_doc_baseline": "6", + "baseline": null, + "growth": "1", + "progress": "100", + "performanceLevel": "7" + }, + + "4": { + "test_doc_baseline": "6", + "baseline": null, + "test_doc_growth": "0.7", + "growth": "1", + "progress": "100", + "performanceLevel": "6.7" + }, + + "5": { + "test_doc_baseline": "6", + "baseline": null, + "growth": "1", + "progress": "83", + "performanceLevel": "7" + }, + "6": { + "baseline": null, + "test_doc_growth": null, + "growth": "0", + "progress": "33", + "performanceLevel": "7" + }, + "7": { + "test_doc_baseline": "6", + "baseline": null, + "test_doc_growth": "0.4", + "growth": "0", + "progress": "63", + "performanceLevel": "6.4" + } + }, + "SCI": { + "1": { + "test_doc_baseline": "6.9", + "baseline": null, + "test_doc_growth": "-0.3", + "growth": "0", + "progress": "38", + "performanceLevel": "6.6" + }, + "3": { + "baseline": null, + "test_doc_growth": null, + "growth": "0", + "progress": "0", + "performanceLevel": null + } + }, + "NGE": { + "1": { + "baseline": null, + "test_doc_growth": null, + "growth": "-1.3", + "progress": "35", + "performanceLevel": "9.4" + } + }, + "HOS": { + "1": { + "test_doc_baseline": "8.3", + "baseline": null, + "test_doc_growth": "0.8", + "growth": "0", + "progress": "45", + "performanceLevel": "9.1" + } + }, + "HW": { + "1": { + "test_doc_baseline": "8", + "baseline": null, + "growth": "0", + "progress": "33", + "performanceLevel": "8" + }, + "2": { + "baseline": null, + "test_doc_growth": null, + "growth": "0", + "progress": "42", + "performanceLevel": "8.4" + }, + "3": { + "test_doc_baseline": "8", + "baseline": null, + "test_doc_growth": "1.2", + "growth": "2.5", + "progress": "56", + "performanceLevel": "9.2" + } + } + }, + "student5": { + "ELA": { + "1": { + "baseline": "5", + "growth": "1.5", + "progress": "33", + "performanceLevel": "6.5" + }, + "2": { + "baseline": "5", + "growth": "2", + "progress": "40", + "performanceLevel": "7" + }, + "3": { + "baseline": "5", + "growth": "3.2", + "progress": "100", + "performanceLevel": "8.2" + }, + "4": { + "baseline": "5", + "growth": "2.5", + "progress": "100", + "performanceLevel": "7.5" + }, + "5": { + "baseline": "5", + "growth": "2", + "progress": "83", + "performanceLevel": "7" + }, + "6": { + "baseline": "5", + "growth": "3", + "progress": "33", + "performanceLevel": "8" + }, + "7": { + "baseline": "5", + "growth": "1.3", + "progress": "63", + "performanceLevel": "6.2" + } + } + } +} \ No newline at end of file diff --git a/cypress/integration/api/student-competencies.js b/cypress/integration/api/student-competencies.js index de5e65e14..1319ee738 100644 --- a/cypress/integration/api/student-competencies.js +++ b/cypress/integration/api/student-competencies.js @@ -41,7 +41,7 @@ describe('/cbl/student-competencies API', () => { expect(response.body.data).to.include({ ID: 1, Class: 'Slate\\CBL\\StudentCompetency', - Created: 1546401845, + Created: 1546398245, CreatorID: 2, StudentID: 4, CompetencyID: 1, @@ -70,7 +70,7 @@ describe('/cbl/student-competencies API', () => { expect(response.body.data.effectiveDemonstrationsData['1'][0]).to.include({ ID: 1, Class: 'Slate\\CBL\\Demonstrations\\DemonstrationSkill', - Created: 1546401845, + Created: 1546398245, CreatorID: 3, Modified: null, ModifierID: null, @@ -79,7 +79,7 @@ describe('/cbl/student-competencies API', () => { TargetLevel: 9, DemonstratedLevel: 9, Override: false, - DemonstrationDate: 1546304460 + DemonstrationDate: 1546300860 }); }); }); diff --git a/cypress/integration/precise-rounding.js b/cypress/integration/precise-rounding.js new file mode 100644 index 000000000..8169d5730 --- /dev/null +++ b/cypress/integration/precise-rounding.js @@ -0,0 +1,218 @@ +const csvtojson = require('csvtojson'); +const testCases = require('../fixtures/precise-rounding.json') + +describe('Confirm rounding is consistent across UI, API, and exports', () => { + before(() => { + cy.resetDatabase(); + }); + + it('Check API Data Against Test Case', () => { + cy.loginAs('teacher'); + cy.server().route('GET', '/cbl/student-competencies*').as('studentCompetencyData'); + cy.visit(`/cbl/dashboards/demonstrations/student`).then(()=>{ + const studentUsernames = Object.keys(testCases); + studentUsernames.forEach(studentUsername =>{ + const studentContentAreas = Object.keys(testCases[studentUsername]) + studentContentAreas.forEach(studentContentArea => { + cy.visit(`/cbl/dashboards/demonstrations/student#${studentUsername}/${studentContentArea}`); + + // ensure that API has loaded required data + cy.wait('@studentCompetencyData') + .should(({ xhr }) => { + const studentCompetencySuffixes = Object.keys(testCases[studentUsername][studentContentArea]); + studentCompetencySuffixes.forEach(studentCompetencySuffix => { + const { data, ContentArea: { Competencies: competencies } } = JSON.parse(xhr.response) + const { ID: competencyId } = competencies.filter(datum => datum.Code === `${studentContentArea}.${studentCompetencySuffix}`).pop(); + const studentData = data.filter(datum => datum.CompetencyID === competencyId) // filter by CompetencyID + .sort((sc1, sc2) => sc1.Level - sc2.Level).pop(); // sort by highest level last, and use that + expect(studentData).to.not.be.null; + const apiBaseLine = studentData.BaselineRating ? Math.round(studentData.BaselineRating * 10) / 10 : studentData.BaselineRating + const apiGrowth = studentData.growth + const apiProgress = studentData.demonstrationsRequired ? ( + studentData.demonstrationsComplete ? + Math.round(studentData.demonstrationsComplete / studentData.demonstrationsRequired * 100) : + 0 + ) : 1; + const apiPerformanceLevel = studentData.demonstrationsAverage + cy.wrap(xhr).its('status').should('eq', 200); + + //convert api baseline to string, if it is not null + expect(apiBaseLine ? `${apiBaseLine}` : apiBaseLine, + `${studentContentArea}.${studentCompetencySuffix} for ${studentUsername} API Baseline Value ${apiBaseLine}: Fixtures data Baseline Value ${testCases[studentUsername][studentContentArea][studentCompetencySuffix].baseline}` + ).to.equal(testCases[studentUsername][studentContentArea][studentCompetencySuffix].baseline); + + // convert api growth to string + expect(`${apiGrowth}`, + `${studentContentArea}.${studentCompetencySuffix} for ${studentUsername} API Growth Value ${apiGrowth}: Fixtures data Growth Value ${testCases[studentUsername][studentContentArea][studentCompetencySuffix].growth}` + ).to.equal(testCases[studentUsername][studentContentArea][studentCompetencySuffix].growth); + + // convert api calculated progress into string + expect(`${apiProgress}`, + `${studentContentArea}.${studentCompetencySuffix} for ${studentUsername} API Completion Percentage Value ${apiProgress}: Fixtures data Completion Percentage Value ${testCases[studentUsername][studentContentArea][studentCompetencySuffix].progress}` + ).to.equal(testCases[studentUsername][studentContentArea][studentCompetencySuffix].progress); + + // compare null comparisons without converting to string + expect(apiPerformanceLevel === null ? apiPerformanceLevel : `${apiPerformanceLevel}`, + `${studentContentArea}.${studentCompetencySuffix} for ${studentUsername} API Performance Level Value ${apiPerformanceLevel}: Fixtures data Perfomance Level Value ${testCases[studentUsername][studentContentArea][studentCompetencySuffix].performanceLevel}` + ).to.equal(testCases[studentUsername][studentContentArea][studentCompetencySuffix].performanceLevel); + }) + }) + }) + }) + }) + }) + + + it('Check UI Data Against Test Case', () => { + cy.loginAs('teacher'); + cy.server().route('GET', '/cbl/student-competencies*').as('studentCompetencyData'); + cy.visit(`/cbl/dashboards/demonstrations/student`).then(()=>{ + const studentUsernames = Object.keys(testCases); + studentUsernames.forEach(studentUsername =>{ + const studentContentAreas = Object.keys(testCases[studentUsername]) + studentContentAreas.forEach(studentContentArea => { + cy.visit(`/cbl/dashboards/demonstrations/student#${studentUsername}/${studentContentArea}`); + cy.wait('@studentCompetencyData') + .then(() => { + cy.wait(500); // wait for dom to render + + // ensure competency card elements have rendered + const studentCompetencySuffixes = Object.keys(testCases[studentUsername][studentContentArea]); + studentCompetencySuffixes.forEach(studentCompetencySuffix => { + cy.get('li.slate-demonstrations-student-competencycard') + .then(() => { + cy.withExt().then(({extQuerySelector}) => { + const card = extQuerySelector(`slate-demonstrations-student-competencycard{getCompetency().get("Code")=="${studentContentArea}.${studentCompetencySuffix}"}`); + const baseline = testCases[studentUsername][studentContentArea][studentCompetencySuffix].baseline + const growth = testCases[studentUsername][studentContentArea][studentCompetencySuffix].growth + const progress = testCases[studentUsername][studentContentArea][studentCompetencySuffix].progress + const performanceLevel = testCases[studentUsername][studentContentArea][studentCompetencySuffix].performanceLevel + checkUIDataAgainstTestCase(`${studentContentArea}.${studentCompetencySuffix}`, card.id, { + baseline, + growth, + progress, + performanceLevel + }); + }); + }); + }) + }); + + }) + }) + }) + + const checkUIDataAgainstTestCase = (code, competencyCardId, { baseline, growth, progress, performanceLevel }) => { + + // check baseline rating calculation + cy.get(`#${competencyCardId}`) + .find('span[data-ref="codeEl"]') + .contains(code); + + if (baseline !== undefined) { + cy.get(`#${competencyCardId}`) + .find('td[data-ref="baselineRatingEl"]') + .contains(baseline === null ? '—' : baseline); + }; + + if (growth !== undefined) { + cy.get(`#${competencyCardId}`) + .find('td[data-ref="growthEl"]') + .contains(growth === null ? '—' : `${growth} yr`); + } + + if (progress !== undefined) { + cy.get(`#${competencyCardId}`) + .find('div[data-ref="meterPercentEl"]') + .contains(progress === null ? '—' : progress); + }; + + if (performanceLevel !== undefined) { + cy.get(`#${competencyCardId}`) + .find('td[data-ref="averageEl"]') + .contains(performanceLevel === null ? '—' : performanceLevel); + }; + }; + }) + + + it('Check CSV Data Against Test Case', () => { + cy.loginAs('admin'); + cy.visit('/exports'); + + // prepare for form submission that returns back a file + // https://on.cypress.io/intercept + cy.intercept({ pathname: '/exports/slate-cbl/student-competencies'}, (req) => { + req.redirect('/exports') + }).as('records'); + + const studentUsernames = Object.keys(testCases); + studentUsernames.forEach(studentUsername =>{ + const studentContentAreas = Object.keys(testCases[studentUsername]) + studentContentAreas.forEach(studentContentArea => { + const studentCompetencySuffixes = Object.keys(testCases[studentUsername][studentContentArea]); + studentCompetencySuffixes.forEach(studentCompetencySuffix => { + cy.get('form[action="/exports/slate-cbl/student-competencies"]').within(() => { + cy.get('input[name=students]').clear().type(`${studentUsername}`); + cy.get('select[name=content_area]').select(studentContentArea); + cy.get('select[name=level]').select('highest'); + cy.root().submit(); + }); + cy.wait('@records').its('request').then((req) => { + cy.request(req) + .then(({ body, headers }) => { + expect(headers).to.have.property('content-type', 'text/csv; charset=utf-8') + return csvtojson().fromString(body) + }).then((records) => { + const studentCompetencyRow = records.filter((record)=> { + return record.Competency === `${studentContentArea}.${studentCompetencySuffix}` + }).pop(); + const csvPerformanceLevel = studentCompetencyRow['Performance Level'] + const csvGrowth = studentCompetencyRow.Growth + const csvBaseLine = studentCompetencyRow.Baseline + const csvProgress = studentCompetencyRow.Progress + const baseline = testCases[studentUsername][studentContentArea][studentCompetencySuffix].baseline + const growth = testCases[studentUsername][studentContentArea][studentCompetencySuffix].growth + const progress = testCases[studentUsername][studentContentArea][studentCompetencySuffix].progress + const performanceLevel = testCases[studentUsername][studentContentArea][studentCompetencySuffix].performanceLevel + + // csv represents null as empty string + expect(csvPerformanceLevel === '' ? null : csvPerformanceLevel, + `${studentContentArea}.${studentCompetencySuffix} for ${studentUsername} CSV Performance Level Value ${csvPerformanceLevel}: Fixtures data Perfomance Level Value ${performanceLevel}` + ).to.equal(performanceLevel); + + // csv represents 0 growth as an empty string + expect(`${csvGrowth === '' ? 0 : csvGrowth}`, + `${studentContentArea}.${studentCompetencySuffix} for ${studentUsername} CSV Growth Value ${csvGrowth}: Fixtures data Growth Value ${growth}` + ).to.equal(growth); + + // if csv value = 0, baseline could = NULL OR 0. + // this needs to be resolved -- the CSV should probably differentiate + if (csvBaseLine === '0') { + expect(baseline, + `${studentContentArea}.${studentCompetencySuffix} for ${studentUsername} CSV Baseline Value ${csvBaseLine}: Fixtures data Baseline Value ${baseline}` + ).to.be.oneOf(['0', null]) + } else { + expect(`${csvBaseLine}`, + `${studentContentArea}.${studentCompetencySuffix} for ${studentUsername} CSV Baseline Value ${csvBaseLine}: Fixtures data Baseline Value ${baseline}` + ).to.equal(baseline); + } + + // csv represents 0 progress as empty string + if (csvProgress === '') { + expect(progress, + `${studentContentArea}.${studentCompetencySuffix} for ${studentUsername} CSV Completion Percentage Value ${csvProgress}: Fixtures data Completion Percentage Value ${progress}` + ).to.equal('0'); // progress is represented as decimal in export + } else { + expect(`${csvProgress}`, + `${studentContentArea}.${studentCompetencySuffix} for ${studentUsername} CSV Completion Percentage Value ${csvProgress}: Fixtures data Completion Percentage Value ${progress}` + ).to.equal(`${progress/100}`); // progress is represented as decimal in export + } + + }) + }) + }) + }) + }) + }) +}) \ No newline at end of file