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 $(`
+
+ `);
+ }
+
+ /**
+ * 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 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 @@