diff --git a/src/BeaconLcp.js b/src/BeaconLcp.js index e052db6..1b51216 100644 --- a/src/BeaconLcp.js +++ b/src/BeaconLcp.js @@ -94,7 +94,7 @@ class BeaconLcp { current_src: "" }; - const css_bg_url_rgx = /url\(\s*?['"]?\s*?(.+?)\s*?["']?\s*?\)/ig; + const css_bg_url_rgx = /url\(\s*(['"]?)(.*?)\1\s*\)/ig; if (nodeName === "img" && element.srcset) { element_info.type = "img-srcset"; @@ -106,22 +106,38 @@ class BeaconLcp { element_info.type = "img"; element_info.src = element.src; element_info.current_src = element.currentSrc; + // Return null if src is empty or just whitespace + if (!element_info.src || !element_info.src.trim()) { + return null; + } } else if (nodeName === "video") { element_info.type = "img"; const source = element.querySelector('source'); element_info.src = element.poster || (source ? source.src : ''); element_info.current_src = element_info.src; + // Return null if src is empty or just whitespace + if (!element_info.src || !element_info.src.trim()) { + return null; + } } else if (nodeName === "svg") { const imageElement = element.querySelector('image'); if (imageElement) { + const href = imageElement.getAttribute('href') || ''; + if (!href || !href.trim()) { + return null; + } element_info.type = "img"; - element_info.src = imageElement.getAttribute('href') || ''; + element_info.src = href; element_info.current_src = element_info.src; } } else if (nodeName === "picture") { element_info.type = "picture"; const img = element.querySelector('img'); element_info.src = img ? img.src : ""; + // Return null if src is empty or just whitespace + if (!element_info.src || !element_info.src.trim()) { + return null; + } element_info.sources = Array.from(element.querySelectorAll('source')).map(source => ({ srcset: source.srcset || '', media: source.media || '', @@ -149,10 +165,10 @@ class BeaconLcp { } const matches = [...full_bg_prop.matchAll(css_bg_url_rgx)]; - element_info.bg_set = matches.map(m => m[1] ? { src: m[1].trim() + (m[2] ? " " + m[2].trim() : "") } : {}); - if (element_info.bg_set.every(item => item.src === "")) { - element_info.bg_set = matches.map(m => m[1] ? { src: m[1].trim() } : {}); - } + // m[2] is the URL content (m[1] is the quote character) + element_info.bg_set = matches + .map(m => m[2] ? { src: m[2].trim() } : {}) + .filter(item => item.src && item.src !== ""); if (element_info.bg_set.length <= 0) { return null; @@ -171,7 +187,13 @@ class BeaconLcp { _initWithFirstElementWithInfo(elements) { const firstElementWithInfo = elements.find(item => { - return item.elementInfo !== null && (item.elementInfo.src || item.elementInfo.srcset); + if (!item.elementInfo) { + return false; + } + const hasSrc = item.elementInfo.src && + (typeof item.elementInfo.src === 'string' ? item.elementInfo.src.trim() !== '' : Array.isArray(item.elementInfo.src)); + const hasSrcset = item.elementInfo.srcset && item.elementInfo.srcset.trim(); + return hasSrc || hasSrcset; }); if (!firstElementWithInfo) { diff --git a/test/BeaconLcp.test.js b/test/BeaconLcp.test.js index be57c0a..41e9e55 100644 --- a/test/BeaconLcp.test.js +++ b/test/BeaconLcp.test.js @@ -83,6 +83,31 @@ describe('BeaconManager', function() { assert.strictEqual(beacon.performanceImages.length, 0); }); + + it('should skip elements with empty src strings', function() { + const elements = [ + { element: { nodeName: 'img' }, elementInfo: { type: 'img', src: '' } }, // empty src + { element: { nodeName: 'img' }, elementInfo: { type: 'img', src: ' ' } }, // whitespace src + { element: { nodeName: 'img' }, elementInfo: { type: 'img', src: 'http://example.com/valid.jpg' } }, + ]; + + beacon._initWithFirstElementWithInfo(elements); + + assert.strictEqual(beacon.performanceImages.length, 1); + assert.strictEqual(beacon.performanceImages[0].src, 'http://example.com/valid.jpg'); + assert.strictEqual(beacon.performanceImages[0].label, 'lcp'); + }); + + it('should handle elements with srcset instead of src', function() { + const elements = [ + { element: { nodeName: 'img' }, elementInfo: { type: 'img-srcset', src: '', srcset: 'image-320w.jpg 320w, image-640w.jpg 640w' } }, + ]; + + beacon._initWithFirstElementWithInfo(elements); + + assert.strictEqual(beacon.performanceImages.length, 1); + assert.strictEqual(beacon.performanceImages[0].srcset, 'image-320w.jpg 320w, image-640w.jpg 640w'); + }); }); describe('#_getElementInfo()', function() { @@ -95,6 +120,120 @@ describe('BeaconManager', function() { assert.strictEqual(elementInfo, null); }); + + it('should return null for img elements with empty src', function() { + const element = { + nodeName: 'img', + src: '', + currentSrc: '' + }; + + const elementInfo = beacon._getElementInfo(element); + + assert.strictEqual(elementInfo, null); + }); + + it('should return null for img elements with whitespace-only src', function() { + const element = { + nodeName: 'img', + src: ' ', + currentSrc: ' ' + }; + + const elementInfo = beacon._getElementInfo(element); + + assert.strictEqual(elementInfo, null); + }); + + it('should return null for svg image elements with empty href', function() { + const imageElement = { + getAttribute: sinon.stub().returns('') + }; + const element = { + nodeName: 'svg', + querySelector: sinon.stub().returns(imageElement) + }; + + const elementInfo = beacon._getElementInfo(element); + + assert.strictEqual(elementInfo, null); + assert.strictEqual(element.querySelector.calledWith('image'), true); + assert.strictEqual(imageElement.getAttribute.calledWith('href'), true); + }); + + it('should return null for svg image elements with whitespace-only href', function() { + const imageElement = { + getAttribute: sinon.stub().returns(' \n\t ') + }; + const element = { + nodeName: 'svg', + querySelector: sinon.stub().returns(imageElement) + }; + + const elementInfo = beacon._getElementInfo(element); + + assert.strictEqual(elementInfo, null); + }); + + it('should return valid element info for svg image elements with non-empty href', function() { + const imageElement = { + getAttribute: sinon.stub().returns('https://example.com/image.svg') + }; + const element = { + nodeName: 'svg', + querySelector: sinon.stub().returns(imageElement) + }; + + const elementInfo = beacon._getElementInfo(element); + + assert.notStrictEqual(elementInfo, null); + assert.strictEqual(elementInfo.type, 'img'); + assert.strictEqual(elementInfo.src, 'https://example.com/image.svg'); + }); + + it('should return null for video elements with empty poster and no source', function() { + const element = { + nodeName: 'video', + poster: '', + querySelector: sinon.stub().returns(null) + }; + + const elementInfo = beacon._getElementInfo(element); + + assert.strictEqual(elementInfo, null); + }); + + it('should return null for picture elements with empty img src', function() { + const imgElement = { + src: '' + }; + const element = { + nodeName: 'picture', + querySelector: sinon.stub().returns(imgElement), + querySelectorAll: sinon.stub().returns([]) + }; + + const elementInfo = beacon._getElementInfo(element); + + assert.strictEqual(elementInfo, null); + }); + + it('should return valid element info for picture elements with non-empty img src', function() { + const imgElement = { + src: 'https://example.com/image.jpg' + }; + const element = { + nodeName: 'picture', + querySelector: sinon.stub().returns(imgElement), + querySelectorAll: sinon.stub().returns([]) + }; + + const elementInfo = beacon._getElementInfo(element); + + assert.notStrictEqual(elementInfo, null); + assert.strictEqual(elementInfo.type, 'picture'); + assert.strictEqual(elementInfo.src, 'https://example.com/image.jpg'); + }); }); describe('#_generateLcpCandidates()', function() {