Skip to content

Commit e3fa27f

Browse files
committed
Update AvalynxLoader to version 1.0.1
Bumped AvalynxLoader version to 1.0.1 across all references, including README, package.json, examples, and documentation. Added error handling for invalid options or language objects, enhanced Jest test coverage for overlays, spinners, and multiple elements, and improved module export handling. Updated Dockerfile to use PHP 8.3 and added Node.js, Yarn, and Nano support.
1 parent 233aadb commit e3fa27f

File tree

10 files changed

+316
-38
lines changed

10 files changed

+316
-38
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Replace `path/to/avalynx-loader.js` with the actual path to the file in your pro
4141
AvalynxLoader is also available via [jsDelivr](https://www.jsdelivr.com/). You can include it in your project like this:
4242

4343
```html
44-
<script src="https://cdn.jsdelivr.net/npm/avalynx-loader@1.0.0/dist/js/avalynx-loader.min.js"></script>
44+
<script src="https://cdn.jsdelivr.net/npm/avalynx-loader@1.0.1/dist/js/avalynx-loader.min.js"></script>
4545
```
4646

4747
## Installation via NPM ([Link](https://www.npmjs.com/package/avalynx-loader))

__tests__/avalynx-loader.test.js

Lines changed: 256 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,271 @@
1-
import {AvalynxLoader} from '../src/js/avalynx-loader.esm.js';
1+
/**
2+
* AvalynxLoader Jest Tests
3+
* Comprehensive test suite for all important functionality
4+
*/
5+
6+
const AvalynxLoader = require('../src/js/avalynx-loader.js');
27

38
describe('AvalynxLoader', () => {
4-
let avalynxLoader;
5-
let div;
9+
let consoleErrorSpy;
610

711
beforeEach(() => {
8-
div = document.createElement('div');
9-
div.id = 'test';
10-
document.body.appendChild(div);
11-
avalynxLoader = new AvalynxLoader('#test');
12+
document.body.innerHTML = '';
13+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
1214
});
1315

14-
it('should create a avalynxLoader overlay when load is set to true', () => {
15-
avalynxLoader.load = true;
16-
expect(div.querySelector('div')).not.toBeNull();
16+
afterEach(() => {
17+
consoleErrorSpy.mockRestore();
18+
document.body.innerHTML = '';
1719
});
1820

19-
it('should hide the loader overlay when load is set to false', () => {
20-
avalynxLoader.load = true;
21-
avalynxLoader.load = false;
22-
expect(div.querySelector('div').style.display).toBe('none');
21+
describe('Constructor and selector handling', () => {
22+
test('initializes with default selector when none provided', () => {
23+
const el1 = document.createElement('div');
24+
el1.className = 'avalynx-loader';
25+
document.body.appendChild(el1);
26+
27+
const loader = new AvalynxLoader();
28+
expect(loader.elements).toBeDefined();
29+
expect(loader.elements.length).toBe(1);
30+
});
31+
32+
test('adds dot prefix if selector does not start with . or #', () => {
33+
const el = document.createElement('div');
34+
el.className = 'my-loader';
35+
document.body.appendChild(el);
36+
37+
const loader = new AvalynxLoader('my-loader');
38+
expect(loader.elements.length).toBe(1);
39+
});
40+
41+
test('keeps # prefix for id selectors', () => {
42+
const el = document.createElement('div');
43+
el.id = 'unique-loader';
44+
document.body.appendChild(el);
45+
46+
const loader = new AvalynxLoader('#unique-loader');
47+
expect(loader.elements.length).toBe(1);
48+
});
49+
50+
test('logs error and returns when no elements are found', () => {
51+
new AvalynxLoader('.not-existing');
52+
expect(consoleErrorSpy).toHaveBeenCalled();
53+
const msg = consoleErrorSpy.mock.calls[0][0];
54+
expect(msg).toMatch(/Loader\(s\) with selector/);
55+
});
2356
});
2457

25-
it('should use the default class name and loader text if none are provided', () => {
26-
avalynxLoader.load = true;
27-
const spinner = div.querySelector('div');
28-
expect(avalynxLoader.options.className).toBe('spinner-border text-primary');
29-
expect(avalynxLoader.language.loaderText).toBe('Loading...');
58+
describe('Options and language defaults/overrides', () => {
59+
test('uses default options and language', () => {
60+
const el = document.createElement('div');
61+
el.className = 'avalynx-loader';
62+
document.body.appendChild(el);
63+
64+
const loader = new AvalynxLoader();
65+
expect(loader.options.className).toBe('spinner-border text-primary');
66+
expect(loader.language.loaderText).toBe('Loading...');
67+
});
68+
69+
test('merges custom options and language', () => {
70+
const el = document.createElement('div');
71+
el.className = 'custom';
72+
document.body.appendChild(el);
73+
74+
const loader = new AvalynxLoader('.custom', { className: 'spinner-grow text-danger' }, { loaderText: 'Bitte warten...' });
75+
expect(loader.options.className).toBe('spinner-grow text-danger');
76+
expect(loader.language.loaderText).toBe('Bitte warten...');
77+
});
3078
});
3179

32-
it('should use the provided class name and loader text if they are provided', () => {
33-
avalynxLoader = new AvalynxLoader('#test', {className: 'test-class'}, {loaderText: 'Test text'});
34-
avalynxLoader.load = true;
35-
const spinner = div.querySelector('div');
36-
expect(avalynxLoader.options.className).toBe('test-class');
37-
expect(avalynxLoader.language.loaderText).toBe('Test text');
80+
describe('Overlay creation and visibility control', () => {
81+
function getOverlayFor(el, loader) {
82+
return loader.loaderOverlays.get(el);
83+
}
84+
85+
test('load=true shows overlay; load=false hides overlay', () => {
86+
const el = document.createElement('div');
87+
el.className = 'target';
88+
el.style.width = '200px';
89+
el.style.height = '100px';
90+
document.body.appendChild(el);
91+
92+
const loader = new AvalynxLoader('.target');
93+
94+
// Initially there should be no overlay in DOM until first show
95+
expect(getOverlayFor(el, loader)).toBeUndefined();
96+
97+
loader.load = true;
98+
const overlay = getOverlayFor(el, loader);
99+
expect(overlay).toBeInstanceOf(HTMLElement);
100+
expect(overlay.style.display).toBe('flex');
101+
102+
loader.load = false;
103+
expect(overlay.style.display).toBe('none');
104+
});
105+
106+
test('creates exactly one overlay per element and reuses it across calls', () => {
107+
const el = document.createElement('div');
108+
el.className = 'target2';
109+
document.body.appendChild(el);
110+
111+
const loader = new AvalynxLoader('.target2');
112+
113+
loader.load = true;
114+
const overlay1 = getOverlayFor(el, loader);
115+
expect(overlay1).toBeTruthy();
116+
117+
// Calling load=true again should not create a new overlay
118+
loader.load = true;
119+
const overlay2 = getOverlayFor(el, loader);
120+
expect(overlay2).toBe(overlay1);
121+
122+
// Ensure only one direct child (overlay) was appended
123+
const directChildren = Array.from(el.children).filter(child => child.nodeType === 1);
124+
expect(directChildren.length).toBe(1);
125+
});
126+
127+
test('recreates overlay if it was removed from the element', () => {
128+
const el = document.createElement('div');
129+
el.className = 'target3';
130+
document.body.appendChild(el);
131+
132+
const loader = new AvalynxLoader('.target3');
133+
loader.load = true;
134+
const overlay1 = getOverlayFor(el, loader);
135+
expect(overlay1).toBeTruthy();
136+
137+
// Remove overlay from DOM manually
138+
overlay1.remove();
139+
// Next show should create a new overlay
140+
loader.load = true;
141+
const overlay2 = getOverlayFor(el, loader);
142+
expect(overlay2).toBeTruthy();
143+
expect(overlay2).not.toBe(overlay1);
144+
});
145+
146+
test('hideLoader is a no-op if overlay does not exist', () => {
147+
const el = document.createElement('div');
148+
el.className = 'target4';
149+
document.body.appendChild(el);
150+
151+
const loader = new AvalynxLoader('.target4');
152+
// Directly call hideLoader without showing
153+
expect(() => loader.hideLoader(el)).not.toThrow();
154+
expect(getOverlayFor(el, loader)).toBeUndefined();
155+
});
156+
157+
test('works with multiple elements selected by the same selector', () => {
158+
const container = document.createElement('div');
159+
for (let i = 0; i < 3; i++) {
160+
const child = document.createElement('div');
161+
child.className = 'multi';
162+
container.appendChild(child);
163+
}
164+
document.body.appendChild(container);
165+
166+
const loader = new AvalynxLoader('.multi');
167+
expect(loader.elements.length).toBe(3);
168+
169+
loader.load = true;
170+
loader.elements.forEach(el => {
171+
const overlay = getOverlayFor(el, loader);
172+
expect(overlay).toBeTruthy();
173+
expect(overlay.style.display).toBe('flex');
174+
});
175+
176+
loader.load = false;
177+
loader.elements.forEach(el => {
178+
const overlay = getOverlayFor(el, loader);
179+
expect(overlay.style.display).toBe('none');
180+
});
181+
});
182+
});
183+
184+
describe('Spinner configuration and language text', () => {
185+
test('uses default spinner class and default loader text', () => {
186+
const el = document.createElement('div');
187+
el.className = 'spinner-default';
188+
document.body.appendChild(el);
189+
190+
const loader = new AvalynxLoader('.spinner-default');
191+
loader.load = true;
192+
193+
const overlay = loader.loaderOverlays.get(el);
194+
const spinner = overlay.querySelector('div');
195+
expect(spinner.className).toBe('spinner-border text-primary');
196+
const srOnly = spinner.querySelector('.visually-hidden');
197+
expect(srOnly).toBeTruthy();
198+
expect(srOnly.textContent).toBe('Loading...');
199+
});
200+
201+
test('applies custom spinner class', () => {
202+
const el = document.createElement('div');
203+
el.className = 'spinner-custom';
204+
document.body.appendChild(el);
205+
206+
const loader = new AvalynxLoader('.spinner-custom', { className: 'spinner-grow text-danger' });
207+
loader.load = true;
208+
209+
const overlay = loader.loaderOverlays.get(el);
210+
const spinner = overlay.querySelector('div');
211+
expect(spinner.className).toBe('spinner-grow text-danger');
212+
});
213+
214+
test('uses provided language text', () => {
215+
const el = document.createElement('div');
216+
el.className = 'lang-text';
217+
document.body.appendChild(el);
218+
219+
const loader = new AvalynxLoader('.lang-text', {}, { loaderText: 'Bitte warten...' });
220+
loader.load = true;
221+
222+
const overlay = loader.loaderOverlays.get(el);
223+
const spinner = overlay.querySelector('div');
224+
const srOnly = spinner.querySelector('.visually-hidden');
225+
expect(srOnly).toBeTruthy();
226+
expect(srOnly.textContent).toBe('Bitte warten...');
227+
});
228+
229+
test('empty loaderText results in empty accessible text (no visible text)', () => {
230+
const el = document.createElement('div');
231+
el.className = 'lang-empty';
232+
document.body.appendChild(el);
233+
234+
const loader = new AvalynxLoader('.lang-empty', {}, { loaderText: '' });
235+
loader.load = true;
236+
237+
const overlay = loader.loaderOverlays.get(el);
238+
const spinner = overlay.querySelector('div');
239+
const srOnly = spinner.querySelector('.visually-hidden');
240+
// Accept either no span or an empty span; in both cases, nothing is announced visually
241+
if (srOnly) {
242+
expect(srOnly.textContent).toBe('');
243+
} else {
244+
expect(spinner.textContent).toBe('');
245+
}
246+
});
38247
});
39248

249+
describe('Overlay style sanity checks', () => {
250+
test('overlay styles set for positioning and appearance', () => {
251+
const el = document.createElement('div');
252+
el.className = 'style-check';
253+
document.body.appendChild(el);
254+
255+
const loader = new AvalynxLoader('.style-check');
256+
loader.load = true;
257+
258+
const overlay = loader.loaderOverlays.get(el);
259+
expect(overlay.style.position).toBe('absolute');
260+
expect(overlay.style.top).toBe('0px');
261+
expect(overlay.style.left).toBe('0px');
262+
expect(overlay.style.width).toBe('100%');
263+
expect(overlay.style.height).toBe('100%');
264+
expect(overlay.style.alignItems).toBe('center');
265+
expect(overlay.style.justifyContent).toBe('center');
266+
expect(overlay.style.zIndex).toBe('1000');
267+
// Parent should become relative
268+
expect(el.style.position).toBe('relative');
269+
});
270+
});
40271
});

build.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ fs.readFile(filePath, 'utf8', (err, data) => {
1818
return;
1919
}
2020

21-
const result = data.replace(/class /g, 'export class ');
21+
let result = data.replace(/class AvalynxLoader /g, 'import * as bootstrap from \'bootstrap\';\n\nexport class AvalynxLoader ');
22+
23+
result = result.replace(/\n\nif \(typeof module !== 'undefined' && module\.exports\) \{\n module\.exports = AvalynxLoader;\n\}\n?$/, '');
2224

2325
fs.writeFile(filePath, result, 'utf8', err => {
2426
if (err) {

dist/js/avalynx-loader.esm.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*
44
* AvalynxLoader is a lightweight JavaScript library designed to provide a loading overlay for DOM elements. Based on Bootstrap >=5.3 without any framework dependencies.
55
*
6-
* @version 1.0.0
6+
* @version 1.0.1
77
* @license MIT
88
* @author https://github.com/avalynx/avalynx-loader/graphs/contributors
99
* @website https://github.com/avalynx/
@@ -12,12 +12,14 @@
1212
*
1313
* @param {string} selector - A custom selector for targeting tables within the DOM (default: '.avalynx-loader').
1414
* @param {object} options - An object containing the following keys:
15-
* @param {string} options.className - A custom export class name for the loader element (default: 'spinner-border text-primary').
15+
* @param {string} options.className - A custom class name for the loader element (default: 'spinner-border text-primary').
1616
* @param {object} language - An object containing the following keys:
1717
* @param {string} language.loaderText - A custom text for the loader element. If set to empty string, no text will be displayed. (default: 'Loading...').
1818
*
1919
*/
2020

21+
import * as bootstrap from 'bootstrap';
22+
2123
export class AvalynxLoader {
2224
constructor(selector, options = {}, language = {}) {
2325
if (!selector) {
@@ -31,10 +33,16 @@ export class AvalynxLoader {
3133
console.error("AvalynxLoader: Loader(s) with selector '" + selector + "' not found");
3234
return;
3335
}
36+
if (options === null || typeof options !== 'object') {
37+
options = {};
38+
}
3439
this.options = {
3540
className: 'spinner-border text-primary',
3641
...options
3742
};
43+
if (language === null || typeof language !== 'object') {
44+
language = {};
45+
}
3846
this.language = {
3947
loaderText: 'Loading...',
4048
...language
@@ -102,4 +110,4 @@ export class AvalynxLoader {
102110

103111
this.loaderOverlays.set(element, overlay);
104112
}
105-
}
113+
}

dist/js/avalynx-loader.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*
44
* AvalynxLoader is a lightweight JavaScript library designed to provide a loading overlay for DOM elements. Based on Bootstrap >=5.3 without any framework dependencies.
55
*
6-
* @version 1.0.0
6+
* @version 1.0.1
77
* @license MIT
88
* @author https://github.com/avalynx/avalynx-loader/graphs/contributors
99
* @website https://github.com/avalynx/
@@ -31,10 +31,16 @@ class AvalynxLoader {
3131
console.error("AvalynxLoader: Loader(s) with selector '" + selector + "' not found");
3232
return;
3333
}
34+
if (options === null || typeof options !== 'object') {
35+
options = {};
36+
}
3437
this.options = {
3538
className: 'spinner-border text-primary',
3639
...options
3740
};
41+
if (language === null || typeof language !== 'object') {
42+
language = {};
43+
}
3844
this.language = {
3945
loaderText: 'Loading...',
4046
...language
@@ -103,3 +109,7 @@ class AvalynxLoader {
103109
this.loaderOverlays.set(element, overlay);
104110
}
105111
}
112+
113+
if (typeof module !== 'undefined' && module.exports) {
114+
module.exports = AvalynxLoader;
115+
}

0 commit comments

Comments
 (0)