Files
vivespos-landing/modules/system/tests/js/cases/framework/FormWidgetDependants.test.js
avives 1f72193a64
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
- 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
2026-08-21 19:29:00 -06:00

150 lines
5.8 KiB
JavaScript

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);
});
});
});