const keyCodeEnter = 13; describe('Image Annotation Control', function() { var $annotatorPNG, $toolbox, $inputUploadImage, $buttonAddArrow, $buttonAddTextbox, $selectAddSymbol, $inputColourPicker, $buttonEraseMode, $buttonSaveImage, $containerAnnotation, $canvasAnnotation; beforeEach(function() { $annotatorPNG = $(document).find("." + flagAnnotatorPNG); $annotatorPNG.annotatorPNG(annotatorSettings); $toolbox = $annotatorPNG.find("." + flagToolbox); $inputUploadImage = $toolbox.find("." + flagUploadImage); $buttonAddArrow = $toolbox.find("." + flagAddArrow); $buttonAddTextbox = $toolbox.find("." + flagAddTextbox); $selectAddSymbol = $toolbox.find("." + flagAddSymbol); $inputColourPicker = $toolbox.find("." + flagColourPicker); $buttonEraseMode = $toolbox.find("." + flagEraseMode); $buttonSaveImage = $toolbox.find("." + flagSaveImage); $containerAnnotation = $annotatorPNG.find("." + flagContainerAnnotation); $canvasAnnotation = $containerAnnotation.find("." + flagCanvasAnnotation); }); afterEach(function() { let canvas = $($containerAnnotation.children()[0].children[0]).clone(); $containerAnnotation.empty(); $containerAnnotation.append(canvas); }); describe('1. Initialization and Setup', function() { it('a. Should initialize correctly on a given DOM element', async function() { expect($annotatorPNG).to.exist; }); it('b. Should create canvas with correct dimensions', async function() { let canvas = $canvasAnnotation.data(keyFabric); expect(canvas.width).to.equal(800); expect(canvas.height).to.equal(600); expect($canvasAnnotation.attr('width')).to.equal('800'); expect($canvasAnnotation.attr('height')).to.equal('600'); }); it('c. Should have all UI elements present with correct values after initialization', async function() { expect($annotatorPNG.find("." + flagToolbox)).to.have.length(1); expect($annotatorPNG.find("." + flagUploadImage)).to.have.length(1); expect($annotatorPNG.find("." + flagAddArrow)).to.have.length(1); expect($annotatorPNG.find("." + flagAddTextbox)).to.have.length(1); expect($annotatorPNG.find("." + flagAddSymbol)).to.have.length(1); expect($annotatorPNG.find("." + flagColourPicker)).to.have.length(1); expect($annotatorPNG.find("." + flagEraseMode)).to.have.length(1); expect($annotatorPNG.find("." + flagSaveImage)).to.have.length(1); expect($annotatorPNG.find("." + flagContainerAnnotation)).to.have.length(1); expect($annotatorPNG.find("." + flagCanvasAnnotation).length > 0).to.be.true; /* expect($toolbox).to.exist; expect($inputUploadImage).to.exist; expect($buttonAddArrow).to.exist; expect($buttonAddTextbox).to.exist; expect($selectAddSymbol).to.exist; expect($selectAddSymbol.find("option")).to.exist; expect($inputColourPicker).to.exist; expect($buttonEraseMode).to.exist; expect($buttonSaveImage).to.exist; expect($containerAnnotation).to.exist; expect($canvasAnnotation).to.exist; */ expect($inputUploadImage.val()).to.equal(''); expect($inputColourPicker.val()).to.equal('#ff0000'); expect($buttonSaveImage.text().length > 0).to.be.true; // For unkown settings argument value expect($buttonSaveImage.text()).to.equal('Save Image'); expect($canvasAnnotation.data(keyFabric)).to.exist; }); }); describe('2. Image Upload and Display', function() { it('a. Should successfully upload a PNG file', function(done) { const file = new File([''], 'screenshot (16).png', { type: 'image/png' }); const fileInput = $inputUploadImage[0]; const dataTransfer = new DataTransfer(); dataTransfer.items.add(file); fileInput.files = dataTransfer.files; $(fileInput).trigger('change'); setTimeout(() => { let canvas = $canvasAnnotation.data(keyFabric); if (canvas) console.log("canvas.backgroundImage: " + canvas.backgroundImage); expect(canvas.backgroundImage).to.exist; done(); }, 100); }); it('b. Should display the uploaded image correctly on the canvas', function(done) { const file = new File([''], 'screenshot (16).png', { type: 'image/png' }); const fileInput = $inputUploadImage[0]; const dataTransfer = new DataTransfer(); dataTransfer.items.add(file); fileInput.files = dataTransfer.files; $(fileInput).trigger('change'); setTimeout(() => { try { let canvas = $canvasAnnotation.data(keyFabric); let context = canvas.getContext('2d'); let imageData = context.getImageData(0, 0, canvas.width, canvas.height); expect(canvas.width).to.equal(800); expect(canvas.height).to.equal(600); expect(imageData.data.length).to.equal(800 * 600 * 4); done(); } catch (e) { console.log("Error during canvas initialisation: " + e); done(e); } }, 100); }); it('d. Should handle invalid file types', async function() { const file = new File([''], 'test.jpg', { type: 'image/jpeg' }); const fileInput = $inputUploadImage[0]; const confirmStub = sinon.stub(window, 'alert').returns(true); const dataTransfer = new DataTransfer(); dataTransfer.items.add(file); fileInput.files = dataTransfer.files; expect(() => $(fileInput).trigger('change')).to.throw(); confirmStub.restore(); }); }); describe('3. Arrow Addition', function() { it('a. Should create a new arrow on the canvas when "Add Arrow" is clicked', async function() { let canvas = $canvasAnnotation.data(keyFabric); console.log("canvas: " + canvas); await waitForClick($buttonAddArrow); console.log("canvas after: " + canvas); expect(canvas.getObjects()).to.have.lengthOf(1); expect(canvas.getObjects()[0].type).to.equal('path'); }); it('b. Should create arrow with correct default properties', async function() { let canvas = $canvasAnnotation.data(keyFabric); await waitForClick($buttonAddArrow); const arrow = canvas.getObjects()[0]; expect(arrow.fill).to.equal('#ff0000'); expect(arrow.stroke).to.equal('#ff0000'); expect(arrow.strokeWidth).to.equal(2); }); it('c. Should allow multiple arrows to be added to the canvas', async function() { let canvas = $canvasAnnotation.data(keyFabric); await waitForClick($buttonAddArrow); await waitForClick($buttonAddArrow); expect(canvas.getObjects()).to.have.lengthOf(2); }); }); describe('4. Textbox Addition', function() { it('a. Should create a new textbox on the canvas when "Add Textbox" is clicked', async function() { let canvas = $canvasAnnotation.data(keyFabric); await waitForClick($buttonAddTextbox); expect(canvas.getObjects()).to.have.lengthOf(1); expect(canvas.getObjects()[0].type).to.equal('textbox'); }); it('b. Should create textbox with correct default properties', async function() { let canvas = $canvasAnnotation.data(keyFabric); await waitForClick($buttonAddTextbox); const textbox = canvas.getObjects()[0]; expect(textbox.fontSize).to.equal(20); expect(textbox.left).to.equal(100); expect(textbox.top).to.equal(100); }); it('c. Should allow text to be entered and edited in the textbox', async function() { this.timeout(10000); let canvas = $canvasAnnotation.data(keyFabric); await waitForClick($buttonAddTextbox); const textbox = canvas.getObjects()[0]; console.log("textbox: " + textbox); textbox.text = 'New Text'; console.log("textbox: " + textbox); canvas.renderAll(); await new Promise(resolve => setTimeout(resolve, 50)); expect(textbox.text).to.equal('New Text'); }); it('d. Should allow multiple textboxes to be added to the canvas', async function() { let canvas = $canvasAnnotation.data(keyFabric); await waitForClick($buttonAddTextbox); await waitForClick($buttonAddTextbox); expect(canvas.getObjects()).to.have.lengthOf(2); }); }); describe('5. Element Manipulation', function() { it('a. Should allow arrows to be selected, moved, resized, and rotated', async function() { let canvas = $canvasAnnotation.data(keyFabric); await waitForClick($buttonAddArrow); const arrow = canvas.getObjects()[0]; arrow.set({ left: 150, top: 150, scaleX: 2, angle: 45 }); canvas.renderAll(); expect(arrow.left).to.equal(150); expect(arrow.top).to.equal(150); expect(arrow.scaleX).to.equal(2); expect(arrow.angle).to.equal(45); }); it('b. Should allow textboxes to be selected, moved, resized, and rotated', async function() { let canvas = $canvasAnnotation.data(keyFabric); await waitForClick($buttonAddTextbox); const textbox = canvas.getObjects()[0]; textbox.set({ left: 150, top: 150, scaleX: 2, angle: 45 }); canvas.renderAll(); expect(textbox.left).to.equal(150); expect(textbox.top).to.equal(150); expect(textbox.scaleX).to.equal(2); expect(textbox.angle).to.equal(45); }); }); describe('6. Erasing Elements', function() { afterEach(function() { setTimeout(() => {}, 2000); $annotatorPNG.annotatorPNG(annotatorSettings); }); it('a. Should remove the currently selected arrow when "Erase Element" is clicked', async function() { let canvas = $canvasAnnotation.data(keyFabric); await waitForClick($buttonAddArrow); canvas.setActiveObject(canvas.getObjects()[0]); await waitForClick($buttonEraseMode); expect(canvas.getObjects()).to.have.lengthOf(0); }); it('b. Should remove the currently selected textbox when "Erase Element" is clicked', function(done) { this.timeout(10000); let canvas = $canvasAnnotation.data(keyFabric); waitForClick($buttonAddTextbox).then(() => { canvas.setActiveObject(canvas.getObjects()[0]); canvas.renderAll(); waitForClick($buttonEraseMode).then(() => { canvas.renderAll(); expect(canvas.getObjects()).to.have.lengthOf(0); done(); }); }); }); it('c. Should not affect other elements on the canvas when erasing', function(done) { this.timeout(10000); let canvas = $canvasAnnotation.data(keyFabric); waitForClick($buttonAddArrow).then(() => { console.log("canvas.getObjects()[0]: ", canvas.getObjects()[0]); waitForClick($buttonAddTextbox).then(() => { canvas.setActiveObject(canvas.getObjects()[0]); waitForClick($buttonEraseMode).then(() => { expect(canvas.getObjects()).to.have.lengthOf(1); console.log("canvas.getObjects()[0]: ", canvas.getObjects()[0]); expect(canvas.getObjects()[0].text).to.exist; done(); }); }); }); }); }); describe('7. Saving Functionality', function() { it('a. Should trigger confirmation dialog when "Save" is clicked', async function() { const confirmStub = sinon.stub(window, 'confirm').returns(true); const alertStub = sinon.stub(window, 'alert').returns(true); await waitForClick($buttonSaveImage); expect(confirmStub.calledOnce).to.be.true; confirmStub.restore(); alertStub.restore(); }); it('b. Should prevent saving when confirmation dialog is canceled', async function() { const confirmStub = sinon.stub(window, 'confirm').returns(false); const consoleStub = sinon.stub(console, 'log'); await waitForClick($buttonSaveImage); expect(consoleStub.called).to.be.false; confirmStub.restore(); consoleStub.restore(); }); it('c. Should generate a Base64 PNG string when saving is confirmed', async function() { sinon.stub(window, 'confirm').returns(true); const alertStub = sinon.stub(window, 'alert').returns(true); const consoleStub = sinon.stub(console, 'log'); await waitForClick($buttonSaveImage); expect(consoleStub.calledOnce).to.be.true; const base64String = consoleStub.getCall(0).args[1]; expect(base64String).to.be.a('string'); expect(base64String).to.match(/^data:image\/png;base64,/); window.confirm.restore(); consoleStub.restore(); alertStub.restore(); }); }); describe('8. Edge Cases and Error Handling', function() { it('a. Should handle initialization on invalid DOM element', async function() { expect(() => $('#non-existent-element').imageAnnotator()).to.throw(); }); }); describe('9. Performance', function() { it('a. Should capture and generate screenshot within acceptable time', function(done) { this.timeout(5000); const startTime = performance.now(); const alertStub = sinon.stub(window, 'alert').returns(true); const confirmStub = sinon.stub(window, 'confirm').returns(true); $buttonSaveImage.click(); const endTime = performance.now(); const duration = endTime - startTime; expect(duration).to.be.below(1000); alertStub.restore(); confirmStub.restore(); done(); }); }); describe('10. Accessibility', function() { it('a. Should have appropriate ARIA labels for all interactive elements', async function() { expect($inputUploadImage.attr('aria-label').length > 0).to.be.true; expect($buttonAddArrow.attr('aria-label').length > 0).to.be.true; expect($buttonAddTextbox.attr('aria-label').length > 0).to.be.true; expect($selectAddSymbol.attr('aria-label').length > 0).to.be.true; expect($inputColourPicker.attr('aria-label').length > 0).to.be.true; expect($buttonEraseMode.attr('aria-label').length > 0).to.be.true; expect($buttonSaveImage.attr('aria-label').length > 0).to.be.true; expect($canvasAnnotation.attr('aria-label').length > 0).to.be.true; }); it('b. File input should be keyboard accessible', function(done) { // By navigation with tab key expect($inputUploadImage).to.satisfy(function($element) { let tabindex = $element.attr('tabindex'); let index = tabindex ? parseInt(tabindex) : NaN; return isNaN(index) || index >= 0; }); // By pressing Enter key $inputUploadImage.on('keydown', function(e) { if (e.which === keyCodeEnter) { done(); } }); const event = new KeyboardEvent('keydown', { 'keyCode': keyCodeEnter }); $inputUploadImage[0].dispatchEvent(event); }); it('c. Add arrow button should be keyboard accessible', function(done) { // By navigation with tab key expect($buttonAddArrow).to.satisfy(function($element) { let tabindex = $element.attr('tabindex'); let index = tabindex ? parseInt(tabindex) : NaN; return isNaN(index) || index >= 0; }); // By pressing Enter key $buttonAddArrow.on('keydown', function(e) { if (e.which === keyCodeEnter) { done(); } }); const event = new KeyboardEvent('keydown', { 'keyCode': keyCodeEnter }); $buttonAddArrow[0].dispatchEvent(event); }); it('d. Add textbox button should be keyboard accessible', function(done) { // By navigation with tab key expect($buttonAddTextbox).to.satisfy(function($element) { let tabindex = $element.attr('tabindex'); let index = tabindex ? parseInt(tabindex) : NaN; return isNaN(index) || index >= 0; }); // By pressing Enter key $buttonAddTextbox.on('keydown', function(e) { if (e.which === keyCodeEnter) { done(); } }); const event = new KeyboardEvent('keydown', { 'keyCode': keyCodeEnter }); $buttonAddTextbox[0].dispatchEvent(event); }); it('e. Add symbol DDL should be keyboard accessible', function(done) { // By navigation with tab key expect($selectAddSymbol).to.satisfy(function($element) { let tabindex = $element.attr('tabindex'); let index = tabindex ? parseInt(tabindex) : NaN; return isNaN(index) || index >= 0; }); // By pressing Enter key $selectAddSymbol.on('keydown', function(e) { if (e.which === keyCodeEnter) { done(); } }); const event = new KeyboardEvent('keydown', { 'keyCode': keyCodeEnter }); $selectAddSymbol[0].dispatchEvent(event); }); it('f. Colour picker should be keyboard accessible', function(done) { // By navigation with tab key expect($inputColourPicker).to.satisfy(function($element) { let tabindex = $element.attr('tabindex'); let index = tabindex ? parseInt(tabindex) : NaN; return isNaN(index) || index >= 0; }); // By pressing Enter key $inputColourPicker.on('keydown', function(e) { if (e.which === keyCodeEnter) { done(); } }); const event = new KeyboardEvent('keydown', { 'keyCode': keyCodeEnter }); $inputColourPicker[0].dispatchEvent(event); }); it('g. Erase mode button should be keyboard accessible', function(done) { // By navigation with tab key expect($buttonEraseMode).to.satisfy(function($element) { let tabindex = $element.attr('tabindex'); let index = tabindex ? parseInt(tabindex) : NaN; return isNaN(index) || index >= 0; }); // By pressing Enter key $buttonEraseMode.on('keydown', function(e) { if (e.which === keyCodeEnter) { done(); } }); const event = new KeyboardEvent('keydown', { 'keyCode': keyCodeEnter }); $buttonEraseMode[0].dispatchEvent(event); }); it('h. Save image button should be keyboard accessible', function(done) { // By navigation with tab key expect($buttonSaveImage).to.satisfy(function($element) { let tabindex = $element.attr('tabindex'); let index = tabindex ? parseInt(tabindex) : NaN; return isNaN(index) || index >= 0; }); // By pressing Enter key $buttonSaveImage.on('keydown', function(e) { if (e.which === keyCodeEnter) { done(); } }); const event = new KeyboardEvent('keydown', { 'keyCode': keyCodeEnter }); $buttonSaveImage[0].dispatchEvent(event); }); }); describe('11. jQuery Plugin Requirements', function() { it('a. Should return jQuery object for chaining', function() { const result = $annotatorPNG.annotatorPNG(annotatorSettings); expect(result).to.equal($annotatorPNG); }); it('b. Should initialize plugin on multiple elements', function() { $('body').append('
'); $('.test-class').annotatorPNG(annotatorSettings); expect($('.test-class').length).to.equal(2); $('.test-class').remove(); }); }); describe('12. Memory Management', function() { it('a. Should not leak memory on repeated initialization and destruction', function() { const initialMemory = performance.memory ? performance.memory.usedJSHeapSize : 0; for (let i = 0; i < 500; i++) { $annotatorPNG.annotatorPNG(annotatorSettings); $canvasAnnotation.data(keyFabric).dispose(); } const finalMemory = performance.memory ? performance.memory.usedJSHeapSize : 0; expect(finalMemory - initialMemory).to.be.below(1000000); // 1 MB ish }); it('b. Should remove all created DOM elements after use', function() { const initialChildCount = $annotatorPNG[0].childElementCount; const initialCanvasCount = $containerAnnotation[0].childElementCount; $annotatorPNG.annotatorPNG(annotatorSettings); expect($annotatorPNG[0].childElementCount).to.equal(initialChildCount); expect($containerAnnotation[0].childElementCount).to.equal(initialCanvasCount); }); }); });