Initial unit tests for front-end framework (#4576)

Credit to @bennothommo
This commit is contained in:
Ben Thomson 2019-09-26 00:23:17 +08:00 committed by Luke Towers
parent 815ec1a174
commit 5f15ed54f9
10 changed files with 808 additions and 3 deletions

13
.babelrc Normal file
View File

@ -0,0 +1,13 @@
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"module-resolver", {
"root": ["."],
"alias": {
"helpers": "./tests/js/helpers"
}
}
]
]
}

24
.github/workflows/frontend-tests.yaml vendored Normal file
View File

@ -0,0 +1,24 @@
name: Tests
on:
push:
branches:
- master
- develop
pull_request:
jobs:
frontendTests:
runs-on: ubuntu-latest
name: JavaScript
steps:
- name: Checkout changes
uses: actions/checkout@v1
- name: Install Node
uses: actions/setup-node@v1
with:
node-version: 8
- name: Install Node dependencies
run: npm install
- name: Run tests
run: npm run test

2
.gitignore vendored
View File

@ -16,6 +16,8 @@ sftp-config.json
.ftpconfig
selenium.php
composer.lock
package-lock.json
/node_modules
_ide_helper.php
# for netbeans

5
.jshintrc Normal file
View File

@ -0,0 +1,5 @@
{
"esversion": 6,
"curly": true,
"asi": true
}

View File

