Add button tests to Quick Test modal

This commit is contained in:
Mathias Malmqvist
2025-09-23 02:17:45 +02:00
committed by dualshock-tools
parent 3bc5c0eb34
commit b3712a24c2
5 changed files with 474 additions and 62 deletions

View File

@@ -51,18 +51,26 @@ dl.row dd {
}
/* Animation classes for different test types */
.test-icon-haptic {
animation: shake 0.5s ease-in-out infinite;
i.fas.test-icon-usb {
animation: pulse 1s ease-in-out infinite !important;
}
.test-icon-adaptive {
animation: pulse 1s ease-in-out infinite;
i.fas.test-icon-buttons {
animation: bounce 0.6s ease-in-out infinite !important;
}
.test-icon-speaker {
animation: bounce 0.6s ease-in-out infinite;
i.fas.test-icon-haptic {
animation: shake 0.5s ease-in-out infinite !important;
}
.test-icon-microphone {
animation: glow 1.5s ease-in-out infinite;
i.fas.test-icon-adaptive {
animation: pulse 1s ease-in-out infinite !important;
}
i.fas.test-icon-speaker {
animation: bounce 0.6s ease-in-out infinite !important;
}
i.fas.test-icon-microphone {
animation: glow 1.5s ease-in-out infinite !important;
}

View File

@@ -150,7 +150,7 @@
<button type="button" class="btn btn-primary ds-btn ds-i18n" onclick="calibrate_range()">Calibrate stick range</button>
<button type="button" class="btn btn-primary ds-btn" onclick="ds5_finetune()" id="ds5finetune"><span class="ds-i18n">Finetune stick calibration</span> <i id="ds-i18n">(beta)</i></button>
<button id="btnmcs" type="button" class="btn btn-outline-secondary ds-btn ds-i18n" onclick="auto_calibrate_stick_centers()">Fast calibrate stick center (OLD)</button>
<button type="button" class="btn btn-outline-info ds-btn ds-i18n" onclick="show_quick_test_modal()" id="quick-test-btn" style="display: none;">
<button type="button" class="btn btn-info ds-btn ds-i18n" onclick="show_quick_test_modal()" id="quick-test-btn" style="display: none;">
<i class="fas fa-vial" id="quick-test-icon"></i>&nbsp;&nbsp;
<span id="quick-test-text">Quick Test</span>
</button>

View File

@@ -668,6 +668,7 @@ function handleControllerInput({ changes, inputConfig, touchPoints, batteryStatu
// Handle Quick Test Modal input (can be open from any tab)
if (isQuickTestVisible()) {
quicktest_handle_controller_input(changes);
return;
}
const current_active_tab = get_current_main_tab();
@@ -985,7 +986,11 @@ window.welcome_accepted = welcome_accepted;
window.show_donate_modal = show_donate_modal;
window.board_model_info = board_model_info;
window.edge_color_info = edge_color_info;
window.show_quick_test_modal = () => show_quick_test_modal(controller, { l });
window.show_quick_test_modal = () => {
show_quick_test_modal(controller, { l }).catch(error => {
throw new Error("Failed to show quick test modal", { cause: error });
});
};
// Auto-initialize the application when the module loads
gboot();

View File

@@ -2,14 +2,37 @@
const ACCORDION_ELEMENTS = [
'usb-test-collapse',
'buttons-test-collapse',
'haptic-test-collapse',
'adaptive-test-collapse',
'speaker-test-collapse',
'microphone-test-collapse'
];
const TEST_SEQUENCE = ['usb', 'haptic', 'adaptive', 'speaker', 'microphone'];
const TEST_SEQUENCE = ['usb', 'buttons', 'haptic', 'adaptive', 'speaker', '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 = {
'triangle': 'qt-Triangle_infill',
'cross': 'qt-Cross_infill',
'circle': 'qt-Circle_infill',
'square': 'qt-Square_infill',
'l1': 'qt-L1_infill',
'r1': 'qt-R1_infill',
'l2': 'qt-L2_infill',
'r2': 'qt-R2_infill',
'l3': 'qt-L3_infill',
'r3': 'qt-R3_infill',
'up': 'qt-Up_infill',
'down': 'qt-Down_infill',
'left': 'qt-Left_infill',
'right': 'qt-Right_infill',
'create': 'qt-Create_infill',
'touchpad': 'qt-Trackpad_infill',
'options': 'qt-Options_infill',
'ps': 'qt-PS_infill',
'mute': 'qt-Mute_infill'
};
/**
* Quick Test Modal Class
* Handles controller feature testing including haptic feedback, adaptive triggers, speaker, and microphone functionality
@@ -19,16 +42,7 @@ export class QuickTestModal {
this.controller = controllerInstance;
this.l = l;
// Test state
this.state = {
haptic: null,
adaptive: null,
speaker: null,
microphone: null,
microphoneStream: null,
microphoneContext: null,
microphoneMonitoring: false
};
this.resetAllTests();
// Bind event handlers to maintain proper context
this._boundAccordionShown = (event) => this._handleAccordionShown(event);
@@ -42,6 +56,24 @@ export class QuickTestModal {
this._initEventListeners();
}
_initializeState() {
this.state = {
usb: null,
buttons: null,
haptic: null,
adaptive: null,
speaker: null,
microphone: null,
microphoneStream: null,
microphoneContext: null,
microphoneMonitoring: false,
buttonPressCount: {},
longPressTimers: {},
longPressThreshold: 400,
isTransitioning: false,
};
}
/**
* Start icon animation for a specific test type
*/
@@ -68,7 +100,9 @@ export class QuickTestModal {
const activeTest = this._getCurrentActiveTest();
const allTestsCompleted = this._areAllTestsCompleted();
if (activeTest) {
if (activeTest === 'buttons') {
$instructionsText.html(this.l('Test all buttons, or long-press <kbd>Square</kbd> to Pass and <kbd>Cross</kbd> to Fail'));
} else if (activeTest) {
$instructionsText.html(this.l('Press <kbd>Square</kbd> to Pass or <kbd>Cross</kbd> to Fail'));
} else if (allTestsCompleted) {
$instructionsText.html(this.l('Press <kbd>Circle</kbd> to close, or <kbd>Square</kbd> to start over'));
@@ -122,10 +156,91 @@ export class QuickTestModal {
/**
* Open the Quick Test modal
*/
open() {
async open() {
await this._initSvgController();
bootstrap.Modal.getOrCreateInstance('#quickTestModal').show();
}
/**
* Initialize SVG controller for the quick test modal
*/
async _initSvgController() {
const svgContainer = document.getElementById('quick-test-controller-svg-placeholder');
if (!svgContainer) {
console.warn('Quick test SVG container not found');
return;
}
let svgContent;
// Check if we have bundled assets (production mode)
if (window.BUNDLED_ASSETS && window.BUNDLED_ASSETS.svg && window.BUNDLED_ASSETS.svg['dualshock-controller.svg']) {
svgContent = window.BUNDLED_ASSETS.svg['dualshock-controller.svg'];
} else {
// Fallback to fetching from server (development mode)
const response = await fetch('assets/dualshock-controller.svg');
if (!response.ok) {
throw new Error('Failed to load controller SVG');
}
svgContent = await response.text();
}
// Modify SVG content to use unique IDs for the quick test modal
svgContent = svgContent.replace(/id="([^"]+)"/g, 'id="qt-$1"');
svgContainer.innerHTML = svgContent;
// Apply initial styling to the SVG
const svg = svgContainer.querySelector('svg');
if (svg) {
svg.id = 'qt-controller-svg';
svg.style.width = '100%';
svg.style.height = 'auto';
}
// Store reference to the SVG container for scoped queries
this.svgContainer = svgContainer;
const lightBlue = '#7ecbff';
const midBlue = '#3399cc';
const dualshock = this._getQuickTestElement('qt-Controller');
this._setSvgGroupColor(dualshock, lightBlue);
['qt-Button_outlines', 'qt-L3_outline', 'qt-R3_outline', 'qt-Trackpad_outline'].forEach(id => {
const group = this._getQuickTestElement(id);
this._setSvgGroupColor(group, midBlue);
});
this._resetButtonColors();
}
/**
* Get element from the quick test modal's SVG (scoped to avoid conflicts with main page)
*/
_getQuickTestElement(id) {
if (!this.svgContainer) {
return null;
}
return this.svgContainer.querySelector(`#${id}`);
}
/**
* Set color for SVG group elements
*/
_setSvgGroupColor(group, color) {
if (group) {
const elements = group.querySelectorAll('path,rect,circle,ellipse,line,polyline,polygon');
elements.forEach(el => {
// Set up a smooth transition for fill and stroke if not already set
if (!el.style.transition) {
el.style.transition = 'fill 0.10s, stroke 0.10s';
}
el.setAttribute('fill', color);
el.setAttribute('stroke', color);
});
}
}
/**
* Handle accordion section being shown (expanded)
*/
@@ -140,6 +255,12 @@ export class QuickTestModal {
// Small delay to ensure UI is fully expanded
setTimeout(() => {
switch (testType) {
case 'usb':
// USB test is manual - no auto-start needed
break;
case 'buttons':
this._startButtonsTest();
break;
case 'haptic':
this._startHapticTest();
break;
@@ -165,6 +286,12 @@ export class QuickTestModal {
// Stop ongoing tests when section is collapsed
switch (testType) {
case 'usb':
// USB test is manual - no stop needed
break;
case 'buttons':
this._stopButtonsTest();
break;
case 'adaptive':
this._stopAdaptiveTest();
break;
@@ -179,6 +306,111 @@ export class QuickTestModal {
}, 300);
}
/**
* Start buttons test
*/
_startButtonsTest() {
this._startIconAnimation('buttons');
// Initialize button press counts only if not already initialized
if (!this.state.buttonPressCount || Object.keys(this.state.buttonPressCount).length === 0) {
this.state.buttonPressCount = {};
BUTTONS.forEach(button => {
this.state.buttonPressCount[button] = 0;
});
}
// Check for any buttons that are already stuck pressed when the test starts
// and draw them as pressed
BUTTONS.forEach(button => {
if (this.controller.button_states[button] === true) {
this._setButtonPressed(button, true);
}
});
}
/**
* Stop buttons test
*/
_stopButtonsTest() {
this._stopIconAnimation('buttons');
// Clear any active long-press timers
this._clearAllLongPressTimers();
}
/**
* Reset all button colors to light blue
*/
_resetButtonColors() {
Object.keys(BUTTON_INFILL_MAPPING).forEach(button => {
const buttonElement = this._getQuickTestElement(BUTTON_INFILL_MAPPING[button]);
this._setSvgGroupColor(buttonElement, 'orange');
});
}
/**
* Update button color based on press count
*/
_updateButtonColor(button) {
const count = this.state.buttonPressCount[button] || 0;
const buttonElement = this._getQuickTestElement(BUTTON_INFILL_MAPPING[button]);
if (buttonElement) {
let color;
// Special buttons (create, options, mute, ps) go straight to green on first press
if (['create', 'options', 'mute', 'ps'].includes(button)) {
color = ['orange'][count] || '#16c016ff';
} else {
// Other buttons follow the 3-press sequence
color = ['orange', '#a5c9fcff', '#287ffaff'][count] || '#16c016ff';
}
this._setSvgGroupColor(buttonElement, color);
}
}
/**
* Check if all buttons have been pressed the required number of times
*/
_checkButtonsTestComplete() {
const allPressed = BUTTONS.every(button => {
const count = this.state.buttonPressCount[button] || 0;
// Special buttons (create, options, mute, ps) only need 1 press
const isSpecialButton = ['create', 'options', 'mute', 'ps'].includes(button);
return isSpecialButton ? count >= 1 : count >= 3;
});
if (allPressed) {
// Auto-pass the test
setTimeout(() => {
this.markTestResult('buttons', true);
}, 500);
}
}
/**
* Reset the buttons test to initial state
*/
resetButtonsTest() {
// Reset button press counts
this.state.buttonPressCount = {};
BUTTONS.forEach(button => {
this.state.buttonPressCount[button] = 0;
});
// Clear any active long-press timers
this._clearAllLongPressTimers();
// Reset all button colors to orange (initial state)
this._resetButtonColors();
// Check for any buttons that are already stuck pressed and draw them as pressed
BUTTONS.forEach(button => {
if (this.controller.button_states[button] === true) {
this._setButtonPressed(button, true);
}
});
}
/**
* Start haptic vibration test
*/
@@ -209,7 +441,7 @@ export class QuickTestModal {
*/
async _startSpeakerTest() {
this._startIconAnimation('speaker');
await this.controller.setSpeakerTone(100);
await this.controller.setSpeakerTone(300);
setTimeout(() => { this._stopIconAnimation('speaker'); }, 1000);
}
@@ -373,12 +605,13 @@ export class QuickTestModal {
}
});
const numTests = TEST_SEQUENCE.length;
if (completed === 0) {
$summary.text(this.l('No tests completed yet.'));
$summary.attr('class', 'text-muted ds-i18n');
} else {
$summary.text(this.l(`${completed}/4 tests completed. ${passed} passed, ${completed - passed} failed.`));
$summary.attr('class', completed === 4 ? 'text-success' : 'text-info');
$summary.text(this.l(`${completed}/${numTests} tests completed. ${passed} passed, ${completed - passed} failed.`));
$summary.attr('class', completed === numTests ? 'text-success' : 'text-info');
}
}
@@ -425,31 +658,149 @@ export class QuickTestModal {
* Handle controller input for test navigation and control
*/
handleControllerInput(changes) {
if(this.state.isTransitioning) return;
const activeTest = this._getCurrentActiveTest();
// Handle Cross button (Start test sequence OR mark test as passed)
// If buttons test is active, track button presses
if (activeTest === 'buttons') {
this._trackButtonPresses(changes);
return;
}
// Helper function to handle button press with transition
const handleButtonPress = (action) => {
this._setTransitioning();
action();
};
// Handle button presses
if (changes.square === true) {
if (!activeTest) {
this._startTestSequence();
handleButtonPress(() => {
if (!activeTest) {
this._startTestSequence();
} else {
this.markTestResult(activeTest, true);
}
});
} else if (activeTest && changes.cross === true) {
handleButtonPress(() => this.markTestResult(activeTest, false));
} else if (changes.triangle === true) {
handleButtonPress(() => this._moveToPreviousTest());
} else if (changes.circle === true) {
handleButtonPress(() => bootstrap.Modal.getOrCreateInstance('#quickTestModal').hide());
}
}
/**
* Set transitioning state to prevent rapid button presses
*/
_setTransitioning() {
this.state.isTransitioning = true;
setTimeout(() => {
this.state.isTransitioning = false;
}, 750);
}
/**
* Track button presses for the buttons test
*/
_trackButtonPresses(changes) {
BUTTONS.forEach(button => {
const handleLongpress = ['cross', 'square', 'triangle'].includes(button);
if (changes[button] === true) {
// Button pressed - increment count and show dark blue infill
this.state.buttonPressCount[button]++;
this._setButtonPressed(button, true);
// Start long-press timer for square and cross buttons
if (handleLongpress) {
this._startLongPressTimer(button);
}
} else if (changes[button] === false) {
// Button released - restore appropriate color based on press count
this._setButtonPressed(button, false);
// Clear long-press timer for square and cross buttons
if (handleLongpress) {
this._clearLongPressTimer(button);
}
}
});
// Check if test is complete
this._checkButtonsTestComplete();
}
/**
* Set button pressed state and update visual appearance
*/
_setButtonPressed(button, isPressed) {
const buttonElement = this._getQuickTestElement(BUTTON_INFILL_MAPPING[button]);
if (buttonElement) {
if (isPressed) {
// Show dark blue infill while pressed
this._setSvgGroupColor(buttonElement, 'rgba(0, 0, 120, 1)');
} else {
this.markTestResult(activeTest, true);
// Restore color based on press count when released
this._updateButtonColor(button);
}
}
}
/**
* Start long-press timer for a button
*/
_startLongPressTimer(button) {
if(this.state.isTransitioning) return;
// Clear any existing timer for this button
this._clearLongPressTimer(button);
// Start new timer
this.state.longPressTimers[button] = setTimeout(() => {
this._handleLongPress(button);
}, this.state.longPressThreshold);
}
/**
* Clear long-press timer for a button
*/
_clearLongPressTimer(button) {
if (this.state.longPressTimers[button]) {
clearTimeout(this.state.longPressTimers[button]);
delete this.state.longPressTimers[button];
}
}
/**
* Clear all active long-press timers
*/
_clearAllLongPressTimers() {
Object.keys(this.state.longPressTimers).forEach(button => {
this._clearLongPressTimer(button);
});
}
/**
* Handle long-press action for square and cross buttons during button test
*/
_handleLongPress(button) {
const activeTest = this._getCurrentActiveTest();
if (activeTest === 'buttons') {
this._setTransitioning();
if (button === 'square') {
this.markTestResult('buttons', true);
} else if (button === 'cross') {
this.markTestResult('buttons', false);
} else if (button === 'triangle') {
this._moveToPreviousTest();
}
}
// Handle Square button (Pass)
if (activeTest && changes.cross === true) {
this.markTestResult(activeTest, false);
}
// Handle Triangle button (Move to previous test)
if (changes.triangle === true) {
this._moveToPreviousTest();
}
// Handle Circle button (Close the modal)
if (changes.circle === true) {
bootstrap.Modal.getOrCreateInstance('#quickTestModal').hide();
}
// Clear the timer since it has been handled
delete this.state.longPressTimers[button];
}
/**
@@ -497,20 +848,19 @@ export class QuickTestModal {
*/
resetAllTests() {
// Reset state
this.state = {
haptic: null,
adaptive: null,
speaker: null,
microphone: null,
microphoneStream: null,
microphoneContext: null,
microphoneMonitoring: false
};
this._initializeState();
// Clear any active long-press timers before resetting state
this._clearAllLongPressTimers();
// Clean up any active tests
this._stopButtonsTest();
this._stopAdaptiveTest();
this._stopMicrophoneTest();
// Reset button colors to initial state
this._resetButtonColors();
// Reset UI
TEST_SEQUENCE.forEach(test => {
this._stopIconAnimation(test);
@@ -587,13 +937,13 @@ export function quicktest_handle_controller_input(changes) {
/**
* Show the Quick Test modal (legacy function for backward compatibility)
*/
export function show_quick_test_modal(controller, { l } = {}) {
export async function show_quick_test_modal(controller, { l } = {}) {
// Destroy any existing instance
destroyCurrentInstance();
// Create new instance
currentQuickTestInstance = new QuickTestModal(controller, { l });
currentQuickTestInstance.open();
await currentQuickTestInstance.open();
}
// Legacy function exports for backward compatibility (used by HTML onclick handlers)
@@ -609,6 +959,13 @@ function resetAllTests() {
}
}
function resetButtonsTest() {
if (currentQuickTestInstance) {
currentQuickTestInstance.resetButtonsTest();
}
}
// Legacy compatibility - expose functions to window for HTML onclick handlers
window.markTestResult = markTestResult;
window.resetAllTests = resetAllTests;
window.resetAllTests = resetAllTests;
window.resetButtonsTest = resetButtonsTest;

View File

@@ -12,14 +12,14 @@
<i class="fas fa-gamepad me-2"></i>
<span class="ds-i18n" id="quick-test-instructions-text">Press <kbd>Square</kbd> to begin</span>
</div>
<div class="accordion" id="quickTestAccordion">
<!-- USB Connector Test -->
<div class="accordion-item" id="usb-test-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#usb-test-collapse" aria-expanded="false" aria-controls="usb-test-collapse">
<div class="d-flex align-items-center w-100">
<i class="fas fa-usb me-3"></i>
<i class="fas fa-plug me-3 test-icon-usb"></i>
<span class="flex-grow-1 ds-i18n">USB Connector Test</span>
<span class="badge bg-secondary me-2" id="usb-test-status">Not tested</span>
</div>
@@ -45,12 +45,54 @@
</div>
</div>
<!-- Buttons Test -->
<div class="accordion-item" id="buttons-test-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#buttons-test-collapse" aria-expanded="false" aria-controls="buttons-test-collapse">
<div class="d-flex align-items-center w-100">
<i class="fas fa-gamepad me-3 test-icon-buttons"></i>
<span class="flex-grow-1 ds-i18n">Buttons Test</span>
<span class="badge bg-secondary me-2" id="buttons-test-status">Not tested</span>
</div>
</button>
</h2>
<div id="buttons-test-collapse" class="accordion-collapse collapse" data-bs-parent="#quickTestAccordion">
<div class="accordion-body">
<p class="ds-i18n">This test checks all controller buttons by requiring you to press each button three times.</p>
<p class="ds-i18n"><strong>Instructions:</strong> Press each button on the controller three times. Buttons will change from light blue to dark blue to green as you press them.</p>
<div class="d-flex justify-content-center mb-3">
<div style="width: 80%; max-width: 400px;" id="quick-test-controller-svg-placeholder">
<!-- SVG will be loaded dynamically -->
</div>
</div>
<div class="alert alert-info mb-3">
<i class="fas fa-info-circle me-2"></i>
<span class="ds-i18n">The test will automatically pass when all buttons turn green (pressed 3 times each).</span>
</div>
<div class="d-flex gap-2 mt-3">
<button type="button" class="btn btn-success" id="buttons-pass-btn" onclick="markTestResult('buttons', true)">
<i class="fas fa-check me-1"></i><span class="ds-i18n">Pass</span>
</button>
<button type="button" class="btn btn-danger" id="buttons-fail-btn" onclick="markTestResult('buttons', false)">
<i class="fas fa-times me-1"></i><span class="ds-i18n">Fail</span>
</button>
<button type="button" class="btn btn-outline-primary" id="buttons-reset-btn" onclick="resetButtonsTest()">
<i class="fas fa-redo me-1"></i><span class="ds-i18n">Restart</span>
</button>
</div>
</div>
</div>
</div>
<!-- Haptic Vibration Test -->
<div class="accordion-item" id="haptic-test-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#haptic-test-collapse" aria-expanded="false" aria-controls="haptic-test-collapse">
<div class="d-flex align-items-center w-100">
<i class="fas fa-mobile-alt me-3"></i>
<i class="fas fa-mobile-alt me-3 test-icon-haptic"></i>
<span class="flex-grow-1 ds-i18n">Haptic Vibration Test</span>
<span class="badge bg-secondary me-2" id="haptic-test-status">Not tested</span>
</div>
@@ -77,7 +119,7 @@
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#adaptive-test-collapse" aria-expanded="false" aria-controls="adaptive-test-collapse">
<div class="d-flex align-items-center w-100">
<i class="fas fa-hand-pointer me-3"></i>
<i class="fas fa-hand-pointer me-3 test-icon-adaptive"></i>
<span class="flex-grow-1 ds-i18n">Adaptive Trigger Test</span>
<span class="badge bg-secondary me-2" id="adaptive-test-status">Not tested</span>
</div>
@@ -104,7 +146,7 @@
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#speaker-test-collapse" aria-expanded="false" aria-controls="speaker-test-collapse">
<div class="d-flex align-items-center w-100">
<i class="fas fa-volume-up me-3"></i>
<i class="fas fa-volume-up me-3 test-icon-speaker"></i>
<span class="flex-grow-1 ds-i18n">Speaker Test</span>
<span class="badge bg-secondary me-2" id="speaker-test-status">Not tested</span>
</div>
@@ -131,7 +173,7 @@
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#microphone-test-collapse" aria-expanded="false" aria-controls="microphone-test-collapse">
<div class="d-flex align-items-center w-100">
<i class="fas fa-microphone me-3"></i>
<i class="fas fa-microphone me-3 test-icon-microphone"></i>
<span class="flex-grow-1 ds-i18n">Microphone Test</span>
<span class="badge bg-secondary me-2" id="microphone-test-status">Not tested</span>
</div>