feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run

- Base: wintercms/winter branch 1.2 (full framework)
- Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS
- Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication
- Partials: hero (offline-first), features, modes (offline/nube toggle),
  screenshots, pricing (3 planes), comparison, FAQ, CTA
- Plugin VivesPOS.Site with ContactForm
- Dockerfile: PHP 8.2 Apache, port 80, healthcheck
- Added winter/wn-pages, blog, sitemap, seo plugins
- Active theme set to vivespos
This commit is contained in:
2026-08-21 19:29:00 -06:00
commit 1f72193a64
3266 changed files with 531480 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
import FakeDom from '../../helpers/FakeDom';
jest.setTimeout(2000);
describe('Original AJAX Framework library', function () {
it('can get data from the current form and a series of parent forms', function (done) {
FakeDom
.new()
.addScript([
'modules/backend/assets/js/vendor/jquery.min.js',
'modules/system/assets/js/framework.js',
])
.render(
'<form name="grandParentForm" id="grandParentForm" data-request-data="formOne: \'valueOne\'>' +
'<input name="fieldOne" value="valueOne" type="text">' +
'<input name="fieldFive" value="valueFive" type="text">' +
'<input name="multiField[]" value="multiOne" type="text">' +
'</form>' +
'<form name="parentForm" id="parentForm" data-request-parent="#grandParentForm" data-request-data="formTwo: \'valueTwo\'">' +
'<input name="fieldOne" value="overrideOne" type="text">' +
'<input name="fieldTwo" value="valueTwo" type="text">' +
'<input name="multiField[]" value="multiTwo" type="text">' +
'</form>' +
'<form name="childForm" id="childForm" data-request-parent="#parentForm" data-request="onTest" data-request-data="formThree: \'valueThree\'">' +
'<input name="fieldOne" value="overrideTwo" type="text">' +
'<input name="fieldThree" value="valueThree" type="text">' +
'<input name="fieldFour" value="valueFour" type="text">' +
'<input name="multiField[]" value="multiThree" type="text">' +
'<input name="multiFieldTwo[]" value="multiOne" type="text">' +
'<input name="multiFieldTwo[]" value="multiTwo" type="text">' +
'</form>'
)
.then(
(dom) => {
const parentDataSpy = jest.spyOn(dom.window.jQuery.fn, 'getRequestParentData');
jest.spyOn(dom.window, 'alert').mockImplementation(() => {});
jest.spyOn(dom.window.jQuery, 'ajax').mockImplementation(() => {
expect(parentDataSpy.mock.results[0].value).toMatchObject({
'formOne': 'valueOne',
'fieldOne': 'overrideTwo',
'fieldFive': 'valueFive',
'formTwo': 'valueTwo',
'fieldTwo': 'valueTwo',
'fieldThree': 'valueThree',
'fieldFour': 'valueFour',
'multiField[]': 'multiThree',
'multiFieldTwo[]': ['multiOne', 'multiTwo'],
});
done();
return dom.window.jQuery.Deferred();
});
dom.window.jQuery('#childForm').trigger('submit');
}
);
});
});

View File

@@ -0,0 +1,149 @@
import FakeDom from '../../helpers/FakeDom';
jest.setTimeout(5000);
describe('Form Widget dependsOn', function () {
/**
* Build a FakeDom with jQuery, WinterCMS foundation, stubs, and the form widget.
* The FormWidgetStubs fixture provides minimal implementations of ocJSON,
* $.fn.render, $.fn.request (synchronous success), and $.fn.loadIndicator
*/
function buildDom(html) {
return FakeDom
.new()
.addScript([
'modules/backend/assets/js/vendor/jquery.min.js',
'modules/system/assets/ui/js/foundation.baseclass.js',
'modules/system/assets/ui/js/foundation.controlutils.js',
'modules/system/tests/js/fixtures/formWidget/FormWidgetStubs.js',
'modules/backend/widgets/form/assets/js/winter.form.js',
])
.render(html);
}
/**
* Build form HTML with fields and their dependsOn declarations.
*
* Collects all field names (both keys and referenced dependencies) and creates
* a div for each. Fields that have dependencies get data-field-depends attributes.
*
* @param {Object} fields - Map of field names to arrays of field names they depend on.
* e.g. { a: ['b'], b: ['a'] } for circular deps.
*/
function buildFormHtml(fields) {
// Collect all unique field names (both dependents and their dependencies)
var allFields = {};
for (var name in fields) {
allFields[name] = fields[name];
fields[name].forEach(function (dep) {
if (!(dep in allFields)) {
allFields[dep] = null;
}
});
}
var fieldHtml = '';
for (var fieldName in allFields) {
fieldHtml += '<div data-field-name="' + fieldName + '"';
if (allFields[fieldName] !== null) {
fieldHtml += " data-field-depends='" + JSON.stringify(allFields[fieldName]) + "'";
}
fieldHtml += '><input type="text" name="' + fieldName + '"></div>';
}
return '<form>'
+ '<div data-control="formwidget" data-refresh-handler="onRefreshField">'
+ fieldHtml
+ '</div>'
+ '</form>';
}
it('refreshes dependent fields when a field changes', function (done) {
buildDom(buildFormHtml({ fieldB: ['fieldA'] }))
.then(function (dom) {
var $ = dom.window.jQuery;
var requestSpy = jest.spyOn($.fn, 'request');
$('[data-field-name="fieldA"]').trigger('change');
// The form widget debounces with a 300ms timer
setTimeout(function () {
try {
expect(requestSpy).toHaveBeenCalledTimes(1);
expect(requestSpy).toHaveBeenCalledWith(
'onRefreshField',
expect.objectContaining({
data: expect.objectContaining({ fields: ['fieldB'] })
})
);
done();
} catch (e) {
done(e);
}
}, 500);
});
});
it('prevents infinite loop with circular dependsOn declarations', function (done) {
buildDom(buildFormHtml({ fieldA: ['fieldB'], fieldB: ['fieldA'] }))
.then(function (dom) {
var $ = dom.window.jQuery;
var requestSpy = jest.spyOn($.fn, 'request');
$('[data-field-name="fieldA"]').trigger('change');
// With 300ms debounce per step, an unguarded circular loop would fire
// many times in 2 seconds. The fix should limit this to exactly 2
// requests: A refreshes B, B refreshes A (blocked by cascade chain).
setTimeout(function () {
try {
expect(requestSpy.mock.calls.length).toBe(2);
done();
} catch (e) {
done(e);
}
}, 2000);
});
});
it('allows transitive cascading (A -> B -> C) without blocking', function (done) {
buildDom(buildFormHtml({ fieldB: ['fieldA'], fieldC: ['fieldB'] }))
.then(function (dom) {
var $ = dom.window.jQuery;
var requestSpy = jest.spyOn($.fn, 'request');
$('[data-field-name="fieldA"]').trigger('change');
// A->B (300ms) then B->C (600ms). Allow time for both.
setTimeout(function () {
try {
expect(requestSpy.mock.calls.length).toBe(2);
done();
} catch (e) {
done(e);
}
}, 1500);
});
});
it('stops cycle in a three-field circular chain (A -> B -> C -> A)', function (done) {
buildDom(buildFormHtml({ fieldA: ['fieldC'], fieldB: ['fieldA'], fieldC: ['fieldB'] }))
.then(function (dom) {
var $ = dom.window.jQuery;
var requestSpy = jest.spyOn($.fn, 'request');
$('[data-field-name="fieldA"]').trigger('change');
// A->B (300ms), B->C (600ms), C tries to refresh A but A is already
// in the cascade chain [fieldA, fieldB] so it stops. Total: 3 requests.
setTimeout(function () {
try {
expect(requestSpy.mock.calls.length).toBe(3);
done();
} catch (e) {
done(e);
}
}, 2000);
});
});
});