@ -66,7 +66,7 @@ var fieldElement=$form.find('[name="'+fieldName+'"], [name="'+fieldName+'[]"], [
if(fieldElement.length>0){var _event=jQuery.Event('ajaxInvalidField')
$(window).trigger(_event,[fieldElement.get(0),fieldName,fieldMessages,isFirstInvalidField])
if(isFirstInvalidField){if(!_event.isDefaultPrevented())fieldElement.focus()
isFirstInvalidField=false}}})},handleFlashMessage:function(message,type){},handleRedirectResponse:function(url){window.location.href=url},handleUpdateResponse:function(data,textStatus,jqXHR){var updatePromise=$.Deferred().done(function(){for(var partial in data){var selector=(options.update[partial])?options.update[partial]:partial
isFirstInvalidField=false}}})},handleFlashMessage:function(message,type){},handleRedirectResponse:function(url){window.location.assign(url)},handleUpdateResponse:function(data,textStatus,jqXHR){var updatePromise=$.Deferred().done(function(){for(var partial in data){var selector=(options.update[partial])?options.update[partial]:partial
if($.type(selector)=='string'&&selector.charAt(0)=='@'){$(selector.substring(1)).append(data[partial]).trigger('ajaxUpdate',[context,data,textStatus,jqXHR])}
else if($.type(selector)=='string'&&selector.charAt(0)=='^'){$(selector.substring(1)).prepend(data[partial]).trigger('ajaxUpdate',[context,data,textStatus,jqXHR])}
else{$(selector).trigger('ajaxBeforeReplace')

View File

@ -66,7 +66,7 @@ var fieldElement=$form.find('[name="'+fieldName+'"], [name="'+fieldName+'[]"], [
if(fieldElement.length>0){var _event=jQuery.Event('ajaxInvalidField')
$(window).trigger(_event,[fieldElement.get(0),fieldName,fieldMessages,isFirstInvalidField])
if(isFirstInvalidField){if(!_event.isDefaultPrevented())fieldElement.focus()
isFirstInvalidField=false}}})},handleFlashMessage:function(message,type){},handleRedirectResponse:function(url){window.location.href=url},handleUpdateResponse:function(data,textStatus,jqXHR){var updatePromise=$.Deferred().done(function(){for(var partial in data){var selector=(options.update[partial])?options.update[partial]:partial
isFirstInvalidField=false}}})},handleFlashMessage:function(message,type){},handleRedirectResponse:function(url){window.location.assign(url)},handleUpdateResponse:function(data,textStatus,jqXHR){var updatePromise=$.Deferred().done(function(){for(var partial in data){var selector=(options.update[partial])?options.update[partial]:partial
if($.type(selector)=='string'&&selector.charAt(0)=='@'){$(selector.substring(1)).append(data[partial]).trigger('ajaxUpdate',[context,data,textStatus,jqXHR])}
else if($.type(selector)=='string'&&selector.charAt(0)=='^'){$(selector.substring(1)).prepend(data[partial]).trigger('ajaxUpdate',[context,data,textStatus,jqXHR])}
else{$(selector).trigger('ajaxBeforeReplace')

View File

@ -267,7 +267,7 @@ if (window.jQuery.request !== undefined) {
* Custom function, redirect the browser to another location
*/
handleRedirectResponse: function(url) {
window.location.href = url
window.location.assign(url)
},
/*

48
package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "octobercms",
"description": "Free, open-source, self-hosted CMS platform based on the Laravel PHP Framework.",
"directories": {
"test": "tests/js/cases",
"helpers": "tests/js/helpers"
},
"scripts": {
"test": "mocha --require @babel/register tests/js/cases/**/*.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/octobercms/october.git"
},
"contributors": [
{
"name": "Alexey Bobkov",
"email": "aleksey.bobkov@gmail.com"
},
{
"name": "Samuel Georges",
"email": "daftspunky@gmail.com"
},
{
"name": "Luke Towers",
"email": "octobercms@luketowers.ca",
"url": "https://luketowers.ca"
}
],
"license": "MIT",
"bugs": {
"url": "https://github.com/octobercms/october/issues"
},
"homepage": "https://octobercms.com/",
"devDependencies": {
"@babel/cli": "^7.5.5",
"@babel/core": "^7.5.5",
"@babel/node": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@babel/register": "^7.5.5",
"babel-plugin-module-resolver": "^3.2.0",
"chai": "^4.2.0",
"jquery": "^3.4.1",
"jsdom": "^15.1.1",
"mocha": "^6.2.0",
"sinon": "^7.4.1"
}
}

View File

@ -0,0 +1,673 @@
import { assert } from 'chai'
import fakeDom from 'helpers/fakeDom'
import sinon from 'sinon'
describe('modules/system/assets/js/framework.js', function () {
describe('ajaxRequests through JS', function () {
let dom,
window,
xhr,
requests = []
this.timeout(1000)
beforeEach(() => {
// Load framework.js in the fake DOM
dom = fakeDom(
'<div id="partialId" class="partialClass">Initial content</div>' +
'<script src="file://./node_modules/jquery/dist/jquery.js" id="jqueryScript"></script>' +
'<script src="file://./modules/system/assets/js/framework.js" id="frameworkScript"></script>',
{
beforeParse: (window) => {
// Mock XHR for tests below
xhr = sinon.useFakeXMLHttpRequest()
xhr.onCreate = (request) => {
requests.push(request)
}
window.XMLHttpRequest = xhr
// Allow window.location.assign() to be stubbed
delete window.location
window.location = {
href: 'https://october.example.org/',
assign: sinon.stub()
}
}
}
)
window = dom.window
// Enable CORS on jQuery
window.jqueryScript.onload = () => {
window.jQuery.support.cors = true
}
})
afterEach(() => {
// Close window and restore XHR functionality to default
window.XMLHttpRequest = sinon.xhr.XMLHttpRequest
window.close()
requests = []
})
it('can make a successful AJAX request', function (done) {
window.frameworkScript.onload = () => {
window.$.request('test::onTest', {
success: function () {
done()
},
error: function () {
done(new Error('AJAX call failed'))
}
})
try {
assert(
requests[1].requestHeaders['X-OCTOBER-REQUEST-HANDLER'] === 'test::onTest',
'Incorrect October request handler'
)
} catch (e) {
done(e)
}
// Mock a successful response from the server
requests[1].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
'successful': true
})
)
}
})
it('can make an unsuccessful AJAX request', function (done) {
window.frameworkScript.onload = () => {
window.$.request('test::onTest', {
success: function () {
done(new Error('AJAX call succeeded'))
},
error: function () {
done()
}
})
try {
assert(
requests[1].requestHeaders['X-OCTOBER-REQUEST-HANDLER'] === 'test::onTest',
'Incorrect October request handler'
)
} catch (e) {
done(e)
}
// Mock a 404 Not Found response from the server
requests[1].respond(
404,
{
'Content-Type': 'text/html'
},
''
)
}
})
it('can update a partial via an ID selector', function (done) {
window.frameworkScript.onload = () => {
window.$.request('test::onTest', {
complete: function () {
let partialContent = dom.window.document.getElementById('partialId').textContent
try {
assert(
partialContent === 'Content passed through AJAX',
'Partial content incorrect - ' +
'expected "Content passed through AJAX", ' +
'found "' + partialContent + '"'
)
done()
} catch (e) {
done(e)
}
}
})
try {
assert(
requests[1].requestHeaders['X-OCTOBER-REQUEST-HANDLER'] === 'test::onTest',
'Incorrect October request handler'
)
} catch (e) {
done(e)
}
// Mock a response from the server that includes a partial change via ID
requests[1].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
'#partialId': 'Content passed through AJAX'
})
)
}
})
it('can update a partial via a class selector', function (done) {
window.frameworkScript.onload = () => {
window.$.request('test::onTest', {
complete: function () {
let partialContent = dom.window.document.getElementById('partialId').textContent
try {
assert(
partialContent === 'Content passed through AJAX',
'Partial content incorrect - ' +
'expected "Content passed through AJAX", ' +
'found "' + partialContent + '"'
)
done()
} catch (e) {
done(e)
}
}
})
try {
assert(
requests[1].requestHeaders['X-OCTOBER-REQUEST-HANDLER'] === 'test::onTest',
'Incorrect October request handler'
)
} catch (e) {
done(e)
}
// Mock a response from the server that includes a partial change via a class
requests[1].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
'.partialClass': 'Content passed through AJAX'
})
)
}
})
it('can redirect after a successful AJAX request', function (done) {
this.timeout(1000)
// Detect a redirect
window.location.assign.callsFake((url) => {
try {
assert(
url === '/test/success',
'Non-matching redirect URL'
)
done()
} catch (e) {
done(e)
}
})
window.frameworkScript.onload = () => {
window.$.request('test::onTest', {
redirect: '/test/success',
})
try {
assert(
requests[1].requestHeaders['X-OCTOBER-REQUEST-HANDLER'] === 'test::onTest',
'Incorrect October request handler'
)
} catch (e) {
done(e)
}
// Mock a successful response from the server
requests[1].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
'successful': true
})
)
}
})
it('can send extra data with the AJAX request', function (done) {
this.timeout(1000)
window.frameworkScript.onload = () => {
window.$.request('test::onTest', {
data: {
test1: 'First',
test2: 'Second'
},
success: function () {
done()
}
})
try {
assert(
requests[1].requestBody === 'test1=First&test2=Second',
'Data incorrect or not included in request'
)
assert(
requests[1].requestHeaders['X-OCTOBER-REQUEST-HANDLER'] === 'test::onTest',
'Incorrect October request handler'
)
} catch (e) {
done(e)
}
// Mock a successful response from the server
requests[1].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
'successful': true
})
)
}
})
it('can call a beforeUpdate handler', function (done) {
const beforeUpdate = function (data, status, jqXHR) {
}
const beforeUpdateSpy = sinon.spy(beforeUpdate)
window.frameworkScript.onload = () => {
window.$.request('test::onTest', {
beforeUpdate: beforeUpdateSpy
})
try {
assert(
requests[1].requestHeaders['X-OCTOBER-REQUEST-HANDLER'] === 'test::onTest',
'Incorrect October request handler'
)
} catch (e) {
done(e)
}
// Mock a successful response from the server
requests[1].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
'successful': true
})
)
try {
assert(
beforeUpdateSpy.withArgs(
{
'successful': true
},
'success'
).calledOnce
)
done()
} catch (e) {
done(e)
}
}
})
})
describe('ajaxRequests through HTML attributes', function () {
let dom,
window,
xhr,
requests = []
this.timeout(1000)
beforeEach(() => {
// Load framework.js in the fake DOM
dom = fakeDom(
'<a ' +
'id="standard" ' +
'href="javascript:;" ' +
'data-request="test::onTest" ' +
'data-request-success="test(\'success\')" ' +
'data-request-error="test(\'failure\')"' +
'></a>' +
'<a ' +
'id="redirect" ' +
'href="javascript:;" ' +
'data-request="test::onTest" ' +
'data-request-redirect="/test/success"' +
'></a>' +
'<a ' +
'id="dataLink" ' +
'href="javascript:;" ' +
'data-request="test::onTest" ' +
'data-request-data="test1: \'First\', test2: \'Second\'" ' +
'data-request-success="test(\'success\')" ' +
'data-request-before-update="beforeUpdateSpy($el.get(), data, textStatus)"' +
'></a>' +
'<div id="partialId" class="partialClass">Initial content</div>' +
'<script src="file://./node_modules/jquery/dist/jquery.js" id="jqueryScript"></script>' +
'<script src="file://./modules/system/assets/js/framework.js" id="frameworkScript"></script>',
{
beforeParse: (window) => {
// Mock XHR for tests below
xhr = sinon.useFakeXMLHttpRequest()
xhr.onCreate = (request) => {
requests.push(request)
}
window.XMLHttpRequest = xhr
// Add a stub for the request handlers
window.test = sinon.stub()
// Add a spy for the beforeUpdate handler
window.beforeUpdate = function (element, data, status) {
}
window.beforeUpdateSpy = sinon.spy(window.beforeUpdate)
// Stub out window.alert
window.alert = sinon.stub()
// Allow window.location.assign() to be stubbed
delete window.location
window.location = {
href: 'https://october.example.org/',
assign: sinon.stub()
}
}
}
)
window = dom.window
// Enable CORS on jQuery
window.jqueryScript.onload = () => {
window.jQuery.support.cors = true
}
})
afterEach(() => {
// Close window and restore XHR functionality to default
window.XMLHttpRequest = sinon.xhr.XMLHttpRequest
window.close()
requests = []
})
it('can make a successful AJAX request', function (done) {
window.frameworkScript.onload = () => {
window.test.callsFake((response) => {
assert(response === 'success', 'Response handler was not "success"')
done()
})
window.$('a#standard').click()
try {
assert(
requests[1].requestHeaders['X-OCTOBER-REQUEST-HANDLER'] === 'test::onTest',
'Incorrect October request handler'
)
} catch (e) {
done(e)
}
// Mock a successful response from the server
requests[1].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
'successful': true
})
)
}
})
it('can make an unsuccessful AJAX request', function (done) {
window.frameworkScript.onload = () => {
window.test.callsFake((response) => {
assert(response === 'failure', 'Response handler was not "failure"')
done()
})
window.$('a#standard').click()
try {
assert(
requests[1].requestHeaders['X-OCTOBER-REQUEST-HANDLER'] === 'test::onTest',
'Incorrect October request handler'
)
} catch (e) {
done(e)
}
// Mock a 404 Not Found response from the server
requests[1].respond(
404,
{
'Content-Type': 'text/html'
},
''
)
}
})
it('can update a partial via an ID selector', function (done) {
window.frameworkScript.onload = () => {
window.test.callsFake(() => {
let partialContent = dom.window.document.getElementById('partialId').textContent
try {
assert(
partialContent === 'Content passed through AJAX',
'Partial content incorrect - ' +
'expected "Content passed through AJAX", ' +
'found "' + partialContent + '"'
)
done()
} catch (e) {
done(e)
}
})
window.$('a#standard').click()
try {
assert(
requests[1].requestHeaders['X-OCTOBER-REQUEST-HANDLER'] === 'test::onTest',
'Incorrect October request handler'
)
} catch (e) {
done(e)
}
// Mock a response from the server that includes a partial change via ID
requests[1].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
'#partialId': 'Content passed through AJAX'
})
)
}
})
it('can update a partial via a class selector', function (done) {
window.frameworkScript.onload = () => {
window.test.callsFake(() => {
let partialContent = dom.window.document.getElementById('partialId').textContent
try {
assert(
partialContent === 'Content passed through AJAX',
'Partial content incorrect - ' +
'expected "Content passed through AJAX", ' +
'found "' + partialContent + '"'
)
done()
} catch (e) {
done(e)
}
})
window.$('a#standard').click()
try {
assert(
requests[1].requestHeaders['X-OCTOBER-REQUEST-HANDLER'] === 'test::onTest',
'Incorrect October request handler'
)
} catch (e) {
done(e)
}
// Mock a response from the server that includes a partial change via a class
requests[1].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
'.partialClass': 'Content passed through AJAX'
})
)
}
})
it('can redirect after a successful AJAX request', function (done) {
this.timeout(1000)
// Detect a redirect
window.location.assign.callsFake((url) => {
try {
assert(
url === '/test/success',
'Non-matching redirect URL'
)
done()
} catch (e) {
done(e)
}
})
window.frameworkScript.onload = () => {
window.$('a#redirect').click()
try {
assert(
requests[1].requestHeaders['X-OCTOBER-REQUEST-HANDLER'] === 'test::onTest',
'Incorrect October request handler'
)
} catch (e) {
done(e)
}
// Mock a successful response from the server
requests[1].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
'succesful': true
})
)
}
})
it('can send extra data with the AJAX request', function (done) {
this.timeout(1000)
window.frameworkScript.onload = () => {
window.test.callsFake((response) => {
assert(response === 'success', 'Response handler was not "success"')
done()
})
window.$('a#dataLink').click()
try {
assert(
requests[1].requestHeaders['X-OCTOBER-REQUEST-HANDLER'] === 'test::onTest',
'Incorrect October request handler'
)
} catch (e) {
done(e)
}
// Mock a successful response from the server
requests[1].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
'succesful': true
})
)
}
})
it('can call a beforeUpdate handler', function (done) {
this.timeout(1000)
window.frameworkScript.onload = () => {
window.test.callsFake((response) => {
assert(response === 'success', 'Response handler was not "success"')
})
window.$('a#dataLink').click()
try {
assert(
requests[1].requestHeaders['X-OCTOBER-REQUEST-HANDLER'] === 'test::onTest',
'Incorrect October request handler'
)
} catch (e) {
done(e)
}
// Mock a successful response from the server
requests[1].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
'successful': true
})
)
try {
assert(
window.beforeUpdateSpy.withArgs(
window.$('a#dataLink').get(),
{
'successful': true
},
'success'
).calledOnce,
'beforeUpdate handler never called, or incorrect arguments provided'
)
done()
} catch (e) {
done(e)
}
}
})
})
})

View File

@ -0,0 +1,40 @@
import { JSDOM } from 'jsdom'
const defaults = {
url: 'https://october.example.org/',
referer: null,
contentType: 'text/html',
head: '<!DOCTYPE html><html><head><title>Fake document</title></head>',
bodyStart: '<body>',
bodyEnd: '</body>',
foot: '</html>',
beforeParse: null
}
const fakeDom = (content, options) => {
const settings = Object.assign({}, defaults, options)
const dom = new JSDOM(
settings.head +
settings.bodyStart +
(content + '') +
settings.bodyEnd +
settings.foot,
{
url: settings.url,
referrer: settings.referer || undefined,
contentType: settings.contenType,
includeNodeLocations: true,
runScripts: 'dangerously',
resources: 'usable',
pretendToBeVisual: true,
beforeParse: (typeof settings.beforeParse === 'function')
? settings.beforeParse
: undefined
}
)
return dom
}
export default fakeDom