@jjlmoya/utils-hardware 1.18.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +3 -1
- package/src/entries.ts +7 -1
- package/src/index.ts +2 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/pagespeed_best_practices.test.ts +198 -0
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/batteryHealthEstimator/component.astro +3 -3
- package/src/tool/gamepadTest/component.astro +4 -3
- package/src/tool/gamepadVibrationTester/component.astro +3 -3
- package/src/tool/keyboardTest/component.astro +6 -1
- package/src/tool/monitorGhostingTest/bibliography.astro +14 -0
- package/src/tool/monitorGhostingTest/bibliography.ts +20 -0
- package/src/tool/monitorGhostingTest/component.astro +156 -0
- package/src/tool/monitorGhostingTest/entry.ts +29 -0
- package/src/tool/monitorGhostingTest/i18n/de.ts +293 -0
- package/src/tool/monitorGhostingTest/i18n/en.ts +293 -0
- package/src/tool/monitorGhostingTest/i18n/es.ts +293 -0
- package/src/tool/monitorGhostingTest/i18n/fr.ts +293 -0
- package/src/tool/monitorGhostingTest/i18n/id.ts +293 -0
- package/src/tool/monitorGhostingTest/i18n/it.ts +293 -0
- package/src/tool/monitorGhostingTest/i18n/ja.ts +293 -0
- package/src/tool/monitorGhostingTest/i18n/ko.ts +293 -0
- package/src/tool/monitorGhostingTest/i18n/nl.ts +293 -0
- package/src/tool/monitorGhostingTest/i18n/pl.ts +293 -0
- package/src/tool/monitorGhostingTest/i18n/pt.ts +293 -0
- package/src/tool/monitorGhostingTest/i18n/ru.ts +293 -0
- package/src/tool/monitorGhostingTest/i18n/sv.ts +293 -0
- package/src/tool/monitorGhostingTest/i18n/tr.ts +293 -0
- package/src/tool/monitorGhostingTest/i18n/zh.ts +293 -0
- package/src/tool/monitorGhostingTest/index.ts +9 -0
- package/src/tool/monitorGhostingTest/logic.ts +195 -0
- package/src/tool/monitorGhostingTest/monitor-ghosting-test.css +546 -0
- package/src/tool/monitorGhostingTest/seo.astro +15 -0
- package/src/tool/monitorGhostingTest/ui.ts +30 -0
- package/src/tool/mouseDoubleClickTest/bibliography.astro +14 -0
- package/src/tool/mouseDoubleClickTest/bibliography.ts +16 -0
- package/src/tool/mouseDoubleClickTest/component.astro +135 -0
- package/src/tool/mouseDoubleClickTest/entry.ts +29 -0
- package/src/tool/mouseDoubleClickTest/i18n/de.ts +274 -0
- package/src/tool/mouseDoubleClickTest/i18n/en.ts +274 -0
- package/src/tool/mouseDoubleClickTest/i18n/es.ts +274 -0
- package/src/tool/mouseDoubleClickTest/i18n/fr.ts +274 -0
- package/src/tool/mouseDoubleClickTest/i18n/id.ts +285 -0
- package/src/tool/mouseDoubleClickTest/i18n/it.ts +274 -0
- package/src/tool/mouseDoubleClickTest/i18n/ja.ts +274 -0
- package/src/tool/mouseDoubleClickTest/i18n/ko.ts +274 -0
- package/src/tool/mouseDoubleClickTest/i18n/nl.ts +274 -0
- package/src/tool/mouseDoubleClickTest/i18n/pl.ts +274 -0
- package/src/tool/mouseDoubleClickTest/i18n/pt.ts +274 -0
- package/src/tool/mouseDoubleClickTest/i18n/ru.ts +274 -0
- package/src/tool/mouseDoubleClickTest/i18n/sv.ts +274 -0
- package/src/tool/mouseDoubleClickTest/i18n/tr.ts +274 -0
- package/src/tool/mouseDoubleClickTest/i18n/zh.ts +274 -0
- package/src/tool/mouseDoubleClickTest/index.ts +9 -0
- package/src/tool/mouseDoubleClickTest/logic.ts +258 -0
- package/src/tool/mouseDoubleClickTest/mouse-double-click-test.css +488 -0
- package/src/tool/mouseDoubleClickTest/seo.astro +15 -0
- package/src/tool/mouseDoubleClickTest/ui.ts +26 -0
- package/src/tool/mousePollingTest/logic/RatonManager.ts +6 -6
- package/src/tool/toneGenerator/component.astro +7 -7
- package/src/tools.ts +3 -2
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
type MouseButtonId = 'left' | 'middle' | 'right' | 'back' | 'forward' | 'other';
|
|
2
|
+
|
|
3
|
+
interface ButtonState {
|
|
4
|
+
count: number;
|
|
5
|
+
lastTime: number;
|
|
6
|
+
suspicious: number;
|
|
7
|
+
fastestGap: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ClickSample {
|
|
11
|
+
button: MouseButtonId;
|
|
12
|
+
label: string;
|
|
13
|
+
gap: number;
|
|
14
|
+
suspicious: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface MouseDoubleClickElements {
|
|
18
|
+
target: HTMLElement | null;
|
|
19
|
+
total: HTMLElement | null;
|
|
20
|
+
suspicious: HTMLElement | null;
|
|
21
|
+
fastest: HTMLElement | null;
|
|
22
|
+
score: HTMLElement | null;
|
|
23
|
+
status: HTMLElement | null;
|
|
24
|
+
threshold: HTMLInputElement | null;
|
|
25
|
+
thresholdValue: HTMLElement | null;
|
|
26
|
+
lastGap: HTMLElement | null;
|
|
27
|
+
log: HTMLElement | null;
|
|
28
|
+
reset: HTMLElement | null;
|
|
29
|
+
buttons: NodeListOf<HTMLElement>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface MouseDoubleClickLabels {
|
|
33
|
+
statusIdle: string;
|
|
34
|
+
statusClean: string;
|
|
35
|
+
statusWarning: string;
|
|
36
|
+
emptyLog: string;
|
|
37
|
+
leftButton: string;
|
|
38
|
+
middleButton: string;
|
|
39
|
+
rightButton: string;
|
|
40
|
+
backButton: string;
|
|
41
|
+
forwardButton: string;
|
|
42
|
+
otherButton: string;
|
|
43
|
+
thresholdUnit: string;
|
|
44
|
+
cleanEvent: string;
|
|
45
|
+
suspiciousEvent: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const BUTTON_FROM_CODE: Record<number, MouseButtonId> = {
|
|
49
|
+
0: 'left',
|
|
50
|
+
1: 'middle',
|
|
51
|
+
2: 'right',
|
|
52
|
+
3: 'back',
|
|
53
|
+
4: 'forward',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export class MouseDoubleClickTester {
|
|
57
|
+
private threshold = 80;
|
|
58
|
+
private samples: ClickSample[] = [];
|
|
59
|
+
private readonly states = new Map<MouseButtonId, ButtonState>();
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
private readonly elements: MouseDoubleClickElements,
|
|
63
|
+
private readonly labels: MouseDoubleClickLabels,
|
|
64
|
+
) {
|
|
65
|
+
this.threshold = Number(elements.threshold?.value ?? 80);
|
|
66
|
+
this.bindEvents();
|
|
67
|
+
this.render();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private bindEvents() {
|
|
71
|
+
this.elements.target?.addEventListener('pointerdown', (event) => this.recordPointer(event));
|
|
72
|
+
this.elements.target?.addEventListener('contextmenu', (event) => event.preventDefault());
|
|
73
|
+
this.elements.target?.addEventListener('auxclick', (event) => event.preventDefault());
|
|
74
|
+
this.elements.threshold?.addEventListener('input', () => {
|
|
75
|
+
this.threshold = Number(this.elements.threshold?.value ?? 80);
|
|
76
|
+
this.recalculateSuspicion();
|
|
77
|
+
this.render();
|
|
78
|
+
});
|
|
79
|
+
this.elements.reset?.addEventListener('click', () => this.reset());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private recordPointer(event: PointerEvent) {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
|
|
85
|
+
const button = BUTTON_FROM_CODE[event.button] ?? 'other';
|
|
86
|
+
const now = performance.now();
|
|
87
|
+
const state = this.getButtonState(button);
|
|
88
|
+
const gap = state.lastTime > 0 ? now - state.lastTime : 0;
|
|
89
|
+
const suspicious = gap > 0 && gap <= this.threshold;
|
|
90
|
+
|
|
91
|
+
state.count++;
|
|
92
|
+
state.lastTime = now;
|
|
93
|
+
|
|
94
|
+
if (gap > 0) {
|
|
95
|
+
state.fastestGap = Math.min(state.fastestGap, gap);
|
|
96
|
+
if (suspicious) state.suspicious++;
|
|
97
|
+
this.samples.unshift({
|
|
98
|
+
button,
|
|
99
|
+
label: this.getButtonLabel(button),
|
|
100
|
+
gap,
|
|
101
|
+
suspicious,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.flashButton(button, suspicious);
|
|
106
|
+
this.render();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private getButtonState(button: MouseButtonId) {
|
|
110
|
+
const existing = this.states.get(button);
|
|
111
|
+
if (existing) return existing;
|
|
112
|
+
|
|
113
|
+
const state: ButtonState = {
|
|
114
|
+
count: 0,
|
|
115
|
+
lastTime: 0,
|
|
116
|
+
suspicious: 0,
|
|
117
|
+
fastestGap: Infinity,
|
|
118
|
+
};
|
|
119
|
+
this.states.set(button, state);
|
|
120
|
+
return state;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private recalculateSuspicion() {
|
|
124
|
+
for (const state of this.states.values()) {
|
|
125
|
+
state.suspicious = 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.samples = this.samples.map((sample) => {
|
|
129
|
+
const suspicious = sample.gap <= this.threshold;
|
|
130
|
+
if (suspicious) {
|
|
131
|
+
this.getButtonState(sample.button).suspicious++;
|
|
132
|
+
}
|
|
133
|
+
return { ...sample, suspicious };
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private reset() {
|
|
138
|
+
this.states.clear();
|
|
139
|
+
this.samples = [];
|
|
140
|
+
this.elements.buttons.forEach((button) => {
|
|
141
|
+
button.dataset.count = '0';
|
|
142
|
+
button.dataset.state = '';
|
|
143
|
+
});
|
|
144
|
+
this.render();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private getTotals() {
|
|
148
|
+
let total = 0;
|
|
149
|
+
let suspicious = 0;
|
|
150
|
+
let fastestGap = Infinity;
|
|
151
|
+
|
|
152
|
+
for (const state of this.states.values()) {
|
|
153
|
+
total += state.count;
|
|
154
|
+
suspicious += state.suspicious;
|
|
155
|
+
fastestGap = Math.min(fastestGap, state.fastestGap);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { total, suspicious, fastestGap };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private getScore() {
|
|
162
|
+
const { total, suspicious } = this.getTotals();
|
|
163
|
+
if (total < 2) return 100;
|
|
164
|
+
const cleanRatio = 1 - suspicious / Math.max(total - 1, 1);
|
|
165
|
+
return Math.max(0, Math.round(cleanRatio * 100));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private render() {
|
|
169
|
+
const score = this.getScore();
|
|
170
|
+
const totals = this.getTotals();
|
|
171
|
+
const lastSample = this.samples[0];
|
|
172
|
+
|
|
173
|
+
this.renderMetrics(score, totals, lastSample);
|
|
174
|
+
this.renderButtonCounts();
|
|
175
|
+
this.renderStatus(totals.total, totals.suspicious);
|
|
176
|
+
this.renderLog();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private renderMetrics(score: number, totals: ReturnType<MouseDoubleClickTester['getTotals']>, lastSample: ClickSample | undefined) {
|
|
180
|
+
this.setText(this.elements.total, String(totals.total));
|
|
181
|
+
this.setText(this.elements.suspicious, String(totals.suspicious));
|
|
182
|
+
this.setText(this.elements.fastest, this.formatGap(totals.fastestGap));
|
|
183
|
+
this.setText(this.elements.score, `${score}%`);
|
|
184
|
+
this.setText(this.elements.thresholdValue, String(this.threshold));
|
|
185
|
+
this.setText(this.elements.lastGap, lastSample ? `${lastSample.label} ${this.formatGap(lastSample.gap)}` : '--');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private setText(element: HTMLElement | null, value: string) {
|
|
189
|
+
if (element) element.textContent = value;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private formatGap(gap: number) {
|
|
193
|
+
return Number.isFinite(gap) ? `${Math.round(gap)} ${this.labels.thresholdUnit}` : '--';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private renderButtonCounts() {
|
|
197
|
+
this.elements.buttons.forEach((element) => {
|
|
198
|
+
const button = (element.dataset.button ?? 'other') as MouseButtonId;
|
|
199
|
+
const state = this.states.get(button);
|
|
200
|
+
element.dataset.count = String(state?.count ?? 0);
|
|
201
|
+
element.dataset.state = state && state.suspicious > 0 ? 'warning' : '';
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private renderStatus(total: number, suspicious: number) {
|
|
206
|
+
if (!this.elements.status) return;
|
|
207
|
+
|
|
208
|
+
if (total < 2) {
|
|
209
|
+
this.elements.status.textContent = this.labels.statusIdle;
|
|
210
|
+
this.elements.status.dataset.state = 'clean';
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.elements.status.textContent = suspicious > 0 ? this.labels.statusWarning : this.labels.statusClean;
|
|
215
|
+
this.elements.status.dataset.state = suspicious > 0 ? 'warning' : 'clean';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private renderLog() {
|
|
219
|
+
if (!this.elements.log) return;
|
|
220
|
+
|
|
221
|
+
if (this.samples.length === 0) {
|
|
222
|
+
this.elements.log.innerHTML = `<li class="mdct-empty">${this.labels.emptyLog}</li>`;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.elements.log.innerHTML = this.samples
|
|
227
|
+
.slice(0, 14)
|
|
228
|
+
.map((sample) => {
|
|
229
|
+
const state = sample.suspicious ? 'suspect' : 'clean';
|
|
230
|
+
const label = sample.suspicious ? this.labels.suspiciousEvent : this.labels.cleanEvent;
|
|
231
|
+
return `<li class="mdct-log-item ${state}"><b>${sample.label}</b><span>${Math.round(sample.gap)} ${this.labels.thresholdUnit}</span><em>${label}</em></li>`;
|
|
232
|
+
})
|
|
233
|
+
.join('');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private flashButton(button: MouseButtonId, suspicious: boolean) {
|
|
237
|
+
this.elements.target?.setAttribute('data-flash', suspicious ? 'warning' : 'clean');
|
|
238
|
+
const visualButton = Array.from(this.elements.buttons).find((element) => element.dataset.button === button);
|
|
239
|
+
if (visualButton) visualButton.dataset.flash = suspicious ? 'warning' : 'clean';
|
|
240
|
+
|
|
241
|
+
window.setTimeout(() => {
|
|
242
|
+
this.elements.target?.setAttribute('data-flash', '');
|
|
243
|
+
if (visualButton) visualButton.dataset.flash = '';
|
|
244
|
+
}, 180);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private getButtonLabel(button: MouseButtonId) {
|
|
248
|
+
const labels: Record<MouseButtonId, string> = {
|
|
249
|
+
left: this.labels.leftButton,
|
|
250
|
+
middle: this.labels.middleButton,
|
|
251
|
+
right: this.labels.rightButton,
|
|
252
|
+
back: this.labels.backButton,
|
|
253
|
+
forward: this.labels.forwardButton,
|
|
254
|
+
other: this.labels.otherButton,
|
|
255
|
+
};
|
|
256
|
+
return labels[button];
|
|
257
|
+
}
|
|
258
|
+
}
|