View File

@@ -0,0 +1,42 @@
import FakeDom from '../../../helpers/FakeDom';
jest.setTimeout(2000);
describe('Data Attribute Request AJAX library', function () {
it('can parse request data', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js',
'modules/system/assets/js/snowboard/build/snowboard.data-attr.js',
])
.render()
.then(
(dom) => {
const DataAttributeSingleton = dom.window.Snowboard.attributeRequest();
expect(
DataAttributeSingleton.parseData('{foo: "bar"}')
).toEqual({ foo: 'bar' });
expect(
DataAttributeSingleton.parseData('foo: \'bar\'')
).toEqual({ foo: 'bar' });
expect(
DataAttributeSingleton.parseData('{"key": "value", "nested": { "otherKey": "otherValue" }}')
).toEqual({
"key": "value",
"nested": {
"otherKey": "otherValue"
}
});
done();
}
);
});
});

View File

@@ -0,0 +1,706 @@
import FakeDom from '../../../helpers/FakeDom';
import FetchMock from '../../../helpers/FetchMock';
jest.setTimeout(2000);
describe('Request AJAX library', function () {
it('can be setup via a global event', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js'
])
.render()
.then(
(dom) => {
dom.window.Snowboard.getPlugin('request').mock('doAjax', (instance) => {
// Simulate success response
const resolved = Promise.resolve({
success: true
});
// Mock events
instance.snowboard.globalEvent('ajaxStart', instance, resolved);
if (instance.element) {
const event = new dom.window.Event('ajaxPromise');
event.promise = resolved;
instance.element.dispatchEvent(event);
}
return resolved;
});
// Listen to global event
dom.window.Snowboard.addPlugin('testListener', class TestListener extends dom.window.Snowboard.Singleton {
listens() {
return {
ajaxSetup: 'ajaxSetup',
};
}
ajaxSetup(instance) {
instance.handler = 'onChanged';
}
});
dom.window.Snowboard.request(undefined, 'onTest', {
complete: (data, instance) => {
expect(instance.handler).toEqual('onChanged');
done();
}
});
}
);
});
it('can be cancelled on setup via a global event', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js'
])
.render()
.then(
(dom) => {
dom.window.Snowboard.getPlugin('request').mock('doAjax', (instance) => {
// Simulate success response
const resolved = Promise.resolve({
success: true
});
// Mock events
instance.snowboard.globalEvent('ajaxStart', instance, resolved);
if (instance.element) {
const event = new dom.window.Event('ajaxPromise');
event.promise = resolved;
instance.element.dispatchEvent(event);
}
return resolved;
});
// Listen to global event
dom.window.Snowboard.addPlugin('testListener', class TestListener extends dom.window.Snowboard.Singleton {
listens() {
return {
ajaxSetup: 'ajaxSetup',
};
}
ajaxSetup() {
// Should cancel
return false;
}
});
const instance = dom.window.Snowboard.request(undefined, 'onTest', {
complete: (data, instance) => {
done(new Error('Request did not cancel'));
}
});
expect(instance.cancelled).toEqual(true);
done();
}
);
});
it('can be setup via a listener on the HTML element', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js'
])
.render('<button id="testElement">Test</button>')
.then(
(dom) => {
dom.window.Snowboard.getPlugin('request').mock('doAjax', (instance) => {
// Simulate success response
const resolved = Promise.resolve({
success: true
});
// Mock events
instance.snowboard.globalEvent('ajaxStart', instance, resolved);
if (instance.element) {
const event = new dom.window.Event('ajaxPromise');
event.promise = resolved;
instance.element.dispatchEvent(event);
}
return resolved;
});
// Listen to HTML element event
const element = dom.window.document.getElementById('testElement');
element.addEventListener('ajaxSetup', (event) => {
expect(event.request).toBeDefined();
event.request.handler = 'onChanged';
});
dom.window.Snowboard.request(element, 'onTest', {
complete: (data, instance) => {
expect(instance.handler).toEqual('onChanged');
done();
}
});
}
);
});
it('can be cancelled on setup via a listener on the HTML element', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js'
])
.render('<button id="testElement">Test</button>')
.then(
(dom) => {
dom.window.Snowboard.getPlugin('request').mock('doAjax', (instance) => {
// Simulate success response
const resolved = Promise.resolve({
success: true
});
// Mock events
instance.snowboard.globalEvent('ajaxStart', instance, resolved);
if (instance.element) {
const event = new dom.window.Event('ajaxPromise');
event.promise = resolved;
instance.element.dispatchEvent(event);
}
return resolved;
});
// Listen to HTML element event
const element = dom.window.document.getElementById('testElement');
element.addEventListener('ajaxSetup', (event) => {
event.preventDefault();
});
const instance = dom.window.Snowboard.request(element, 'onTest', {
complete: (data, instance) => {
done(new Error('Request did not cancel'));
}
});
expect(instance.cancelled).toEqual(true);
done();
}
);
});
it('can do a request and listen for completion', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js'
])
.render()
.then(
(dom) => {
dom.window.Snowboard.getPlugin('request').mock('doAjax', (instance) => {
// Simulate success response
const resolved = Promise.resolve({
success: true
});
// Mock events
instance.snowboard.globalEvent('ajaxStart', instance, resolved);
if (instance.element) {
const event = new Event('ajaxPromise');
event.promise = resolved;
instance.element.dispatchEvent(event);
}
return resolved;
});
dom.window.Snowboard.request(undefined, 'onTest', {
complete: (data, instance) => {
expect(data).toEqual({
success: true,
});
expect(instance.responseData).toEqual({
success: true,
});
expect(instance.responseError).toEqual(null);
done();
return false;
}
});
}
);
});
it('can do a request and listen for completion via a global event', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js'
])
.render()
.then(
(dom) => {
dom.window.Snowboard.getPlugin('request').mock('doAjax', (instance) => {
// Simulate success response
const resolved = Promise.resolve({
success: true
});
// Mock events
instance.snowboard.globalEvent('ajaxStart', instance, resolved);
if (instance.element) {
const event = new Event('ajaxPromise');
event.promise = resolved;
instance.element.dispatchEvent(event);
}
return resolved;
});
// Listen to global event
dom.window.Snowboard.addPlugin('testListener', class TestListener extends dom.window.Snowboard.Singleton {
listens() {
return {
ajaxDone: 'ajaxDone',
};
}
ajaxDone(data, instance) {
expect(data).toEqual({
success: true,
});
expect(instance.responseData).toEqual({
success: true,
});
expect(instance.responseError).toEqual(null);
done();
}
});
dom.window.Snowboard.request(undefined, 'onTest');
}
);
});
it('can do a request and listen for completion via a listener on the HTML element', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js'
])
.render('<button id="testElement">Test</button>')
.then(
(dom) => {
dom.window.Snowboard.getPlugin('request').mock('doAjax', (instance) => {
// Simulate success response
const resolved = Promise.resolve({
success: true
});
// Mock events
instance.snowboard.globalEvent('ajaxStart', instance, resolved);
if (instance.element) {
const event = new dom.window.Event('ajaxPromise');
event.promise = resolved;
instance.element.dispatchEvent(event);
}
return resolved;
});
// Listen to HTML element event
const element = dom.window.document.getElementById('testElement');
element.addEventListener('ajaxAlways', (event) => {
expect(event.request).toBeDefined();
expect(event.responseData).toEqual({
success: true
});
expect(event.responseError).toEqual(null);
done();
});
dom.window.Snowboard.request(element, 'onTest');
}
);
});
it('can do a request and listen for success', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js'
])
.render()
.then(
(dom) => {
dom.window.Snowboard.getPlugin('request').mock('doAjax', (instance) => {
// Simulate success response
const resolved = Promise.resolve({
success: true
});
// Mock events
instance.snowboard.globalEvent('ajaxStart', instance, resolved);
if (instance.element) {
const event = new Event('ajaxPromise');
event.promise = resolved;
instance.element.dispatchEvent(event);
}
return resolved;
});
dom.window.Snowboard.request(undefined, 'onTest', {
success: (data, instance) => {
expect(data).toEqual({
success: true,
});
expect(instance.responseData).toEqual({
success: true,
});
expect(instance.responseError).toEqual(null);
},
complete: (data, instance) => {
expect(data).toEqual({
success: true,
});
expect(instance.responseData).toEqual({
success: true,
});
expect(instance.responseError).toEqual(null);
done();
},
});
}
);
});
it('can do a request and listen for success via a global event', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js'
])
.render()
.then(
(dom) => {
dom.window.Snowboard.getPlugin('request').mock('doAjax', (instance) => {
// Simulate success response
const resolved = Promise.resolve({
success: true
});
// Mock events
instance.snowboard.globalEvent('ajaxStart', instance, resolved);
if (instance.element) {
const event = new Event('ajaxPromise');
event.promise = resolved;
instance.element.dispatchEvent(event);
}
return resolved;
});
// Listen to global event
dom.window.Snowboard.addPlugin('testListener', class TestListener extends dom.window.Snowboard.Singleton {
listens() {
return {
ajaxSuccess: 'ajaxSuccess',
};
}
ajaxSuccess(data, instance) {
expect(data).toEqual({
success: true,
})
expect(instance.responseData).toEqual({
success: true,
});
expect(instance.responseError).toEqual(null);
done();
}
});
dom.window.Snowboard.request(undefined, 'onTest');
}
);
});
it('can do a request and listen for failure', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js'
])
.render()
.then(
(dom) => {
dom.window.Snowboard.getPlugin('request').mock('doAjax', (instance) => {
// Simulate error response
const resolved = Promise.reject('This is an error');
// Mock events
instance.snowboard.globalEvent('ajaxStart', instance, resolved);
if (instance.element) {
const event = new Event('ajaxPromise');
event.promise = resolved;
instance.element.dispatchEvent(event);
}
return resolved;
});
dom.window.Snowboard.request(undefined, 'onTest', {
error: (data, instance) => {
expect(data).toEqual('This is an error');
expect(instance.responseData).toEqual(null);
expect(instance.responseError).toEqual('This is an error');
},
complete: (data, instance) => {
// Data will be null because no data was provided in the response.
expect(data).toBeNull();
expect(instance.responseData).toEqual(null);
expect(instance.responseError).toEqual('This is an error');
done();
},
});
}
);
});
it('can do a request and listen for failure via global event', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js'
])
.render()
.then(
(dom) => {
dom.window.Snowboard.getPlugin('request').mock('doAjax', (instance) => {
// Simulate error response
const resolved = Promise.reject('This is an error');
// Mock events
instance.snowboard.globalEvent('ajaxStart', instance, resolved);
if (instance.element) {
const event = new Event('ajaxPromise');
event.promise = resolved;
instance.element.dispatchEvent(event);
}
return resolved;
});
// Listen to global event
dom.window.Snowboard.addPlugin('testListener', class TestListener extends dom.window.Snowboard.Singleton {
listens() {
return {
ajaxError: 'ajaxError',
};
}
ajaxError(data, instance) {
expect(data).toEqual('This is an error')
expect(instance.responseData).toEqual(null);
expect(instance.responseError).toEqual('This is an error');
done();
}
});
dom.window.Snowboard.request(undefined, 'onTest');
}
);
});
it('requires a valid HTML element if provided', function () {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js'
])
.render()
.then(
(dom) => {
expect(() => {
const docFragment = dom.window.document.createDocumentFragment();
dom.window.Snowboard.request(docFragment, 'onTest');
}).toThrow('The element provided must be an Element instance');
}
);
});
it('requires a handler to be specified', function () {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js'
])
.render()
.then(
(dom) => {
expect(() => {
dom.window.Snowboard.request(undefined, undefined);
}).toThrow('The AJAX handler name is not specified');
}
);
});
it('requires a handler to be of the correct format', function () {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js'
])
.render()
.then(
(dom) => {
expect(() => {
dom.window.Snowboard.request(undefined, 'notRight');
}).toThrow('Invalid AJAX handler name');
}
);
});
it('can be run detached from an element with two parameters (handler and options)', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js'
])
.render()
.then(
(dom) => {
dom.window.Snowboard.getPlugin('request').mock('doAjax', (instance) => {
// Simulate success response
const resolved = Promise.resolve({
success: true
});
// Mock events
instance.snowboard.globalEvent('ajaxStart', instance, resolved);
if (instance.element) {
const event = new Event('ajaxPromise');
event.promise = resolved;
instance.element.dispatchEvent(event);
}
return resolved;
});
dom.window.Snowboard.request('onTest', {
complete: (data, instance) => {
done();
return false;
}
});
}
);
});
it('can correctly receive a mocked 404 JSON response', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.request.js'
])
.render()
.then(
(dom) => {
dom.window.fetch = FetchMock(
dom,
404,
'{"title":"404 Popup","markup":"<div>\\n <div class=\\"w-full bg-black\\">\\n <h2 class=\\"p-6 text-white text-2xl\\">Content not found<\\/h2>\\n<\\/div> <div class=\\"container p-6 mx-auto\\">\\n The requested popup could not be found, please try again.\\n<\\/div><\\/div>\\n"}',
{
'Content-Type': 'application/json'
}
);
dom.window.Snowboard.request('onTest', {
error: (error, instance) => {
expect(error).toEqual({
title: '404 Popup',
markup: '<div>\n <div class="w-full bg-black">\n <h2 class="p-6 text-white text-2xl">Content not found</h2>\n</div> <div class="container p-6 mx-auto">\n The requested popup could not be found, please try again.\n</div></div>\n'
})
expect(instance).toBeDefined();
expect(instance.responseData).toEqual(null);
expect(instance.responseError).toEqual({
title: '404 Popup',
markup: '<div>\n <div class="w-full bg-black">\n <h2 class="p-6 text-white text-2xl">Content not found</h2>\n</div> <div class="container p-6 mx-auto">\n The requested popup could not be found, please try again.\n</div></div>\n'
});
done();
}
});
}
);
});
});

