From 213da3f4d5a2b02488cdfe1316019fb046be4c4f Mon Sep 17 00:00:00 2001 From: jparez Date: Thu, 16 Jan 2025 09:30:44 +0100 Subject: [PATCH] refacto identifiers --- src/assets/images/ic_generate.svg | 3 + src/plugins/Identifiers.js | 205 ++++++++++--------------- src/plugins/util/components.js | 12 +- src/scss/base/_genymotion.scss | 6 +- src/scss/components/_chipTag.scss | 2 +- src/scss/components/_imei.scss | 56 ++++--- src/scss/components/_widgetwindow.scss | 2 +- tests/unit/identifiers.test.js | 76 +++++---- 8 files changed, 178 insertions(+), 184 deletions(-) create mode 100644 src/assets/images/ic_generate.svg diff --git a/src/assets/images/ic_generate.svg b/src/assets/images/ic_generate.svg new file mode 100644 index 00000000..feee38dd --- /dev/null +++ b/src/assets/images/ic_generate.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/plugins/Identifiers.js b/src/plugins/Identifiers.js index 7a85a44e..ea70a841 100644 --- a/src/plugins/Identifiers.js +++ b/src/plugins/Identifiers.js @@ -1,6 +1,7 @@ 'use strict'; const OverlayPlugin = require('./util/OverlayPlugin'); +const {textInput, chipTag} = require('./util/components'); const HEX = '0123456789abcdef'; const DIGITS = '0123456789'; @@ -29,10 +30,6 @@ module.exports = class Identifiers extends OverlayPlugin { // Register plugin this.instance.identifiers = this; - // Initial state - this.invalidAndroidId = false; - this.invalidDeviceId = false; - // Render components this.renderToolbarButton(); this.renderWidget(); @@ -46,11 +43,11 @@ module.exports = class Identifiers extends OverlayPlugin { const deviceId = values[1].match(/(device_id:)(\w+)/); if (deviceId) { - this.deviceInput.value = deviceId[2]; + this.deviceInput.setValue(deviceId[2]); } const androidId = values[1].match(/(android_id:)(\w+)/); if (androidId) { - this.androidInput.value = androidId[2]; + this.androidInput.setValue(androidId[2]); } }); } @@ -79,55 +76,80 @@ module.exports = class Identifiers extends OverlayPlugin { */ renderWidget() { // Create elements - this.widget = document.createElement('div'); - this.container = document.createElement('div'); - - // Generate title - const title = document.createElement('div'); - title.className = 'gm-title'; - title.innerHTML = this.i18n.IDENTIFIERS_TITLE || 'Identifiers'; - this.container.appendChild(title); + const {modal, container} = this.createTemplateModal({ + title: this.i18n.IDENTIFIERS_TITLE || 'Identifiers', + classes: 'gm-identifiers-plugin', + width: 378, + height: 422, + }); + this.widget = modal; + this.container = container; // Generate input rows const inputs = document.createElement('div'); inputs.className = 'gm-inputs'; // Build field list - const android = this.generateInput( - 'android', - 'Android ID', - '', - this.generateRandomAndroidId.bind(this), - this.validateAndroidId.bind(this), - ); - const device = this.generateInput( - 'device', - 'Device ID', - '(IMEI/MEID)', - this.generateRandomDeviceId.bind(this), - this.validateDeviceId.bind(this), - ); - inputs.appendChild(android); - inputs.appendChild(device); + const androidInputDiv = document.createElement('div'); + androidInputDiv.className = 'gm-identifier-android'; + const labelAndroidId = document.createElement('label'); + labelAndroidId.innerHTML = 'Android ID'; + this.androidInput = textInput.createTextInput({ + regexFilter: new RegExp(`^[${HEX}]{0,16}$`), + regexValidField: new RegExp(`^[${HEX}]{16}$`), + classes: 'gm-identifiers-android-input', + onChange: () => { + this.container.classList.remove('gm-identifiers-saved'); + this.checkIDsValidity(); + }, + }); + const generateAndroidIdBtn = document.createElement('div'); + generateAndroidIdBtn.className = 'gm-icon-button gm-identifiers-android-generate'; + generateAndroidIdBtn.onclick = this.generateRandomAndroidId.bind(this); + androidInputDiv.appendChild(this.androidInput.element); + androidInputDiv.appendChild(generateAndroidIdBtn); + inputs.appendChild(labelAndroidId); + inputs.appendChild(androidInputDiv); + + const deviceInputDiv = document.createElement('div'); + deviceInputDiv.className = 'gm-identifier-device'; + const labelDeviceId = document.createElement('label'); + labelDeviceId.innerHTML = 'Device ID (IMEI/MEID)'; + this.deviceInput = textInput.createTextInput({ + regexFilter: new RegExp(`^[${HEX}]{0,15}$`), + regexValidField: new RegExp(`^[${HEX}]{14,15}$`), + classes: 'gm-identifiers-device-input', + onChange: () => { + this.container.classList.remove('gm-identifiers-saved'); + this.checkIDsValidity(); + }, + }); + const generateDeviceIdBtn = document.createElement('div'); + generateDeviceIdBtn.className = 'gm-icon-button gm-identifiers-device-generate'; + generateDeviceIdBtn.onclick = this.generateRandomDeviceId.bind(this); + deviceInputDiv.appendChild(this.deviceInput.element); + deviceInputDiv.appendChild(generateDeviceIdBtn); + inputs.appendChild(labelDeviceId); + inputs.appendChild(deviceInputDiv); + + const actionsDiv = document.createElement('div'); + actionsDiv.className = 'gm-actions'; + const separator = document.createElement('div'); + separator.className = 'gm-separator'; + + const appliedTag = chipTag.createChip(); + actionsDiv.appendChild(appliedTag.element); this.submitBtn = document.createElement('button'); - this.submitBtn.innerHTML = this.i18n.IDENTIFIERS_UPDATE || 'Update'; - this.submitBtn.className = 'gm-action gm-identifiers-update'; - inputs.appendChild(this.submitBtn); + this.submitBtn.innerHTML = this.i18n.IDENTIFIERS_APPLY || 'Apply'; + this.submitBtn.className = 'gm-btn gm-identifiers-update'; + actionsDiv.appendChild(this.submitBtn); + this.submitBtn.disabled = true; this.submitBtn.onclick = this.sendDataToInstance.bind(this); this.container.appendChild(inputs); - - // Setup - this.widget.className = 'gm-overlay gm-identifiers-plugin gm-hidden'; - - // Add close button - const close = document.createElement('div'); - close.className = 'gm-close-btn'; - close.onclick = this.toggleWidget.bind(this); - - this.widget.appendChild(close); - this.widget.appendChild(this.container); + this.container.appendChild(separator); + this.container.appendChild(actionsDiv); // Render into document this.instance.root.appendChild(this.widget); @@ -141,10 +163,10 @@ module.exports = class Identifiers extends OverlayPlugin { sendDataToInstance(event) { event.preventDefault(); - const androidId = this.androidInput.value; - const deviceId = this.deviceInput.value; + const androidId = this.androidInput.getValue(); + const deviceId = this.deviceInput.getValue(); - if (androidId && this.androidInput.checkValidity()) { + if (androidId) { const json = { channel: 'framework', messages: ['set parameter android_id:' + androidId], @@ -152,14 +174,15 @@ module.exports = class Identifiers extends OverlayPlugin { this.instance.sendEvent(json); } - if (deviceId && this.deviceInput.checkValidity()) { + if (deviceId) { const json = { channel: 'settings', messages: ['set parameter device_id:' + deviceId], }; this.instance.sendEvent(json); } - this.toggleWidget(); + + this.container.classList.add('gm-identifiers-saved'); } /** @@ -172,17 +195,8 @@ module.exports = class Identifiers extends OverlayPlugin { event.preventDefault(); } - this.androidInput.value = this.generateHash(16, HEX); - this.invalidAndroidId = false; - this.checkErrors(); - } - - /** - * Validate Android ID input value. - */ - validateAndroidId() { - this.invalidAndroidId = !this.androidInput.checkValidity(); - this.checkErrors(); + this.androidInput.setValue(this.generateHash(16, HEX)); + this.checkIDsValidity(); } /** @@ -195,74 +209,19 @@ module.exports = class Identifiers extends OverlayPlugin { event.preventDefault(); } - this.deviceInput.value = this.generateHash(15, DIGITS); - this.invalidDeviceId = false; - this.checkErrors(); + this.deviceInput.setValue(this.generateHash(15, DIGITS)); + this.checkIDsValidity(); } /** - * Validate Device ID input value. + * Check if the IDs are valid. */ - validateDeviceId() { - this.invalidDeviceId = !this.deviceInput.checkValidity(); - this.checkErrors(); - } - - /** - * Input form validation. - */ - checkErrors() { - this.androidInput.classList[this.invalidAndroidId ? 'add' : 'remove']('gm-error'); - this.deviceInput.classList[this.invalidDeviceId ? 'add' : 'remove']('gm-error'); - - this.submitBtn.disabled = this.invalidAndroidId || this.invalidDeviceId; - } - - /** - * Create a form field element. - * - * @param {string} type Input type. - * @param {string} label Input label. - * @param {string} description Input description. - * @param {string} generationMethod Input random value generation method. - * @param {string} validationMethod Input value validation method. - * @return {HTMLElement} The created input. - */ - generateInput(type, label, description, generationMethod, validationMethod) { - const field = document.createElement('div'); - const inputWrap = document.createElement('div'); - const input = document.createElement('input'); - const button = document.createElement('button'); - - inputWrap.className = 'gm-input-group'; - input.className = 'gm-identifier-' + type + '-input'; - input.type = 'text'; - input.required = true; - this.instance.addListener(input, 'keyup', validationMethod); - inputWrap.appendChild(input); - - // Some customization - if (type === 'device') { - input.maxLength = 15; - input.pattern = '[' + HEX + ']{14,15}'; - } else if (type === 'android') { - input.maxLength = 16; - input.pattern = '[' + HEX + ']{16}'; + checkIDsValidity() { + if (this.androidInput.checkValidity() && this.deviceInput.checkValidity()) { + this.submitBtn.disabled = false; + } else { + this.submitBtn.disabled = true; } - - button.className = 'gm-identifier-' + type + '-generate'; - button.innerHTML = this.i18n.IDENTIFIERS_GENERATE || 'Generate'; - button.onclick = generationMethod; - inputWrap.appendChild(button); - - field.className = 'gm-identifier-' + type; - field.innerHTML = ''; - field.appendChild(inputWrap); - - this[type + 'Input'] = input; - this[type + 'Gen'] = button; - - return field; } /** diff --git a/src/plugins/util/components.js b/src/plugins/util/components.js index 1fffd381..570b2af9 100644 --- a/src/plugins/util/components.js +++ b/src/plugins/util/components.js @@ -181,7 +181,8 @@ const slider = (() => { */ const textInput = (() => { - const createTextInput = ({onChange = null, value = '', regexFilter, appendText = '', classes=''}) => { + const createTextInput = ({ + onChange = null, value = '', regexFilter, regexValidField, appendText = '', classes=''}) => { const inputDiv = document.createElement('div'); inputDiv.className = classes; const inputDivContainer = document.createElement('div'); @@ -221,8 +222,16 @@ const textInput = (() => { input.readOnly = readOnly; }; + const checkValidity = () => { + if (regexValidField && regexValidField.test(input.value)) { + return true; + } + return false; + }; + input.addEventListener('input', (event) => { const {value: v, selectionStart} = event.target; + if (regexFilter && !regexFilter.test(v)) { // delete the last character if it doesn't match the regex const correctedValue = v.slice(0, selectionStart - 1) + v.slice(selectionStart); @@ -240,6 +249,7 @@ const textInput = (() => { element: inputDiv, setValue, getValue, + checkValidity, setReadOnly, }; }; diff --git a/src/scss/base/_genymotion.scss b/src/scss/base/_genymotion.scss index 0c476f6b..d649e0cf 100644 --- a/src/scss/base/_genymotion.scss +++ b/src/scss/base/_genymotion.scss @@ -236,8 +236,12 @@ border-radius: 4px; &:hover{ - background: var(--gm-primary-color); background: color-mix(in srgb, var(--gm-primary-color), transparent 85%); color: var(--gm-primary-color) } + &:disabled{ + color: color-mix(in srgb, var(--gm-text-color), transparent 70%); + background: none; + cursor: default; + } } diff --git a/src/scss/components/_chipTag.scss b/src/scss/components/_chipTag.scss index 3fb3d7f7..884376c2 100644 --- a/src/scss/components/_chipTag.scss +++ b/src/scss/components/_chipTag.scss @@ -2,7 +2,7 @@ .gm-tag{ &-success{ .gm-tag-container{ - display: flex; + display: inline-flex; align-items: center; border-radius: 8px; padding: 2px 6px; diff --git a/src/scss/components/_imei.scss b/src/scss/components/_imei.scss index 961f1bdb..1d7306d2 100644 --- a/src/scss/components/_imei.scss +++ b/src/scss/components/_imei.scss @@ -2,30 +2,50 @@ * Identifiers (IMEI) plugin styles */ .device-renderer-instance .gm-identifiers-plugin { - .gm-identifier-device, - .gm-identifier-android { - .gm-description { - font-size: 90%; - padding-left: 10px; - } - - margin-bottom: 16px; - - .gm-input-group { - display: flex; + .gm-modal-body { + display: flex; + flex-direction: column; + } - button { - margin-top: -5px; + .gm-identifiers-saved { + .gm-actions{ + .gm-tag-success { + visibility: visible; } } } - input { - margin-right: 6px; + .gm-inputs{ flex: 1; - - &.gm-error { - border-bottom: 1px solid var(--gm-btn-bg-color); + .gm-identifier-android{ + margin-bottom: $modal-section-mb; + } + .gm-identifier-android, + .gm-identifier-device{ + display: flex; + justify-content: space-between; + gap: calc($modal-x-padding / 2); + align-items: center; + .gm-identifiers-device-input, + .gm-identifiers-android-input { + flex: 1; + } + .gm-identifiers-android-generate, + .gm-identifiers-device-generate { + mask-image: url('../assets/images/ic_generate.svg'); + -webkit-mask-image: url('../assets/images/ic_generate.svg'); + background-color: var(--gm-text-color); + cursor: pointer; + } + } + } + .gm-actions{ + display: flex; + justify-content: space-between; + align-items: center; + height: 60px; + .gm-tag-success { + visibility: hidden; } } } diff --git a/src/scss/components/_widgetwindow.scss b/src/scss/components/_widgetwindow.scss index f0573139..30fd7ec3 100644 --- a/src/scss/components/_widgetwindow.scss +++ b/src/scss/components/_widgetwindow.scss @@ -222,7 +222,7 @@ } .gm-separator { - width: calc(100% + $modal-x-padding + $modal-x-padding); + width: calc(100% + $modal-x-padding * 2); margin-left: -$modal-x-padding; margin-bottom: $spacing-m; } diff --git a/tests/unit/identifiers.test.js b/tests/unit/identifiers.test.js index 4c57b6b7..6d75626b 100644 --- a/tests/unit/identifiers.test.js +++ b/tests/unit/identifiers.test.js @@ -25,8 +25,7 @@ describe('Identifiers Plugin', () => { instance = new Instance(); new Identifiers(instance, { IDENTIFIERS_TITLE: 'TEST IDENTIFIERS PLUGIN TITLE', - IDENTIFIERS_UPDATE: 'TEST IDENTIFIERS PLUGIN UPDATE BUTTON', - IDENTIFIERS_GENERATE: 'TEST IDENTIFIERS PLUGIN GENERATE BUTTON', + IDENTIFIERS_APPLY: 'TEST IDENTIFIERS PLUGIN UPDATE BUTTON', }); plugin = document.getElementsByClassName('gm-identifiers-plugin')[0]; }); @@ -41,19 +40,18 @@ describe('Identifiers Plugin', () => { test('has translations', () => { expect(plugin.innerHTML).toEqual(expect.stringContaining('TEST IDENTIFIERS PLUGIN TITLE')); expect(plugin.innerHTML).toEqual(expect.stringContaining('TEST IDENTIFIERS PLUGIN UPDATE BUTTON')); - expect(plugin.innerHTML).toEqual(expect.stringContaining('TEST IDENTIFIERS PLUGIN GENERATE BUTTON')); }); describe('field validation', () => { - ['android', 'device'].forEach((field) => { + ['android', 'device'].forEach(() => { test('button disabled', () => { - const fieldInput = document.getElementsByClassName(`gm-identifier-${field}-input`)[0]; - const button = document.getElementsByClassName('gm-action gm-identifiers-update')[0]; - fieldInput.value = 'jean-michel'; - fieldInput.dispatchEvent(new Event('keyup')); - - expect(fieldInput.classList.contains('gm-error')).toBeTruthy(); - expect(button.disabled).toBeTruthy(); + expect(identifiers.submitBtn.disabled).toBeTruthy(); + identifiers.androidInput.setValue('jean-michel', true); + identifiers.deviceInput.setValue('jean-michel', true); + expect(identifiers.submitBtn.disabled).toBeTruthy(); + identifiers.androidInput.setValue('0123456789abcdef', true); + identifiers.deviceInput.setValue('0123456789abcde', true); + expect(identifiers.submitBtn.disabled).toBeFalsy(); }); }); }); @@ -64,37 +62,32 @@ describe('Identifiers Plugin', () => { test('unrelevant topics', () => { ['unrelevant', 'parameters unrelevant:value', ''].forEach((invalidValue) => { instance.emit('settings', `${invalidValue}`); - identifiers.validateAndroidId(); - identifiers.validateDeviceId(); - expect(identifiers.invalidAndroidId).toBeTruthy(); - expect(identifiers.invalidDeviceId).toBeTruthy(); + expect(identifiers.androidInput.checkValidity()).toBeFalsy(); + expect(identifiers.deviceInput.checkValidity()).toBeFalsy(); }); }); test('android_id', () => { ['jean-michel', '123', '0123456789abcdef0', ''].forEach((invalidValue) => { instance.emit('settings', `parameter android_id:${invalidValue}`); - identifiers.validateAndroidId(); - expect(identifiers.invalidAndroidId).toBeTruthy(); + expect(identifiers.androidInput.checkValidity()).toBeFalsy(); }); ['0123456789abcdef'].forEach((validValue) => { instance.emit('settings', `parameter android_id:${validValue}`); - identifiers.validateAndroidId(); - expect(identifiers.invalidAndroidId).toBeFalsy(); + expect(identifiers.androidInput.checkValidity()).toBeTruthy(); }); }); test('device_id', () => { ['jean-michel', '123', '', '0123456789abcdef'].forEach((invalidValue) => { instance.emit('settings', `parameter device_id:${invalidValue}`); - identifiers.validateDeviceId(); - expect(identifiers.invalidDeviceId).toBeTruthy(); + expect(identifiers.deviceInput.checkValidity()).toBeFalsy(); }); ['0123456789abcd', '0123456789abcde'].forEach((validValue) => { instance.emit('settings', `parameter device_id:${validValue}`); - identifiers.validateDeviceId(); + identifiers.deviceInput.checkValidity(); expect(identifiers.invalidDeviceId).toBeFalsy(); }); }); @@ -104,13 +97,13 @@ describe('Identifiers Plugin', () => { test('outgoing events', () => { const sendEventSpy = jest.spyOn(instance, 'sendEvent'); - identifiers.androidInput.value = 'jean-michel'; - identifiers.deviceInput.value = 'jean-michel'; + identifiers.androidInput.setValue('jean-michel'); + identifiers.deviceInput.setValue('jean-michel'); identifiers.sendDataToInstance(new Event('')); expect(sendEventSpy).toHaveBeenCalledTimes(0); - identifiers.androidInput.value = 'jean-michel'; - identifiers.deviceInput.value = '0123456789abcde'; + identifiers.androidInput.setValue('jean-michel'); + identifiers.deviceInput.setValue('0123456789abcde'); identifiers.sendDataToInstance(new Event('')); expect(sendEventSpy).toHaveBeenCalledTimes(1); expect(instance.outgoingMessages[0]).toEqual({ @@ -118,33 +111,38 @@ describe('Identifiers Plugin', () => { messages: ['set parameter device_id:0123456789abcde'], }); - identifiers.androidInput.value = '0123456789abcdef'; - identifiers.deviceInput.value = 'jean-michel'; + identifiers.androidInput.setValue('0123456789abcdef'); + // input jean-michel is invalid so we keep the previous value + identifiers.deviceInput.setValue('jean-michel'); identifiers.sendDataToInstance(new Event('')); - expect(sendEventSpy).toHaveBeenCalledTimes(2); + expect(sendEventSpy).toHaveBeenCalledTimes(3); expect(instance.outgoingMessages[1]).toEqual({ channel: 'framework', messages: ['set parameter android_id:0123456789abcdef'], }); + expect(instance.outgoingMessages[2]).toEqual({ + channel: 'settings', + messages: ['set parameter device_id:0123456789abcde'], + }); - identifiers.androidInput.value = '0123456789abcdef'; - identifiers.deviceInput.value = '0123456789abcde'; + identifiers.androidInput.setValue('0123456789abcdef'); + identifiers.deviceInput.setValue('0123456789abcdb');; identifiers.sendDataToInstance(new Event('')); - expect(sendEventSpy).toHaveBeenCalledTimes(4); - expect(instance.outgoingMessages[2]).toEqual({ + expect(sendEventSpy).toHaveBeenCalledTimes(5); + expect(instance.outgoingMessages[3]).toEqual({ channel: 'framework', messages: ['set parameter android_id:0123456789abcdef'], }); - expect(instance.outgoingMessages[3]).toEqual({ + expect(instance.outgoingMessages[4]).toEqual({ channel: 'settings', - messages: ['set parameter device_id:0123456789abcde'], + messages: ['set parameter device_id:0123456789abcdb'], }); - identifiers.androidInput.value = '1234567891234@é%'; - identifiers.deviceInput.value = '0123456789abcde'; + identifiers.androidInput.setValue('1234567891234@é%'); + identifiers.deviceInput.setValue('0123456789abcde'); identifiers.sendDataToInstance(new Event('')); - expect(sendEventSpy).toHaveBeenCalledTimes(5); - expect(instance.outgoingMessages[4]).toEqual({ + expect(sendEventSpy).toHaveBeenCalledTimes(7); + expect(instance.outgoingMessages[6]).toEqual({ channel: 'settings', messages: ['set parameter device_id:0123456789abcde'], });