@jjlmoya/utils-hardware 1.18.0 → 1.19.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 +2 -1
- package/src/entries.ts +4 -1
- package/src/index.ts +1 -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/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 +2 -2
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { testTeclado } from '../tool/keyboardTest/index';
|
|
|
4
4
|
import { testMando } from '../tool/gamepadTest/index';
|
|
5
5
|
import { probadorVibracionMando } from '../tool/gamepadVibrationTester/index';
|
|
6
6
|
import { testRaton } from '../tool/mousePollingTest/index';
|
|
7
|
+
import { mouseDoubleClickTest } from '../tool/mouseDoubleClickTest/index';
|
|
7
8
|
import { estimadorSaludBateria } from '../tool/batteryHealthEstimator/index';
|
|
8
9
|
import { toneGenerator } from '../tool/toneGenerator/index';
|
|
9
10
|
import { refreshRateDetector } from '../tool/refreshRateDetector/index';
|
|
@@ -11,7 +12,7 @@ import { spectrumCanvas } from '../tool/colorAccuracyTest/index';
|
|
|
11
12
|
|
|
12
13
|
export const hardwareCategory: HardwareCategoryEntry = {
|
|
13
14
|
icon: 'mdi:memory',
|
|
14
|
-
tools: [pixelesPantalla, testTeclado, testMando, probadorVibracionMando, testRaton, estimadorSaludBateria, toneGenerator, refreshRateDetector, spectrumCanvas],
|
|
15
|
+
tools: [pixelesPantalla, testTeclado, testMando, probadorVibracionMando, testRaton, mouseDoubleClickTest, estimadorSaludBateria, toneGenerator, refreshRateDetector, spectrumCanvas],
|
|
15
16
|
i18n: {
|
|
16
17
|
en: () => import('./i18n/en').then((m) => m.content),
|
|
17
18
|
es: () => import('./i18n/es').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -10,6 +10,8 @@ export { testTeclado } from './tool/keyboardTest/entry';
|
|
|
10
10
|
export type { TestTecladoLocaleContent } from './tool/keyboardTest/entry';
|
|
11
11
|
export { testRaton } from './tool/mousePollingTest/entry';
|
|
12
12
|
export type { TestRatonLocaleContent } from './tool/mousePollingTest/entry';
|
|
13
|
+
export { mouseDoubleClickTest } from './tool/mouseDoubleClickTest/entry';
|
|
14
|
+
export type { MouseDoubleClickTestLocaleContent } from './tool/mouseDoubleClickTest/entry';
|
|
13
15
|
export { toneGenerator } from './tool/toneGenerator/entry';
|
|
14
16
|
export type { ToneGeneratorLocaleContent } from './tool/toneGenerator/entry';
|
|
15
17
|
export { refreshRateDetector } from './tool/refreshRateDetector/entry';
|
|
@@ -23,7 +25,8 @@ import { testMando } from './tool/gamepadTest/entry';
|
|
|
23
25
|
import { probadorVibracionMando } from './tool/gamepadVibrationTester/entry';
|
|
24
26
|
import { testTeclado } from './tool/keyboardTest/entry';
|
|
25
27
|
import { testRaton } from './tool/mousePollingTest/entry';
|
|
28
|
+
import { mouseDoubleClickTest } from './tool/mouseDoubleClickTest/entry';
|
|
26
29
|
import { toneGenerator } from './tool/toneGenerator/entry';
|
|
27
30
|
import { refreshRateDetector } from './tool/refreshRateDetector/entry';
|
|
28
31
|
import { spectrumCanvas } from './tool/colorAccuracyTest/entry';
|
|
29
|
-
export const ALL_ENTRIES = [estimadorSaludBateria, pixelesPantalla, testMando, probadorVibracionMando, testTeclado, testRaton, toneGenerator, refreshRateDetector, spectrumCanvas];
|
|
32
|
+
export const ALL_ENTRIES = [estimadorSaludBateria, pixelesPantalla, testMando, probadorVibracionMando, testTeclado, testRaton, mouseDoubleClickTest, toneGenerator, refreshRateDetector, spectrumCanvas];
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ export { TEST_TECLADO_TOOL } from './tool/keyboardTest/index';
|
|
|
22
22
|
export { TEST_MANDO_TOOL } from './tool/gamepadTest/index';
|
|
23
23
|
export { PROBADOR_VIBRACION_MANDO_TOOL } from './tool/gamepadVibrationTester/index';
|
|
24
24
|
export { TEST_RATON_TOOL } from './tool/mousePollingTest/index';
|
|
25
|
+
export { MOUSE_DOUBLE_CLICK_TEST_TOOL } from './tool/mouseDoubleClickTest/index';
|
|
25
26
|
export { ESTIMADOR_SALUD_BATERIA_TOOL } from './tool/batteryHealthEstimator/index';
|
|
26
27
|
export { TONE_GENERATOR_TOOL } from './tool/toneGenerator/index';
|
|
27
28
|
export { REFRESH_RATE_DETECTOR_TOOL } from './tool/refreshRateDetector/index';
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
3
|
+
import { join, relative } from 'path';
|
|
4
|
+
import { ALL_TOOLS } from '../tools';
|
|
5
|
+
import type { SEOSection, ToolLocaleContent } from '../types';
|
|
6
|
+
|
|
7
|
+
const srcDir = join(process.cwd(), 'src');
|
|
8
|
+
const toolDir = join(srcDir, 'tool');
|
|
9
|
+
const geometryReads = [
|
|
10
|
+
'offsetWidth',
|
|
11
|
+
'offsetHeight',
|
|
12
|
+
'offsetTop',
|
|
13
|
+
'offsetLeft',
|
|
14
|
+
'clientWidth',
|
|
15
|
+
'clientHeight',
|
|
16
|
+
'clientTop',
|
|
17
|
+
'clientLeft',
|
|
18
|
+
'scrollWidth',
|
|
19
|
+
'scrollHeight',
|
|
20
|
+
'scrollTop',
|
|
21
|
+
'scrollLeft',
|
|
22
|
+
'getBoundingClientRect',
|
|
23
|
+
'getClientRects',
|
|
24
|
+
'computedStyle',
|
|
25
|
+
'getComputedStyle',
|
|
26
|
+
];
|
|
27
|
+
const domWrites = [
|
|
28
|
+
'.style.',
|
|
29
|
+
'.classList.add',
|
|
30
|
+
'.classList.remove',
|
|
31
|
+
'.classList.toggle',
|
|
32
|
+
'.appendChild',
|
|
33
|
+
'.insertBefore',
|
|
34
|
+
'.prepend',
|
|
35
|
+
'.append',
|
|
36
|
+
'.remove',
|
|
37
|
+
'.innerHTML',
|
|
38
|
+
'.textContent',
|
|
39
|
+
'.setAttribute',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function findFiles(dir: string, extensions: string[]): string[] {
|
|
43
|
+
const files: string[] = [];
|
|
44
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
45
|
+
const fullPath = join(dir, entry.name);
|
|
46
|
+
if (entry.isDirectory()) files.push(...findFiles(fullPath, extensions));
|
|
47
|
+
else if (extensions.some((extension) => entry.name.endsWith(extension))) files.push(fullPath);
|
|
48
|
+
}
|
|
49
|
+
return files;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function relativePath(file: string): string {
|
|
53
|
+
return relative(process.cwd(), file).replace(/\\/g, '/');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function findFormControls(content: string, tagName: 'input' | 'select'): RegExpMatchArray[] {
|
|
57
|
+
return Array.from(content.matchAll(new RegExp(`<${tagName}\\b[^>]*>`, 'gi')));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function attrValue(tag: string, attr: string): string | null {
|
|
61
|
+
const match = tag.match(new RegExp(`\\b${attr}\\s*=\\s*(?:"([^"]+)"|'([^']+)'|\\{([^}]+)\\})`, 'i'));
|
|
62
|
+
return match?.[1] ?? match?.[2] ?? match?.[3] ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function booleanAttr(tag: string, attr: string): boolean {
|
|
66
|
+
return new RegExp(`\\b${attr}\\b`, 'i').test(tag);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function controlStartIndex(content: string, tag: RegExpMatchArray): number {
|
|
70
|
+
return tag.index ?? content.indexOf(tag[0]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hasWrappingLabel(content: string, tag: RegExpMatchArray): boolean {
|
|
74
|
+
const index = controlStartIndex(content, tag);
|
|
75
|
+
const before = content.slice(0, index);
|
|
76
|
+
const labelOpen = before.lastIndexOf('<label');
|
|
77
|
+
const labelClose = before.lastIndexOf('</label>');
|
|
78
|
+
const nextLabelClose = content.indexOf('</label>', index + tag[0].length);
|
|
79
|
+
return labelOpen > labelClose && nextLabelClose !== -1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function hasAccessibleName(content: string, tag: RegExpMatchArray): boolean {
|
|
83
|
+
const source = tag[0];
|
|
84
|
+
if (attrValue(source, 'aria-label')) return true;
|
|
85
|
+
if (attrValue(source, 'aria-labelledby')) return true;
|
|
86
|
+
const id = attrValue(source, 'id');
|
|
87
|
+
if (id && hasExplicitLabel(content, id)) return true;
|
|
88
|
+
return hasWrappingLabel(content, tag);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isVisuallyHiddenFileInput(tag: string): boolean {
|
|
92
|
+
const type = attrValue(tag, 'type')?.toLowerCase() ?? 'text';
|
|
93
|
+
const attributes = `${attrValue(tag, 'style') ?? ''} ${attrValue(tag, 'class') ?? ''}`.toLowerCase();
|
|
94
|
+
const hiddenPatterns = ['display:none', 'display: none', 'file-input'];
|
|
95
|
+
return type === 'file' && hiddenPatterns.some((pattern) => attributes.includes(pattern));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isIgnoredInput(tag: string): boolean {
|
|
99
|
+
const type = attrValue(tag, 'type')?.toLowerCase() ?? 'text';
|
|
100
|
+
return ['hidden', 'button', 'submit', 'reset'].includes(type) || booleanAttr(tag, 'aria-hidden') || isVisuallyHiddenFileInput(tag);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function controlFailures(content: string, tagName: 'input' | 'select'): string[] {
|
|
104
|
+
return findFormControls(content, tagName)
|
|
105
|
+
.filter((tag) => tagName !== 'input' || !isIgnoredInput(tag[0]))
|
|
106
|
+
.filter((tag) => !hasAccessibleName(content, tag))
|
|
107
|
+
.map((tag) => tag[0]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function explicitLabelMessage(tagName: string, path: string, failures: string[]): string {
|
|
111
|
+
return `${tagName} controls without label, wrapping label, aria-label or aria-labelledby in ${path}:\n${failures.join('\n')}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function hasExplicitLabel(content: string, id: string): boolean {
|
|
115
|
+
const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
116
|
+
return (
|
|
117
|
+
new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*["']${escapedId}["'][^>]*>`, 'i').test(content)
|
|
118
|
+
|| new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*\\{${escapedId}\\}[^>]*>`, 'i').test(content)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function headingLevels(sections: SEOSection[]): number[] {
|
|
123
|
+
return sections
|
|
124
|
+
.filter((section) => section.type === 'title')
|
|
125
|
+
.map((section) => Number('level' in section ? section.level : 0))
|
|
126
|
+
.filter((level) => Number.isInteger(level) && level > 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function findHeadingLevelJumps(levels: number[]): string[] {
|
|
130
|
+
const failures: string[] = [];
|
|
131
|
+
levels.forEach((level, index) => {
|
|
132
|
+
const previous = index === 0 ? 1 : levels[index - 1];
|
|
133
|
+
if (previous && level > previous + 1) {
|
|
134
|
+
failures.push(`h${previous} -> h${level}`);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return failures;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function hasDomWriteBeforeGeometryRead(content: string): boolean {
|
|
141
|
+
const normalized = content.replace(/\s+/g, ' ');
|
|
142
|
+
return domWrites.some((write) => {
|
|
143
|
+
const writeIndex = normalized.indexOf(write);
|
|
144
|
+
if (writeIndex === -1) return false;
|
|
145
|
+
return geometryReads.some((read) => normalized.indexOf(read, writeIndex + write.length) !== -1);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
describe('PageSpeed best-practice guards', () => {
|
|
150
|
+
const astroToolFiles = findFiles(toolDir, ['.astro']);
|
|
151
|
+
const scriptFiles = findFiles(toolDir, ['.astro', '.ts', '.js']);
|
|
152
|
+
|
|
153
|
+
astroToolFiles.forEach((file) => {
|
|
154
|
+
const displayPath = relativePath(file);
|
|
155
|
+
|
|
156
|
+
it(`${displayPath} labels every input with an explicit label`, () => {
|
|
157
|
+
const content = readFileSync(file, 'utf-8');
|
|
158
|
+
const failures = controlFailures(content, 'input');
|
|
159
|
+
|
|
160
|
+
expect(failures, explicitLabelMessage('Input', displayPath, failures)).toEqual([]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it(`${displayPath} labels every select with an explicit label`, () => {
|
|
164
|
+
const content = readFileSync(file, 'utf-8');
|
|
165
|
+
const failures = controlFailures(content, 'select');
|
|
166
|
+
|
|
167
|
+
expect(failures, explicitLabelMessage('Select', displayPath, failures)).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
ALL_TOOLS.forEach((tool) => {
|
|
172
|
+
Object.entries(tool.entry.i18n).forEach(([locale, loader]) => {
|
|
173
|
+
it(`${tool.entry.id}/${locale} keeps SEO headings sequential`, async () => {
|
|
174
|
+
if (!loader) return;
|
|
175
|
+
const content = (await loader()) as ToolLocaleContent;
|
|
176
|
+
const levels = headingLevels(content.seo);
|
|
177
|
+
const failures = findHeadingLevelJumps(levels);
|
|
178
|
+
|
|
179
|
+
expect(
|
|
180
|
+
failures,
|
|
181
|
+
`SEO headings in ${tool.entry.id}/${locale} skip levels: ${failures.join(', ')}`,
|
|
182
|
+
).toEqual([]);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
scriptFiles.forEach((file) => {
|
|
188
|
+
const displayPath = relativePath(file);
|
|
189
|
+
|
|
190
|
+
it(`${displayPath} avoids static forced-reflow patterns`, () => {
|
|
191
|
+
const content = readFileSync(file, 'utf-8');
|
|
192
|
+
expect(
|
|
193
|
+
hasDomWriteBeforeGeometryRead(content),
|
|
194
|
+
`${displayPath} appears to read layout geometry after DOM/style mutations. Split writes and reads across frames or measure before mutating.`,
|
|
195
|
+
).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -4,8 +4,8 @@ import { hardwareCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 10 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(10);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('hardwareCategory should be defined', () => {
|
|
@@ -37,7 +37,7 @@ const S = JSON.stringify({
|
|
|
37
37
|
<div class="bh-sliders">
|
|
38
38
|
<div class="bh-slider-group">
|
|
39
39
|
<div class="bh-slider-header">
|
|
40
|
-
<label class="bh-slider-label">{t.voltageLabel}</label>
|
|
40
|
+
<label for="bh-v" class="bh-slider-label">{t.voltageLabel}</label>
|
|
41
41
|
<span id="bh-v-val" class="bh-slider-val">3.70V</span>
|
|
42
42
|
</div>
|
|
43
43
|
<input type="range" id="bh-v" class="bh-slider" min="3.0" max="4.2" step="0.01" value="3.70" />
|
|
@@ -46,7 +46,7 @@ const S = JSON.stringify({
|
|
|
46
46
|
|
|
47
47
|
<div class="bh-slider-group">
|
|
48
48
|
<div class="bh-slider-header">
|
|
49
|
-
<label class="bh-slider-label">{t.cyclesLabel}</label>
|
|
49
|
+
<label for="bh-c" class="bh-slider-label">{t.cyclesLabel}</label>
|
|
50
50
|
<span id="bh-c-val" class="bh-slider-val">50</span>
|
|
51
51
|
</div>
|
|
52
52
|
<input type="range" id="bh-c" class="bh-slider" min="0" max="1000" step="1" value="50" />
|
|
@@ -54,7 +54,7 @@ const S = JSON.stringify({
|
|
|
54
54
|
|
|
55
55
|
<div class="bh-slider-group">
|
|
56
56
|
<div class="bh-slider-header">
|
|
57
|
-
<label class="bh-slider-label">{t.tempLabel}</label>
|
|
57
|
+
<label for="bh-t" class="bh-slider-label">{t.tempLabel}</label>
|
|
58
58
|
<span id="bh-t-val" class="bh-slider-val">25°C</span>
|
|
59
59
|
</div>
|
|
60
60
|
<input type="range" id="bh-t" class="bh-slider" min="0" max="60" step="1" value="25" />
|
|
@@ -341,11 +341,12 @@ const t = (ui ?? {}) as TestMandoUI;
|
|
|
341
341
|
(targetBtn as HTMLElement).style.background = currentTarget.color;
|
|
342
342
|
(targetBtn as HTMLElement).style.color = 'white';
|
|
343
343
|
targetBtn.classList.remove('tm-target-hidden', 'tm-target-pop');
|
|
344
|
-
void targetBtn.offsetWidth;
|
|
345
|
-
targetBtn.classList.add('tm-target-pop');
|
|
346
344
|
targetLabel.textContent = currentTarget.label;
|
|
347
345
|
targetSub.textContent = currentTarget.type === 'axis' ? LABEL_MOVE : LABEL_PRESS;
|
|
348
|
-
|
|
346
|
+
requestAnimationFrame(() => {
|
|
347
|
+
targetBtn?.classList.add('tm-target-pop');
|
|
348
|
+
setTimeout(() => targetBtn?.classList.remove('tm-target-pop'), 300);
|
|
349
|
+
});
|
|
349
350
|
}
|
|
350
351
|
function startTimer() {
|
|
351
352
|
timeLeft = 30;
|
|
@@ -113,7 +113,7 @@ const t = (ui ?? {}) as ProbadorVibracionMandoUI;
|
|
|
113
113
|
|
|
114
114
|
<div class="pv-slider-group">
|
|
115
115
|
<div class="pv-slider-header">
|
|
116
|
-
<
|
|
116
|
+
<label for="pv-inp-str">{t.customStrongLabel}</label>
|
|
117
117
|
<span id="pv-sl-str" class="pv-slider-val">100%</span>
|
|
118
118
|
</div>
|
|
119
119
|
<input type="range" class="pv-slider" id="pv-inp-str" min="0" max="1" step="0.05" value="1.0" />
|
|
@@ -121,7 +121,7 @@ const t = (ui ?? {}) as ProbadorVibracionMandoUI;
|
|
|
121
121
|
|
|
122
122
|
<div class="pv-slider-group">
|
|
123
123
|
<div class="pv-slider-header">
|
|
124
|
-
<
|
|
124
|
+
<label for="pv-inp-wk">{t.customWeakLabel}</label>
|
|
125
125
|
<span id="pv-sl-wk" class="pv-slider-val">100%</span>
|
|
126
126
|
</div>
|
|
127
127
|
<input type="range" class="pv-slider" id="pv-inp-wk" min="0" max="1" step="0.05" value="1.0" />
|
|
@@ -129,7 +129,7 @@ const t = (ui ?? {}) as ProbadorVibracionMandoUI;
|
|
|
129
129
|
|
|
130
130
|
<div class="pv-slider-group">
|
|
131
131
|
<div class="pv-slider-header">
|
|
132
|
-
<
|
|
132
|
+
<label for="pv-inp-dur">{t.customDurationLabel}</label>
|
|
133
133
|
<span id="pv-sl-dur" class="pv-slider-val">500ms</span>
|
|
134
134
|
</div>
|
|
135
135
|
<input type="range" class="pv-slider" id="pv-inp-dur" min="50" max="2500" step="50" value="500" />
|
|
@@ -187,6 +187,11 @@ const testedClass = 'tt-key-tested';
|
|
|
187
187
|
const activeKeys = new Set<string>();
|
|
188
188
|
let maxSimultaneous = 0;
|
|
189
189
|
|
|
190
|
+
function scrollLogToEnd(el: HTMLElement) {
|
|
191
|
+
void el.offsetWidth;
|
|
192
|
+
el.scrollLeft = el.scrollWidth;
|
|
193
|
+
}
|
|
194
|
+
|
|
190
195
|
const keyMap = new Map<string, Element>();
|
|
191
196
|
keys.forEach((k) => {
|
|
192
197
|
const code = k.getAttribute('data-code');
|
|
@@ -199,7 +204,7 @@ const testedClass = 'tt-key-tested';
|
|
|
199
204
|
div.textContent = `${msg}`;
|
|
200
205
|
eventLog.appendChild(div);
|
|
201
206
|
if (eventLog.children.length > 50) eventLog.firstElementChild?.remove();
|
|
202
|
-
eventLog
|
|
207
|
+
scrollLogToEnd(eventLog);
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
function handleDown(e: KeyboardEvent) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import type { KnownLocale } from '../../types';
|
|
4
|
+
import { mouseDoubleClickTest } from './index';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const content = await mouseDoubleClickTest.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && content.bibliography.length > 0 && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'UI Events: click and dblclick event definitions',
|
|
6
|
+
url: 'https://www.w3.org/TR/uievents/',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'MDN Web Docs: Element click event',
|
|
10
|
+
url: 'https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'MDN Web Docs: performance.now()',
|
|
14
|
+
url: 'https://developer.mozilla.org/en-US/docs/Web/API/Performance/now',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { KnownLocale } from '../../types';
|
|
3
|
+
import type { MouseDoubleClickTestUI } from './ui';
|
|
4
|
+
import { Icon } from 'astro-icon/components';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
ui?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { ui } = Astro.props;
|
|
12
|
+
const t = (ui ?? {}) as MouseDoubleClickTestUI;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<div class="mdct-root" id="mdct-root" data-config={JSON.stringify({
|
|
16
|
+
statusIdle: t.statusIdle,
|
|
17
|
+
statusClean: t.statusClean,
|
|
18
|
+
statusWarning: t.statusWarning,
|
|
19
|
+
emptyLog: t.emptyLog,
|
|
20
|
+
leftButton: t.leftButton,
|
|
21
|
+
middleButton: t.middleButton,
|
|
22
|
+
rightButton: t.rightButton,
|
|
23
|
+
backButton: t.backButton,
|
|
24
|
+
forwardButton: t.forwardButton,
|
|
25
|
+
otherButton: t.otherButton,
|
|
26
|
+
thresholdUnit: t.thresholdUnit,
|
|
27
|
+
cleanEvent: t.cleanEvent,
|
|
28
|
+
suspiciousEvent: t.suspiciousEvent,
|
|
29
|
+
})}>
|
|
30
|
+
<section class="mdct-card">
|
|
31
|
+
<div class="mdct-stage">
|
|
32
|
+
<button id="mdct-target" class="mdct-target" type="button">
|
|
33
|
+
<span class="mdct-mouse" aria-hidden="true">
|
|
34
|
+
<span class="mdct-cable"></span>
|
|
35
|
+
<span class="mdct-button mdct-left" data-button="left" data-label={t.leftButton}></span>
|
|
36
|
+
<span class="mdct-button mdct-right" data-button="right" data-label={t.rightButton}></span>
|
|
37
|
+
<span class="mdct-button mdct-middle" data-button="middle" data-label={t.middleButton}></span>
|
|
38
|
+
<span class="mdct-button mdct-back" data-button="back" data-label={t.backButton}></span>
|
|
39
|
+
<span class="mdct-button mdct-forward" data-button="forward" data-label={t.forwardButton}></span>
|
|
40
|
+
<span class="mdct-palm"></span>
|
|
41
|
+
</span>
|
|
42
|
+
<span class="mdct-target-copy">
|
|
43
|
+
<span class="mdct-target-kicker">
|
|
44
|
+
<Icon name="mdi:mouse" class="mdct-target-icon" />
|
|
45
|
+
{t.badge}
|
|
46
|
+
</span>
|
|
47
|
+
<strong>{t.clickTarget}</strong>
|
|
48
|
+
<small>{t.clickTargetHint}</small>
|
|
49
|
+
</span>
|
|
50
|
+
</button>
|
|
51
|
+
|
|
52
|
+
<div class="mdct-dashboard">
|
|
53
|
+
<div class="mdct-readout">
|
|
54
|
+
<span>{t.healthScore}</span>
|
|
55
|
+
<strong id="mdct-score">100%</strong>
|
|
56
|
+
<small id="mdct-status" data-state="clean">{t.statusIdle}</small>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="mdct-metrics">
|
|
60
|
+
<div class="mdct-metric mdct-metric-total">
|
|
61
|
+
<span>{t.totalClicks}</span>
|
|
62
|
+
<strong id="mdct-total">0</strong>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="mdct-metric mdct-metric-alert">
|
|
65
|
+
<span>{t.suspiciousClicks}</span>
|
|
66
|
+
<strong id="mdct-suspicious">0</strong>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<label class="mdct-threshold">
|
|
71
|
+
<span>{t.thresholdLabel}</span>
|
|
72
|
+
<b><span id="mdct-threshold-value">80</span> {t.thresholdUnit}</b>
|
|
73
|
+
<input id="mdct-threshold" type="range" min="30" max="180" value="80" step="5" />
|
|
74
|
+
</label>
|
|
75
|
+
|
|
76
|
+
<div class="mdct-speed-row">
|
|
77
|
+
<div>
|
|
78
|
+
<span>{t.fastestGap}</span>
|
|
79
|
+
<strong id="mdct-fastest">--</strong>
|
|
80
|
+
</div>
|
|
81
|
+
<div>
|
|
82
|
+
<span>{t.lastGap}</span>
|
|
83
|
+
<strong id="mdct-last-gap">--</strong>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="mdct-log-head">
|
|
88
|
+
<span>{t.logTitle}</span>
|
|
89
|
+
<button id="mdct-reset" class="mdct-reset" type="button">{t.reset}</button>
|
|
90
|
+
</div>
|
|
91
|
+
<ol id="mdct-log" class="mdct-log"></ol>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</section>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<script>
|
|
98
|
+
import { MouseDoubleClickTester } from './logic';
|
|
99
|
+
|
|
100
|
+
const root = document.getElementById('mdct-root');
|
|
101
|
+
const labels = JSON.parse(root?.dataset.config ?? '{}');
|
|
102
|
+
const buttons = root?.querySelectorAll<HTMLElement>('.mdct-button') ?? document.querySelectorAll<HTMLElement>('.mdct-button');
|
|
103
|
+
|
|
104
|
+
new MouseDoubleClickTester(
|
|
105
|
+
{
|
|
106
|
+
target: document.getElementById('mdct-target'),
|
|
107
|
+
total: document.getElementById('mdct-total'),
|
|
108
|
+
suspicious: document.getElementById('mdct-suspicious'),
|
|
109
|
+
fastest: document.getElementById('mdct-fastest'),
|
|
110
|
+
score: document.getElementById('mdct-score'),
|
|
111
|
+
status: document.getElementById('mdct-status'),
|
|
112
|
+
threshold: document.getElementById('mdct-threshold') as HTMLInputElement | null,
|
|
113
|
+
thresholdValue: document.getElementById('mdct-threshold-value'),
|
|
114
|
+
lastGap: document.getElementById('mdct-last-gap'),
|
|
115
|
+
log: document.getElementById('mdct-log'),
|
|
116
|
+
reset: document.getElementById('mdct-reset'),
|
|
117
|
+
buttons,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
statusIdle: labels.statusIdle,
|
|
121
|
+
statusClean: labels.statusClean,
|
|
122
|
+
statusWarning: labels.statusWarning,
|
|
123
|
+
emptyLog: labels.emptyLog,
|
|
124
|
+
leftButton: labels.leftButton,
|
|
125
|
+
middleButton: labels.middleButton,
|
|
126
|
+
rightButton: labels.rightButton,
|
|
127
|
+
backButton: labels.backButton,
|
|
128
|
+
forwardButton: labels.forwardButton,
|
|
129
|
+
otherButton: labels.otherButton,
|
|
130
|
+
thresholdUnit: labels.thresholdUnit,
|
|
131
|
+
cleanEvent: labels.cleanEvent,
|
|
132
|
+
suspiciousEvent: labels.suspiciousEvent,
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
</script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HardwareToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
|
|
3
|
+
import type { MouseDoubleClickTestUI } from './ui';
|
|
4
|
+
export type MouseDoubleClickTestLocaleContent = ToolLocaleContent<MouseDoubleClickTestUI>;
|
|
5
|
+
|
|
6
|
+
export const mouseDoubleClickTest: HardwareToolEntry<MouseDoubleClickTestUI> = {
|
|
7
|
+
id: 'mouse-double-click-test',
|
|
8
|
+
icons: {
|
|
9
|
+
bg: 'mdi:mouse',
|
|
10
|
+
fg: 'mdi:gesture-tap-button',
|
|
11
|
+
},
|
|
12
|
+
i18n: {
|
|
13
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
14
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
15
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
16
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
17
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
18
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
19
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
20
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
21
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
22
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
23
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
24
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
25
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
26
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
27
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
28
|
+
},
|
|
29
|
+
};
|