View File

@@ -0,0 +1,316 @@
import FakeDom from '../../../helpers/FakeDom';
jest.setTimeout(2000);
describe('The Data Config extra functionality', function () {
it('can read the config from an element\'s data attributes', function (done) {
FakeDom
.new()
.addCss([
'modules/system/assets/css/snowboard.extras.css',
])
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.extras.js',
'modules/system/tests/js/fixtures/dataConfig/DataConfigFixture.js',
])
.render(
`<div
id="testElement"
data-id="389"
data-string-value="Hi there"
data-boolean="true"
></div>
<div
id="testElementTwo"
data-string-value="Hi there again"
data-name="Ben"
data-boolean="false"
data-extra-attr="This should not be available"
data-base64="base64:SSdtIGEgQmFzZTY0LWRlY29kZWQgc3RyaW5n"
></div>`
)
.then(
(dom) => {
const instance = dom.window.Snowboard.dataConfigFixture(
dom.window.document.querySelector('#testElement')
);
try {
expect(instance.config.get('id')).toEqual(389);
// Name should be null as it's the default value and not specified above
expect(instance.config.get('name')).toBeNull();
expect(instance.config.get('stringValue')).toBe('Hi there');
// Missing should be undefined as it's neither defined nor part of the default data
expect(instance.config.get('missing')).toBeUndefined();
expect(instance.config.get('boolean')).toBe(true);
expect(instance.config.get()).toMatchObject({
id: 389,
name: null,
stringValue: 'Hi there',
boolean: true,
base64: null,
});
} catch (error) {
done(error);
return;
}
const instanceTwo = dom.window.Snowboard.dataConfigFixture(
dom.window.document.querySelector('#testElementTwo')
);
try {
// ID is null as it's the default value and not specified above
expect(instanceTwo.config.get('id')).toBeNull();
expect(instanceTwo.config.get('name')).toBe('Ben');
expect(instanceTwo.config.get('stringValue')).toBe('Hi there again');
expect(instanceTwo.config.get('missing')).toBeUndefined();
expect(instanceTwo.config.get('boolean')).toBe(false);
// Extra attr is specified above, but it should not be available as a config value
// because it's not part of the `defaults()` in the fixture
expect(instanceTwo.config.get('extraAttr')).toBeUndefined();
// Base-64 decoded string
expect(instanceTwo.config.get('base64')).toBe('I\'m a Base64-decoded string');
done();
} catch (error) {
done(error);
}
}
);
});
it('can read the config from every data attribute of an element with "acceptAllDataConfigs" enabled', function (done) {
FakeDom
.new()
.addCss([
'modules/system/assets/css/snowboard.extras.css',
])
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.extras.js',
'modules/system/tests/js/fixtures/dataConfig/DataConfigFixture.js',
])
.render(
`<div
id="testElementTwo"
data-string-value="Hi there again"
data-name="Ben"
data-boolean="false"
data-extra-attr="This should now be available"
data-json="{ &quot;name&quot;: &quot;Ben&quot; }"
data-another-base64="base64:dHJ1ZQ=="
data-json-base64="base64:eyAiaWQiOiAxLCAidGl0bGUiOiAiU29tZSB0aXRsZSIgfQ=="
></div>`
)
.then(
(dom) => {
const instance = dom.window.Snowboard.dataConfigFixture(
dom.window.document.querySelector('#testElementTwo')
);
instance.acceptAllDataConfigs = true;
instance.config.refresh();
try {
// ID is null as it's the default value and not specified above
expect(instance.config.get('id')).toBeNull();
expect(instance.config.get('name')).toBe('Ben');
expect(instance.config.get('stringValue')).toBe('Hi there again');
expect(instance.config.get('missing')).toBeUndefined();
expect(instance.config.get('boolean')).toBe(false);
// These attributes below are specified above, and although they're not part of the
// defaults, they should be available because "acceptAllDataConfigs" is true
expect(instance.config.get('extraAttr')).toBe('This should now be available');
expect(instance.config.get('json')).toMatchObject({
name: 'Ben'
});
expect(instance.config.get('anotherBase64')).toBe(true);
expect(instance.config.get('jsonBase64')).toMatchObject({
id: 1,
title: 'Some title',
});
expect(instance.config.get()).toMatchObject({
id: null,
name: 'Ben',
stringValue: 'Hi there again',
boolean: false,
extraAttr: 'This should now be available',
json: {
name: 'Ben',
},
anotherBase64: true,
jsonBase64: {
id: 1,
title: 'Some title',
},
});
done();
} catch (error) {
done(error);
}
}
);
});
it('can refresh the config from the data attributes on the fly', function (done) {
FakeDom
.new()
.addCss([
'modules/system/assets/css/snowboard.extras.css',
])
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.extras.js',
'modules/system/tests/js/fixtures/dataConfig/DataConfigFixture.js',
])
.render(
`<div
id="testElement"
data-string-value="Hi there again"
data-name="Ben"
data-boolean="no"
></div>`
)
.then(
(dom) => {
const instance = dom.window.Snowboard.dataConfigFixture(
dom.window.document.querySelector('#testElement')
);
try {
expect(instance.config.get('id')).toBeNull();
expect(instance.config.get('name')).toBe('Ben');
expect(instance.config.get('stringValue')).toBe('Hi there again');
expect(instance.config.get('boolean')).toBe(false);
expect(instance.config.get()).toMatchObject({
id: null,
name: 'Ben',
stringValue: 'Hi there again',
boolean: false,
});
dom.window.document.querySelector('#testElement').setAttribute('data-id', '456');
dom.window.document.querySelector('#testElement').setAttribute('data-string-value', 'Changed');
dom.window.document.querySelector('#testElement').removeAttribute('data-boolean');
// Refresh config
instance.config.refresh();
expect(instance.config.get('id')).toBe(456);
expect(instance.config.get('name')).toBe('Ben');
expect(instance.config.get('stringValue')).toBe('Changed');
expect(instance.config.get('boolean')).toBeNull();
expect(instance.config.get()).toMatchObject({
id: 456,
name: 'Ben',
stringValue: 'Changed',
boolean: null,
});
done();
} catch (error) {
done(error);
}
}
);
});
it('can set config values at runtime', function (done) {
FakeDom
.new()
.addCss([
'modules/system/assets/css/snowboard.extras.css',
])
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.extras.js',
'modules/system/tests/js/fixtures/dataConfig/DataConfigFixture.js',
])
.render(
`<div
id="testElement"
data-string-value="Hi there again"
data-name="Ben"
data-boolean="false"
></div>`
)
.then(
(dom) => {
const instance = dom.window.Snowboard.dataConfigFixture(
dom.window.document.querySelector('#testElement')
);
try {
expect(instance.config.get('name')).toBe('Ben');
// Set config
instance.config.set('name', 'Luke');
expect(instance.config.get('name')).toBe('Luke');
// Refresh config
instance.config.refresh();
expect(instance.config.get('name')).toBe('Ben');
done();
} catch (error) {
done(error);
}
}
);
});
it('can set config values at runtime that persist through a reset', function (done) {
FakeDom
.new()
.addCss([
'modules/system/assets/css/snowboard.extras.css',
])
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/assets/js/snowboard/build/snowboard.extras.js',
'modules/system/tests/js/fixtures/dataConfig/DataConfigFixture.js',
])
.render(
`<div
id="testElement"
data-string-value="Hi there again"
data-name="Ben"
data-boolean="no"
></div>`
)
.then(
(dom) => {
const instance = dom.window.Snowboard.dataConfigFixture(
dom.window.document.querySelector('#testElement')
);
try {
expect(instance.config.get('name')).toBe('Ben');
// Set config
instance.config.set('name', 'Luke', true);
expect(instance.config.get('name')).toBe('Luke');
// Refresh config
instance.config.refresh();
expect(instance.config.get('name')).toBe('Luke');
done();
} catch (error) {
done(error);
}
}
);
});
});

