From 1252f43d23b8865e2c41bdd10edf52c1f079cb4c Mon Sep 17 00:00:00 2001 From: Mathias Malmqvist Date: Mon, 29 Sep 2025 16:11:33 +0200 Subject: [PATCH] Allow user to skip quick-tests and then add them back --- css/main.css | 24 +- js/modals/quick-test-modal.js | 425 ++++++++++++++++++++++++++++++-- templates/quick-test-modal.html | 195 +-------------- 3 files changed, 426 insertions(+), 218 deletions(-) diff --git a/css/main.css b/css/main.css index fd2f5a3..c415a4f 100644 --- a/css/main.css +++ b/css/main.css @@ -50,27 +50,37 @@ dl.row dd { 50% { text-shadow: 0 0 15px rgba(13, 110, 253, 0.8), 0 0 25px rgba(13, 110, 253, 0.6); } } -/* Animation classes for different test types */ -i.fas.test-icon-usb { +/* Animation classes for different test types - only animate when accordion is expanded */ +.accordion-item:has(.accordion-collapse.show) i.fas.test-icon-usb { animation: pulse 1s ease-in-out infinite !important; } -i.fas.test-icon-buttons { +.accordion-item:has(.accordion-collapse.show) i.fas.test-icon-buttons { animation: bounce 0.6s ease-in-out infinite !important; } -i.fas.test-icon-haptic { +.accordion-item:has(.accordion-collapse.show) i.fas.test-icon-haptic { animation: shake 0.5s ease-in-out infinite !important; } -i.fas.test-icon-adaptive { +.accordion-item:has(.accordion-collapse.show) i.fas.test-icon-adaptive { animation: pulse 1s ease-in-out infinite !important; } -i.fas.test-icon-speaker { +.accordion-item:has(.accordion-collapse.show) i.fas.test-icon-speaker { animation: bounce 0.6s ease-in-out infinite !important; } -i.fas.test-icon-microphone { +.accordion-item:has(.accordion-collapse.show) i.fas.test-icon-microphone { animation: glow 1.5s ease-in-out infinite !important; } + +/* Skip button hover behavior */ +.skip-btn { + opacity: 0; + transition: opacity 0.2s ease-in-out; +} + +.accordion-header:hover .skip-btn { + opacity: 1; +} diff --git a/js/modals/quick-test-modal.js b/js/modals/quick-test-modal.js index 24e597d..eeb108a 100644 --- a/js/modals/quick-test-modal.js +++ b/js/modals/quick-test-modal.js @@ -10,6 +10,14 @@ const ACCORDION_ELEMENTS = [ ]; const TEST_SEQUENCE = ['usb', 'buttons', 'haptic', 'adaptive', 'speaker', 'microphone']; +const TEST_NAMES = { + 'usb': 'USB Connector', + 'buttons': 'Buttons', + 'haptic': 'Haptic Vibration', + 'adaptive': 'Adaptive Trigger', + 'speaker': 'Speaker', + 'microphone': 'Microphone' +}; const BUTTONS = ['triangle', 'cross', 'circle', 'square', 'l1', 'r1', 'l2', 'r2', 'l3', 'r3', 'up', 'down', 'left', 'right', 'create', 'touchpad', 'options', 'ps', 'mute']; const BUTTON_INFILL_MAPPING = { @@ -41,9 +49,12 @@ export class QuickTestModal { constructor(controllerInstance, { l }) { this.controller = controllerInstance; this.l = l; + this._modalListenersAdded = false; this.resetAllTests(); + this._loadSkippedTestsFromStorage(); + // Bind event handlers to maintain proper context this._boundAccordionShown = (event) => this._handleAccordionShown(event); this._boundAccordionHidden = (event) => this._handleAccordionHidden(event); @@ -71,9 +82,228 @@ export class QuickTestModal { longPressTimers: {}, longPressThreshold: 400, isTransitioning: false, + skippedTests: [], }; } + /** + * Save skipped tests to localStorage + */ + _saveSkippedTestsToStorage() { + try { + localStorage.setItem('quickTestSkippedTests', JSON.stringify(this.state.skippedTests)); + } catch (error) { + console.warn('Failed to save skipped tests to localStorage:', error); + } + } + + /** + * Load skipped tests from localStorage + */ + _loadSkippedTestsFromStorage() { + try { + const saved = localStorage.getItem('quickTestSkippedTests'); + if (saved) { + const skippedTests = JSON.parse(saved); + if (Array.isArray(skippedTests)) { + this.state.skippedTests = skippedTests.filter(test => TEST_SEQUENCE.includes(test)); + // Apply the skipped tests to the UI + this._applySkippedTestsToUI(); + } + } + } catch (error) { + console.warn('Failed to load skipped tests from localStorage:', error); + this.state.skippedTests = []; + } + } + + /** + * Apply skipped tests to the UI (rebuild accordion with non-skipped tests) + */ + _applySkippedTestsToUI() { + this._buildDynamicAccordion(); + this._updateSkippedTestsDropdown(); + } + + /** + * Build dynamic accordion with only non-skipped tests + */ + _buildDynamicAccordion() { + const $accordion = $('#quickTestAccordion'); + $accordion.empty(); + + // Get non-skipped tests in order + const activeTests = TEST_SEQUENCE.filter(testType => !this.state.skippedTests.includes(testType)); + + activeTests.forEach(testType => { + const accordionItem = this._createAccordionItem(testType); + $accordion.append(accordionItem); + }); + + // Re-initialize event listeners for the new accordion items + this._initEventListeners(); + } + + /** + * Create an accordion item for a specific test type + */ + _createAccordionItem(testType) { + const testName = TEST_NAMES[testType]; + const testIcons = { + 'usb': 'fas fa-plug', + 'buttons': 'fas fa-gamepad', + 'haptic': 'fas fa-mobile-alt', + 'adaptive': 'fas fa-hand-pointer', + 'speaker': 'fas fa-volume-up', + 'microphone': 'fas fa-microphone' + }; + + const testContent = this._getTestContent(testType); + + return $(` +
+

+ +

+
+
+ ${testContent} +
+
+
+ `); + } + + /** + * Get the content for a specific test type + */ + _getTestContent(testType) { + switch (testType) { + case 'usb': + return ` +

This test checks the reliability of the USB port.

+

Instructions: Wiggle the USB cable to see if the controller disconnects.

+
+ + Be gentle to avoid damage. +
+
+ + +
+ `; + case 'buttons': + return ` +

This test checks all controller buttons by requiring you to press each button three times.

+

Instructions: Press each button until they turn green.

+
+
+ +
+
+
+ + The test will automatically pass when all buttons have turned green. +
+
+ + + +
+ `; + case 'haptic': + return ` +

This test will activate the controller's vibration motors for 3 seconds.

+

Instructions: Feel for vibration in the controller.

+
+ + +
+ `; + case 'adaptive': + return ` +

This test will enable heavy resistance on both L2 and R2 triggers.

+

Instructions: Press L2 and R2 triggers to feel the trigger resistance.

+
+ + +
+ `; + case 'speaker': + return ` +

This test will play a tone through the controller's built-in speaker.

+

Instructions: Listen for a tone from the controller speaker.

+
+ + +
+ `; + case 'microphone': + return ` +

This test will monitor the controller's microphone input levels.

+

Instructions: Blow gently into the controller's microphone. You should see the audio level indicator respond.

+ +
+ + +
+ `; + default: + return ''; + } + } + + /** + * Clear saved skipped tests from localStorage + */ + _clearSkippedTestsFromStorage() { + try { + localStorage.removeItem('quickTestSkippedTests'); + } catch (error) { + console.warn('Failed to clear skipped tests from localStorage:', error); + } + } + /** * Start icon animation for a specific test type */ @@ -115,7 +345,7 @@ export class QuickTestModal { * Check if all tests have been completed */ _areAllTestsCompleted() { - return TEST_SEQUENCE.every(test => this.state[test] !== null); + return TEST_SEQUENCE.every(test => this.state[test] !== null || this.state.skippedTests.includes(test)); } /** @@ -123,7 +353,13 @@ export class QuickTestModal { */ // Set up event listeners for accordion collapse events to auto-start tests _initEventListeners() { - ACCORDION_ELEMENTS.forEach(elementId => { + // Remove existing listeners first + this._removeAccordionEventListeners(); + + // Add listeners for currently active tests + const activeTests = TEST_SEQUENCE.filter(testType => !this.state.skippedTests.includes(testType)); + activeTests.forEach(testType => { + const elementId = `${testType}-test-collapse`; const $element = $(`#${elementId}`); if ($element.length) { $element.on('shown.bs.collapse', this._boundAccordionShown); @@ -131,9 +367,28 @@ export class QuickTestModal { } }); - $('#quickTestModal').on('hidden.bs.modal', this._boundModalHidden); - $('#quickTestModal').on('shown.bs.modal', () => { - this._updateInstructions(); + // Only add modal listeners once + if (!this._modalListenersAdded) { + $('#quickTestModal').on('hidden.bs.modal', this._boundModalHidden); + $('#quickTestModal').on('shown.bs.modal', () => { + this._updateInstructions(); + }); + this._modalListenersAdded = true; + } + } + + /** + * Remove accordion event listeners only + */ + _removeAccordionEventListeners() { + // Remove listeners from all possible test elements + TEST_SEQUENCE.forEach(testType => { + const elementId = `${testType}-test-collapse`; + const $element = $(`#${elementId}`); + if ($element.length) { + $element.off('shown.bs.collapse', this._boundAccordionShown); + $element.off('hidden.bs.collapse', this._boundAccordionHidden); + } }); } @@ -142,21 +397,17 @@ export class QuickTestModal { */ removeEventListeners() { console.log("Removing event listeners"); - ACCORDION_ELEMENTS.forEach(elementId => { - const $element = $(`#${elementId}`); - if ($element.length) { - $element.off('shown.bs.collapse', this._boundAccordionShown); - $element.off('hidden.bs.collapse', this._boundAccordionHidden); - } - }); - + this._removeAccordionEventListeners(); $('#quickTestModal').off('hidden.bs.modal', this._boundModalHidden); + this._modalListenersAdded = false; } /** * Open the Quick Test modal */ async open() { + // Build the dynamic accordion first + this._buildDynamicAccordion(); await this._initSvgController(); bootstrap.Modal.getOrCreateInstance('#quickTestModal').show(); } @@ -165,9 +416,14 @@ export class QuickTestModal { * Initialize SVG controller for the quick test modal */ async _initSvgController() { + // Only initialize SVG if buttons test is not skipped + if (this.state.skippedTests.includes('buttons')) { + return; + } + const svgContainer = document.getElementById('quick-test-controller-svg-placeholder'); if (!svgContainer) { - console.warn('Quick test SVG container not found'); + console.warn('Quick test SVG container not found - buttons test may be skipped'); return; } @@ -589,6 +845,88 @@ export class QuickTestModal { this._expandNextTest(testType); } + /** + * Skip a test and remove it from the accordion + */ + skipTest(testType) { + // Add to skipped tests if not already there + if (!this.state.skippedTests.includes(testType)) { + this.state.skippedTests.push(testType); + } + + // Save to localStorage + this._saveSkippedTestsToStorage(); + + // Stop any ongoing test activities + this._stopIconAnimation(testType); + if (testType === 'adaptive') { + this._stopAdaptiveTest(); + } else if (testType === 'microphone') { + this._stopMicrophoneTest(); + } else if (testType === 'buttons') { + this._stopButtonsTest(); + } + + // Rebuild the accordion without the skipped test + this._buildDynamicAccordion(); + + this._updateSkippedTestsDropdown(); + this._updateTestSummary(); + this._expandNextTest(testType); + this._updateInstructions(); + } + + /** + * Add a test back from the skipped list + */ + addTestBack(testType) { + // Remove from skipped tests + const index = this.state.skippedTests.indexOf(testType); + if (index > -1) { + this.state.skippedTests.splice(index, 1); + } + + this._saveSkippedTestsToStorage(); + + // Reset test status in state + this.state[testType] = null; + + // Rebuild the accordion with the restored test + this._buildDynamicAccordion(); + + this._updateSkippedTestsDropdown(); + this._updateTestSummary(); + this._updateInstructions(); + } + + /** + * Update the skipped tests dropdown + */ + _updateSkippedTestsDropdown() { + const $dropdown = $('#skipped-tests-dropdown'); + const $list = $('#skipped-tests-list'); + + if (this.state.skippedTests.length === 0) { + $dropdown.hide(); + return; + } + + $dropdown.show(); + $list.empty(); + + this.state.skippedTests.forEach(testType => { + const testName = this.l(TEST_NAMES[testType]); + const $item = $(` +
  • + + ${testName} + +
  • + `); + $list.append($item); + }); + } + /** * Update test summary display */ @@ -597,6 +935,7 @@ export class QuickTestModal { let completed = 0; let passed = 0; + let skipped = this.state.skippedTests.length; TEST_SEQUENCE.forEach(test => { if (this.state[test] !== null) { @@ -606,12 +945,18 @@ export class QuickTestModal { }); const numTests = TEST_SEQUENCE.length; - if (completed === 0) { + const totalProcessed = completed + skipped; + + if (totalProcessed === 0) { $summary.text(this.l('No tests completed yet.')); $summary.attr('class', 'text-muted ds-i18n'); } else { - $summary.text(this.l(`${completed}/${numTests} tests completed. ${passed} passed, ${completed - passed} failed.`)); - $summary.attr('class', completed === numTests ? 'text-success' : 'text-info'); + let summaryText = this.l(`${completed}/${numTests} tests completed. ${passed} passed, ${completed - passed} failed.`); + if (skipped > 0) { + summaryText += this.l(` ${skipped} skipped.`); + } + $summary.text(summaryText); + $summary.attr('class', totalProcessed === numTests ? 'text-success' : 'text-info'); } } @@ -625,10 +970,10 @@ export class QuickTestModal { const $currentCollapse = $(`#${currentTest}-test-collapse`); bootstrap.Collapse.getInstance($currentCollapse[0])?.hide(); - // Find next untested item + // Find next untested item (not skipped and not completed) for (let i = currentIndex + 1; i < TEST_SEQUENCE.length; i++) { const nextTest = TEST_SEQUENCE[i]; - if (this.state[nextTest] === null) { + if (this.state[nextTest] === null && !this.state.skippedTests.includes(nextTest)) { const $nextCollapse = $(`#${nextTest}-test-collapse`); // Expand next @@ -646,6 +991,10 @@ export class QuickTestModal { */ _getCurrentActiveTest() { for (const test of TEST_SEQUENCE) { + // Skip tests that are in the skipped list + if (this.state.skippedTests.includes(test)) { + continue; + } const $collapse = $(`#${test}-test-collapse`); if ($collapse.hasClass('show')) { return test; @@ -820,11 +1169,15 @@ export class QuickTestModal { // First, reset all tests to ensure clean state this.resetAllTests(); - // After a short delay, start with the first test + // After a short delay, start with the first non-skipped test setTimeout(() => { - const [firstTest] = TEST_SEQUENCE; - const $firstCollapse = $(`#${firstTest}-test-collapse`); - bootstrap.Collapse.getOrCreateInstance($firstCollapse[0]).show(); + // Find the first test that is not skipped + const firstAvailableTest = TEST_SEQUENCE.find(test => !this.state.skippedTests.includes(test)); + + if (firstAvailableTest) { + const $firstCollapse = $(`#${firstAvailableTest}-test-collapse`); + bootstrap.Collapse.getOrCreateInstance($firstCollapse[0]).show(); + } }, 300); } @@ -860,6 +1213,9 @@ export class QuickTestModal { // Reset state this._initializeState(); + // Load saved skipped tests from localStorage + this._loadSkippedTestsFromStorage(); + // Clear any active long-press timers before resetting state this._clearAllLongPressTimers(); @@ -884,12 +1240,18 @@ export class QuickTestModal { $accordionItem.removeClass('border-success border-danger'); $accordionButton.css('backgroundColor', ''); // Clear background color + // Show all test items initially + $accordionItem.show(); + if (test === 'microphone') { const $levelContainer = $('#mic-level-container'); $levelContainer.hide(); } }); + // Apply skipped tests to UI (hide skipped items) + this._applySkippedTestsToUI(); + this._updateTestSummary(); // Update instructions after reset @@ -956,7 +1318,6 @@ export async function show_quick_test_modal(controller, { l } = {}) { await currentQuickTestInstance.open(); } -// Legacy function exports for backward compatibility (used by HTML onclick handlers) function markTestResult(testType, passed) { if (currentQuickTestInstance) { currentQuickTestInstance.markTestResult(testType, passed); @@ -975,7 +1336,21 @@ function resetButtonsTest() { } } +function skipTest(testType) { + if (currentQuickTestInstance) { + currentQuickTestInstance.skipTest(testType); + } +} + +function addTestBack(testType) { + if (currentQuickTestInstance) { + currentQuickTestInstance.addTestBack(testType); + } +} + // Legacy compatibility - expose functions to window for HTML onclick handlers window.markTestResult = markTestResult; window.resetAllTests = resetAllTests; -window.resetButtonsTest = resetButtonsTest; \ No newline at end of file +window.resetButtonsTest = resetButtonsTest; +window.skipTest = skipTest; +window.addTestBack = addTestBack; \ No newline at end of file diff --git a/templates/quick-test-modal.html b/templates/quick-test-modal.html index 47677d2..409608a 100644 --- a/templates/quick-test-modal.html +++ b/templates/quick-test-modal.html @@ -14,192 +14,7 @@
    - -
    -

    - -

    -
    -
    -

    This test checks the reliability of the USB port.

    -

    Instructions: Wiggle the USB cable to see if the controller disconnects.

    -
    - - Be gentle to avoid damage. -
    -
    - - -
    -
    -
    -
    - - -
    -

    - -

    -
    -
    -

    This test checks all controller buttons by requiring you to press each button three times.

    -

    Instructions: Press each button until they turn green.

    - -
    -
    - -
    -
    - -
    - - The test will automatically pass when all buttons have turned green. -
    - -
    - - - -
    -
    -
    -
    - - -
    -

    - -

    -
    -
    -

    This test will activate the controller's vibration motors for 3 seconds.

    -

    Instructions: Feel for vibration in the controller.

    -
    - - -
    -
    -
    -
    - - -
    -

    - -

    -
    -
    -

    This test will enable heavy resistance on both L2 and R2 triggers.

    -

    Instructions: Press L2 and R2 triggers to feel the trigger resistance.

    -
    - - -
    -
    -
    -
    - - -
    -

    - -

    -
    -
    -

    This test will play a tone through the controller's built-in speaker.

    -

    Instructions: Listen for a tone from the controller speaker.

    -
    - - -
    -
    -
    -
    - - -
    -

    - -

    -
    -
    -

    This test will monitor the controller's microphone input levels.

    -

    Instructions: Blow gently into the controller's microphone. You should see the audio level indicator respond.

    - -
    - - -
    -
    -
    -
    +
    @@ -208,6 +23,14 @@