View File

@@ -0,0 +1,134 @@
import FakeDom from '../../../helpers/FakeDom';
describe('PluginLoader class', function () {
it('can mock plugin methods', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js'
])
.render()
.then(
(dom) => {
dom.window.Snowboard.getPlugin('sanitizer').mock('sanitize', () => {
return 'all good';
});
expect(
dom.window.Snowboard.sanitizer().sanitize('<p onload="derp;"></p>')
).toEqual('all good');
// Test unmock
dom.window.Snowboard.getPlugin('sanitizer').unmock('sanitize');
expect(
dom.window.Snowboard.sanitizer().sanitize('<p onload="derp;"></p>')
).toEqual('<p></p>');
done();
},
(error) => {
done(error);
}
);
});
it('is frozen on construction and doesn\'t allow prototype pollution', function () {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
])
.render()
.then(
(dom) => {
const loader = dom.window.Snowboard.getPlugin('sanitizer');
expect(() => {
loader.newMethod = () => {
return true;
};
}).toThrow(TypeError);
expect(() => {
loader.newProperty = 'test';
}).toThrow(TypeError);
expect(() => {
loader.singleton.test = 'test';
}).toThrow(TypeError);
expect(loader.newMethod).toBeUndefined();
expect(loader.newProperty).toBeUndefined();
},
(error) => {
throw error;
}
);
});
it('should prevent modification of root instances', function () {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/tests/js/fixtures/framework/TestPlugin.js',
'modules/system/tests/js/fixtures/framework/TestSingleton.js',
])
.render()
.then(
(dom) => {
const rootInstance = dom.window.Snowboard.getPlugin('testPlugin').instance;
expect(() => {
rootInstance.newMethod = () => {
return true;
}
}).toThrow(TypeError);
expect(rootInstance.newMethod).toBeUndefined();
// Modifications can however be made to instances retrieved from the loader
const loadedInstance = dom.window.Snowboard.getPlugin('testPlugin').getInstance();
loadedInstance.newMethod = () => {
return true;
};
expect(loadedInstance.newMethod).toEqual(expect.any(Function));
expect(loadedInstance.newMethod()).toBe(true);
// But shouldn't follow through to new instances
const loadedInstanceTwo = dom.window.Snowboard.getPlugin('testPlugin').getInstance();
expect(loadedInstanceTwo.newMethod).toBeUndefined();
// The same rules apply for singletons, except that modifications will follow through to other uses
// of the singleton, since it's only one global instance.
const rootSingleton = dom.window.Snowboard.getPlugin('testSingleton').instance;
expect(() => {
rootSingleton.newMethod = () => {
return true;
}
}).toThrow(TypeError);
const loadedSingleton = dom.window.Snowboard.getPlugin('testSingleton').getInstance();
loadedSingleton.newMethod = () => {
return true;
};
expect(loadedSingleton.newMethod).toEqual(expect.any(Function));
expect(loadedSingleton.newMethod()).toBe(true);
const loadedSingletonTwo = dom.window.Snowboard.getPlugin('testSingleton').getInstance();
expect(loadedSingletonTwo.newMethod).toEqual(expect.any(Function));
expect(loadedSingletonTwo.newMethod()).toBe(true);
}
);
});
});

View File

@@ -0,0 +1,620 @@
import FakeDom from '../../../helpers/FakeDom';
describe('Snowboard framework', function () {
it('initialises correctly', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
])
.render()
.then(
(dom) => {
// Run assertions
try {
expect(dom.window.Snowboard).toBeDefined();
expect(dom.window.Snowboard.addPlugin).toBeDefined();
expect(dom.window.Snowboard.addPlugin).toEqual(expect.any(Function));
// Check PluginBase and Singleton abstracts exist
expect(dom.window.Snowboard.PluginBase).toBeDefined();
expect(dom.window.Snowboard.Singleton).toBeDefined();
// Check in-built plugins
expect(dom.window.Snowboard.getPluginNames()).toEqual(
expect.arrayContaining(['jsonparser', 'sanitizer'])
);
expect(dom.window.Snowboard.getPlugin('jsonparser').isFunction()).toEqual(false);
expect(dom.window.Snowboard.getPlugin('jsonparser').isSingleton()).toEqual(true);
expect(dom.window.Snowboard.getPlugin('sanitizer').isFunction()).toEqual(false);
expect(dom.window.Snowboard.getPlugin('sanitizer').isSingleton()).toEqual(true);
done();
} catch (error) {
done(error);
}
},
(error) => {
throw error;
}
);
});
it('is frozen on construction and doesn\'t allow prototype pollution', function () {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/tests/js/fixtures/framework/TestPlugin.js',
])
.render()
.then(
(dom) => {
expect(() => {
dom.window.Snowboard.newMethod = () => {
return true;
};
}).toThrow(TypeError);
expect(() => {
dom.window.Snowboard.newProperty = 'test';
}).toThrow(TypeError);
expect(() => {
dom.window.Snowboard.readiness.test = 'test';
}).toThrow(TypeError);
expect(dom.window.Snowboard.newMethod).toBeUndefined();
expect(dom.window.Snowboard.newProperty).toBeUndefined();
// You should not be able to modify the Snowboard object fed to plugins either
const instance = dom.window.Snowboard.testPlugin();
expect(() => {
instance.snowboard.newMethod = () => {
return true;
};
}).toThrow(TypeError);
},
(error) => {
throw error;
}
);
});
it('can add and remove a plugin', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/tests/js/fixtures/framework/TestPlugin.js',
])
.render()
.then(
(dom) => {
// Run assertions
const Snowboard = dom.window.Snowboard;
try {
// Check plugin caller
expect('testPlugin' in Snowboard).toEqual(true);
expect('testSingleton' in Snowboard).toEqual(false);
expect(Snowboard.hasPlugin('testPlugin')).toBe(true);
expect(Snowboard.getPluginNames()).toEqual(
expect.arrayContaining(['jsonparser', 'sanitizer', 'testplugin'])
);
const instance = Snowboard.testPlugin();
// Check plugin injected methods
expect(instance.snowboard).toBeDefined();
expect(instance.snowboard.getPlugin).toEqual(expect.any(Function));
expect(() => {
const method = instance.snowboard.initialise;
}).toThrow('cannot use');
expect(instance.destructor).toEqual(expect.any(Function));
// Check plugin method
expect(instance.testMethod).toBeDefined();
expect(instance.testMethod).toEqual(expect.any(Function));
expect(instance.testMethod()).toEqual('Tested');
// Check multiple instances
const instanceOne = Snowboard.testPlugin();
instanceOne.changed = true;
const instanceTwo = Snowboard.testPlugin();
expect(instanceOne).not.toEqual(instanceTwo);
const factory = Snowboard.getPlugin('testPlugin');
expect(factory.getInstances()).toEqual([instance, instanceOne, instanceTwo]);
// Remove plugin
Snowboard.removePlugin('testPlugin');
expect(Snowboard.hasPlugin('testPlugin')).toEqual(false);
expect(dom.window.Snowboard.getPluginNames()).toEqual(
expect.arrayContaining(['jsonparser', 'sanitizer'])
);
expect(Snowboard.testPlugin).not.toBeDefined();
done();
} catch (error) {
done(error);
}
},
(error) => {
throw error;
}
);
});
it('can add and remove a singleton', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/tests/js/fixtures/framework/TestSingleton.js',
])
.render()
.then(
(dom) => {
// Run assertions
const Snowboard = dom.window.Snowboard;
try {
expect('testPlugin' in Snowboard).toEqual(false);
expect('testSingleton' in Snowboard).toEqual(true);
// Check plugin caller
expect(Snowboard.hasPlugin('testSingleton')).toBe(true);
expect(Snowboard.getPluginNames()).toEqual(
expect.arrayContaining(['jsonparser', 'sanitizer', 'testsingleton'])
);
expect(Snowboard.testSingleton).toEqual(expect.any(Function));
const instance = Snowboard.testSingleton();
// Check plugin injected methods
expect(instance.snowboard).toBeDefined();
expect(instance.snowboard.getPlugin).toEqual(expect.any(Function));
expect(() => {
const method = instance.snowboard.initialise;
}).toThrow('cannot use');
expect(instance.destructor).toEqual(expect.any(Function));
// Check plugin method
expect(instance.testMethod).toBeDefined();
expect(instance.testMethod).toEqual(expect.any(Function));
expect(instance.testMethod()).toEqual('Tested');
// Check multiple instances (these should all be the same as this instance is a singleton)
const instanceOne = Snowboard.testSingleton();
instanceOne.changed = true;
const instanceTwo = Snowboard.testSingleton();
expect(instanceOne).toEqual(instanceTwo);
const factory = Snowboard.getPlugin('testSingleton');
expect(factory.getInstances()).toEqual([instance]);
// Remove plugin
Snowboard.removePlugin('testSingleton');
expect(Snowboard.hasPlugin('testSingleton')).toEqual(false);
expect(dom.window.Snowboard.getPluginNames()).toEqual(
expect.arrayContaining([ 'jsonparser', 'sanitizer'])
);
expect(Snowboard.testSingleton).not.toBeDefined();
done();
} catch (error) {
done(error);
}
},
(error) => {
throw error;
}
);
});
it('can listen and call global events', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/tests/js/fixtures/framework/TestListener.js',
])
.render()
.then(
(dom) => {
// Run assertions
const Snowboard = dom.window.Snowboard;
try {
expect(Snowboard.listensToEvent('eventOne')).toEqual(['test']);
expect(Snowboard.listensToEvent('eventTwo')).toEqual(['test']);
expect(Snowboard.listensToEvent('eventThree')).toEqual([]);
// Call global event one
const testClass = Snowboard.test();
Snowboard.globalEvent('eventOne', 42);
expect(testClass.eventResult).toEqual('Event called with arg 42');
// Call global event two - should fail as the test plugin doesn't have that method
expect(() => {
Snowboard.globalEvent('eventTwo');
}).toThrow('Missing "notExists" method in "test" plugin');
// Call global event three - nothing should happen
expect(() => {
Snowboard.globalEvent('eventThree');
}).not.toThrow();
done();
} catch (error) {
done(error);
}
},
(error) => {
throw error;
}
);
});
it('can listen and call global events that are simple closures', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/tests/js/fixtures/framework/TestClosureListener.js',
])
.render()
.then(
(dom) => {
// Run assertions
const Snowboard = dom.window.Snowboard;
try {
expect(Snowboard.listensToEvent('eventOne')).toEqual(['testclosure']);
expect(Snowboard.listensToEvent('eventTwo')).toEqual(['testclosure']);
expect(Snowboard.listensToEvent('eventThree')).toEqual([]);
// Call global event one
const testClass = Snowboard.testClosure();
Snowboard.globalEvent('eventOne');
expect(testClass.eventResult).toEqual('Closure eventOne called');
// Call global event two - should fail as the test plugin doesn't have that method
Snowboard.globalEvent('eventTwo', 42);
expect(testClass.eventResult).toEqual('Closure eventTwo called with arg \'42\'');
// Call global event three - nothing should happen
expect(() => {
Snowboard.globalEvent('eventThree');
}).not.toThrow();
done();
} catch (error) {
done(error);
}
},
(error) => {
throw error;
}
);
});
it('can listen and call global promise events', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/tests/js/fixtures/framework/TestPromiseListener.js',
])
.render()
.then(
(dom) => {
// Run assertions
const Snowboard = dom.window.Snowboard;
try {
expect(Snowboard.listensToEvent('promiseOne')).toEqual(['test']);
expect(Snowboard.listensToEvent('promiseTwo')).toEqual(['test']);
expect(Snowboard.listensToEvent('promiseThree')).toEqual([]);
// Call global event one
const testClass = Snowboard.test();
Snowboard.globalPromiseEvent('promiseOne', 'promise').then(
() => {
expect(testClass.eventResult).toEqual('Event called with arg promise');
// Call global event two - it should still work, even though it doesn't return a promise
Snowboard.globalPromiseEvent('promiseTwo', 'promise 2').then(
() => {
expect(testClass.eventResult).toEqual('Promise two called with arg promise 2');
// Call global event three - it should still work
Snowboard.globalPromiseEvent('promiseThree', 'promise 3').then(
() => {
done();
},
(error) => {
done(error);
}
);
},
(error) => {
done(error);
}
);
},
(error) => {
done(error);
}
);
} catch (error) {
done(error);
}
},
(error) => {
throw error;
}
);
});
it('can listen and call global promise events that are simple closures', function (done) {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/tests/js/fixtures/framework/TestPromiseClosureListener.js',
])
.render()
.then(
(dom) => {
// Run assertions
const Snowboard = dom.window.Snowboard;
try {
expect(Snowboard.listensToEvent('promiseOne')).toEqual(['testclosure']);
expect(Snowboard.listensToEvent('promiseTwo')).toEqual(['testclosure']);
expect(Snowboard.listensToEvent('promiseThree')).toEqual([]);
// Call global event one
const testClass = Snowboard.testClosure();
Snowboard.globalPromiseEvent('promiseOne', 'promise').then(
() => {
expect(testClass.eventResult).toEqual('Event called with arg promise');
// Call global event two - it should still work, even though it doesn't return a promise
Snowboard.globalPromiseEvent('promiseTwo', 'promise 2').then(
() => {
expect(testClass.eventResult).toEqual('Promise two called with arg promise 2');
// Call global event three - it should still work
Snowboard.globalPromiseEvent('promiseThree', 'promise 3').then(
() => {
done();
},
(error) => {
done(error);
}
);
},
(error) => {
done(error);
}
);
},
(error) => {
done(error);
}
);
} catch (error) {
done(error);
}
},
(error) => {
throw error;
}
);
});
it('will throw an error when using a plugin that has unfulfilled dependencies', function () {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/tests/js/fixtures/framework/TestHasDependencies.js',
])
.render()
.then(
(dom) => {
// Run assertions
const Snowboard = dom.window.Snowboard;
expect(() => {
Snowboard.testHasDependencies();
}).toThrow('The "testhasdependencies" plugin requires the following plugins: testdependencyone, testdependencytwo');
},
(error) => {
throw error;
}
);
});
it('will throw an error when using a plugin that has some unfulfilled dependencies', function () {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/tests/js/fixtures/framework/TestHasDependencies.js',
'modules/system/tests/js/fixtures/framework/TestDependencyOne.js',
])
.render()
.then(
(dom) => {
// Run assertions
const Snowboard = dom.window.Snowboard;
expect(() => {
Snowboard.testHasDependencies();
}).toThrow('The "testhasdependencies" plugin requires the following plugins: testdependencytwo');
},
(error) => {
throw error;
}
);
});
it('will not throw an error when using a plugin that has fulfilled dependencies', function () {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/tests/js/fixtures/framework/TestDependencyTwo.js',
'modules/system/tests/js/fixtures/framework/TestHasDependencies.js',
'modules/system/tests/js/fixtures/framework/TestDependencyOne.js',
])
.render()
.then(
(dom) => {
// Run assertions
const Snowboard = dom.window.Snowboard;
expect(() => {
Snowboard.testHasDependencies();
}).not.toThrow();
expect(Snowboard.testHasDependencies().testMethod()).toEqual('Tested');
},
(error) => {
throw error;
}
);
});
it('will not initialise a singleton that has unfulfilled dependencies', function () {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/tests/js/fixtures/framework/TestSingletonWithDependency.js',
])
.render()
.then(
(dom) => {
// Run assertions
const Snowboard = dom.window.Snowboard;
expect(() => {
Snowboard.testSingleton();
}).toThrow('The "testsingleton" plugin requires the following plugins: testdependencyone');
expect(Snowboard.listensToEvent('ready')).not.toContain('testsingleton');
expect(() => {
Snowboard.globalEvent('ready');
}).not.toThrow();
},
(error) => {
throw error;
}
);
});
it('will allow plugins to call other plugin methods', function () {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
'modules/system/tests/js/fixtures/framework/TestDependencyOne.js',
'modules/system/tests/js/fixtures/framework/TestSingletonWithDependency.js',
])
.render()
.then(
(dom) => {
// Run assertions
const Snowboard = dom.window.Snowboard;
const instance = Snowboard.testSingleton();
expect(instance.dependencyTest()).toEqual('Tested');
},
(error) => {
throw error;
}
);
});
it('doesn\'t allow PluginBase or Singleton abstracts to be modified', function () {
FakeDom
.new()
.addScript([
'modules/system/assets/js/build/manifest.js',
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
'modules/system/assets/js/snowboard/build/snowboard.base.js',
])
.render()
.then(
(dom) => {
expect(() => {
dom.window.Snowboard.PluginBase.newMethod = () => {
return true;
};
}).toThrow(TypeError);
expect(() => {
dom.window.Snowboard.PluginBase.destruct = () => {
return true;
};
}).toThrow(TypeError);
expect(() => {
dom.window.Snowboard.PluginBase.prototype.newMethod = () => {
return true;
};
}).toThrow(TypeError);
expect(() => {
dom.window.Snowboard.Singleton.newMethod = () => {
return true;
};
}).toThrow(TypeError);
expect(() => {
dom.window.Snowboard.Singleton.destruct = () => {
return true;
};
}).toThrow(TypeError);
expect(() => {
dom.window.Snowboard.Singleton.prototype.newMethod = () => {
return true;
};
}).toThrow(TypeError);
},
);
});
});

View File

@@ -0,0 +1,26 @@
/* globals window */
((Snowboard) => {
class DataConfigFixture extends Snowboard.PluginBase {
construct(element) {
this.element = element;
this.config = this.snowboard.dataConfig(this, element);
}
dependencies() {
return ['dataConfig'];
}
defaults() {
return {
id: null,
name: null,
stringValue: null,
boolean: null,
base64: null,
};
}
}
Snowboard.addPlugin('dataConfigFixture', DataConfigFixture);
})(window.Snowboard);

View File

@@ -0,0 +1,39 @@
/*
* Stubs for testing winter.form.js in isolation.
*
* Provides minimal implementations of WinterCMS dependencies that the
* form widget relies on at load time.
*/
// ocJSON - used by paramToObj inside the form widget
window.ocJSON = function (str) {
return JSON.parse(str);
};
// $(document).render() - used by the form widget to auto-initialize on DOM ready
jQuery.fn.render = function (fn) {
fn();
};
/*
* $.fn.request() - stub that mimics WinterCMS's AJAX framework contract.
*
* Returns a resolved jQuery Deferred with .done()/.fail()/.always() and a
* .success() alias (matching the WinterCMS framework.js Request class).
* The deferred resolves immediately since there is no real network I/O.
*/
jQuery.fn.request = function (handler, options) {
var deferred = jQuery.Deferred();
deferred.success = function (fn) {
return deferred.done(fn);
};
deferred.resolve();
return deferred;
};
// $.fn.loadIndicator() - no-op stub
jQuery.fn.loadIndicator = function () {
return this;
};

View File

@@ -0,0 +1,18 @@
/* globals window */
((Snowboard) => {
class TestClosureListener extends Snowboard.Singleton {
listens() {
return {
eventOne: () => {
this.eventResult = 'Closure eventOne called';
},
eventTwo: (arg) => {
this.eventResult = `Closure eventTwo called with arg '${arg}'`;
}
};
}
}
Snowboard.addPlugin('testClosure', TestClosureListener);
})(window.Snowboard);

View File

@@ -0,0 +1,11 @@
/* globals window */
((Snowboard) => {
class TestDependencyOne extends Snowboard.Singleton {
testMethod() {
return 'Tested';
}
}
Snowboard.addPlugin('testDependencyOne', TestDependencyOne);
})(window.Snowboard);

View File

@@ -0,0 +1,11 @@
/* globals window */
((Snowboard) => {
class TestDependencyTwo extends Snowboard.Singleton {
testMethod() {
return 'Tested';
}
}
Snowboard.addPlugin('testDependencyTwo', TestDependencyTwo);
})(window.Snowboard);

View File

@@ -0,0 +1,15 @@
/* globals window */
((Snowboard) => {
class TestHasDependencies extends Snowboard.Singleton {
dependencies() {
return ['testDependencyOne', 'testDependencyTwo'];
}
testMethod() {
return 'Tested';
}
}
Snowboard.addPlugin('testHasDependencies', TestHasDependencies);
})(window.Snowboard);

View File

@@ -0,0 +1,18 @@
/* globals window */
((Snowboard) => {
class TestListener extends Snowboard.Singleton {
listens() {
return {
eventOne: 'eventOne',
eventTwo: 'notExists'
};
}
eventOne(arg) {
this.eventResult = 'Event called with arg ' + arg;
}
}
Snowboard.addPlugin('test', TestListener);
})(window.Snowboard);

View File

@@ -0,0 +1,11 @@
/* globals window */
((Snowboard) => {
class TestPlugin extends Snowboard.PluginBase {
testMethod() {
return 'Tested';
}
}
Snowboard.addPlugin('testPlugin', TestPlugin);
})(window.Snowboard);

View File

@@ -0,0 +1,24 @@
/* globals window */
((Snowboard) => {
class TestPromiseClosureListener extends Snowboard.Singleton {
listens() {
return {
promiseOne: (arg) => {
return new Promise((resolve) => {
window.setTimeout(() => {
this.eventResult = 'Event called with arg ' + arg;
resolve();
}, 500);
});
},
promiseTwo: (arg) => {
this.eventResult = 'Promise two called with arg ' + arg;
return true;
},
};
}
}
Snowboard.addPlugin('testClosure', TestPromiseClosureListener);
})(window.Snowboard);

View File

@@ -0,0 +1,28 @@
/* globals window */
((Snowboard) => {
class TestPromiseListener extends Snowboard.Singleton {
listens() {
return {
promiseOne: 'promiseOne',
promiseTwo: 'promiseTwo'
};
}
promiseOne(arg) {
return new Promise((resolve) => {
window.setTimeout(() => {
this.eventResult = 'Event called with arg ' + arg;
resolve();
}, 500);
});
}
promiseTwo(arg) {
this.eventResult = 'Promise two called with arg ' + arg;
return true;
}
}
Snowboard.addPlugin('test', TestPromiseListener);
})(window.Snowboard);

View File

@@ -0,0 +1,11 @@
/* globals window */
((Snowboard) => {
class TestSingleton extends Snowboard.Singleton {
testMethod() {
return 'Tested';
}
}
Snowboard.addPlugin('testSingleton', TestSingleton);
})(window.Snowboard);

View File

@@ -0,0 +1,29 @@
/* globals window */
((Snowboard) => {
class TestSingletonWithDependency extends Snowboard.Singleton {
dependencies() {
return ['testDependencyOne'];
}
listens() {
return {
ready: 'ready',
};
}
ready() {
return 'Ready';
}
testMethod() {
return 'Tested';
}
dependencyTest() {
return this.snowboard.testDependencyOne().testMethod();
}
}
Snowboard.addPlugin('testSingleton', TestSingletonWithDependency);
})(window.Snowboard);

View File

@@ -0,0 +1,189 @@
/* globals __dirname, URL */
import { JSDOM } from 'jsdom'
import path from 'path'
export default class FakeDom
{
constructor(content, options)
{
if (options === undefined) {
options = {};
}
// Header settings
this.url = options.url || `file://${path.resolve(__dirname, '../../../../')}`;
this.referer = options.referer;
this.contentType = options.contentType || 'text/html';
// Content settings
this.headStart = options.headStart || '<!DOCTYPE html><html><head><title>Fake document</title>';
this.headEnd = options.headEnd || '</head>';
this.bodyStart = options.bodyStart || '<body>';
this.content = content || '';
this.bodyEnd = options.bodyEnd || '</body>';
this.foot = options.foot || '</html>';
// Callback settings
this.beforeParse = (typeof options.beforeParse === 'function')
? options.beforeParse
: undefined;
// Assets
this.css = [];
this.scripts = [];
this.inline = [];
}
static new(content, options)
{
return new FakeDom(content, options);
}
setContent(content)
{
this.content = content;
return this;
}
addScript(script, id)
{
if (Array.isArray(script)) {
script.forEach((item) => {
this.addScript(item);
});
return this;
}
let url = new URL(script, this.url);
let base = new URL(this.url);
if (url.host === base.host) {
this.scripts.push({
url: `${url.pathname}`,
id: id || this.generateId(),
});
} else {
this.scripts.push({
url,
id: id || this.generateId(),
});
}
return this;
}
addCss(css, id)
{
if (Array.isArray(css)) {
css.forEach((item) => {
this.addCss(item)
});
return this;
}
let url = new URL(css, this.url);
let base = new URL(this.url);
if (url.host === base.host) {
this.css.push({
url: `${url.pathname}`,
id: id || this.generateId(),
});
} else {
this.css.push({
url,
id: id || this.generateId(),
});
}
return this;
}
addInlineScript(script, id)
{
this.inline.push({
script,
id: id || this.generateId(),
element: null,
});
return this;
}
generateId()
{
let id = 'script-';
let chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-';
let charLength = chars.length;
for (let i = 0; i < 10; i++) {
let currentChar = chars.substr(Math.floor(Math.random() * charLength), 1);
id = `${id}${currentChar}`;
}
return id;
}
render(content)
{
if (content) {
this.content = content;
}
return new Promise((resolve, reject) => {
try {
const dom = new JSDOM(
this._renderContent(),
{
url: this.url,
referrer: this.referer,
contentType: this.contentType,
includeNodeLocations: true,
runScripts: 'dangerously',
resources: 'usable',
pretendToBeVisual: true,
beforeParse: this.beforeParse,
}
);
dom.window.resolver = () => {
resolve(dom);
};
} catch (e) {
reject(e);
}
});
}
_renderContent()
{
// Create content list
const content = [this.headStart];
// Embed CSS
this.css.forEach((css) => {
content.push(`<link rel="stylesheet" href="${css.url}" id="${css.id}">`);
});
content.push(this.headEnd, this.bodyStart, this.content);
// Embed scripts
this.scripts.forEach((script) => {
content.push(`<script src="${script.url}" id="${script.id}"></script>`);
});
this.inline.forEach((script) => {
content.push(`<script id="${script.id}">${script.script}</script>`);
});
// Add resolver
content.push(`<script>window.resolver()</script>`);
// Add final content
content.push(this.bodyEnd);
content.push(this.foot);
return content.join('\n');
}
}

View File

@@ -0,0 +1,23 @@
export default function (dom, statusCode, body, headers, jsonPromise, textPromise) {
return function () {
return Promise.resolve({
ok: statusCode >= 200 && statusCode < 300,
status: statusCode,
headers: new dom.window.Headers(headers),
json: () => {
if (jsonPromise) {
return jsonPromise;
}
return Promise.resolve(JSON.parse(body));
},
text: () => {
if (textPromise) {
return textPromise;
}
return Promise.resolve(body);
},
});
}
};

View File

@@ -0,0 +1,194 @@
/*
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/81/m6w95r0j7ms_10c47hdbz4gw0000gn/T/jest_dx",
// Automatically clear mock calls, instances and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
// coverageDirectory: undefined,
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
// coverageProvider: "babel",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};