@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
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -4,14 +4,16 @@ 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
|
+
import { monitorGhostingTest } from '../tool/monitorGhostingTest/index';
|
|
10
12
|
import { spectrumCanvas } from '../tool/colorAccuracyTest/index';
|
|
11
13
|
|
|
12
14
|
export const hardwareCategory: HardwareCategoryEntry = {
|
|
13
15
|
icon: 'mdi:memory',
|
|
14
|
-
tools: [pixelesPantalla, testTeclado, testMando, probadorVibracionMando, testRaton, estimadorSaludBateria, toneGenerator, refreshRateDetector, spectrumCanvas],
|
|
16
|
+
tools: [pixelesPantalla, testTeclado, testMando, probadorVibracionMando, testRaton, mouseDoubleClickTest, estimadorSaludBateria, toneGenerator, refreshRateDetector, monitorGhostingTest, spectrumCanvas],
|
|
15
17
|
i18n: {
|
|
16
18
|
en: () => import('./i18n/en').then((m) => m.content),
|
|
17
19
|
es: () => import('./i18n/es').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -10,10 +10,14 @@ 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';
|
|
16
18
|
export type { RefreshRateDetectorLocaleContent } from './tool/refreshRateDetector/entry';
|
|
19
|
+
export { monitorGhostingTest } from './tool/monitorGhostingTest/entry';
|
|
20
|
+
export type { MonitorGhostingTestLocaleContent } from './tool/monitorGhostingTest/entry';
|
|
17
21
|
export { spectrumCanvas } from './tool/colorAccuracyTest/entry';
|
|
18
22
|
export type { SpectrumCanvasLocaleContent } from './tool/colorAccuracyTest/entry';
|
|
19
23
|
export { hardwareCategory } from './category';
|
|
@@ -23,7 +27,9 @@ import { testMando } from './tool/gamepadTest/entry';
|
|
|
23
27
|
import { probadorVibracionMando } from './tool/gamepadVibrationTester/entry';
|
|
24
28
|
import { testTeclado } from './tool/keyboardTest/entry';
|
|
25
29
|
import { testRaton } from './tool/mousePollingTest/entry';
|
|
30
|
+
import { mouseDoubleClickTest } from './tool/mouseDoubleClickTest/entry';
|
|
26
31
|
import { toneGenerator } from './tool/toneGenerator/entry';
|
|
27
32
|
import { refreshRateDetector } from './tool/refreshRateDetector/entry';
|
|
33
|
+
import { monitorGhostingTest } from './tool/monitorGhostingTest/entry';
|
|
28
34
|
import { spectrumCanvas } from './tool/colorAccuracyTest/entry';
|
|
29
|
-
export const ALL_ENTRIES = [estimadorSaludBateria, pixelesPantalla, testMando, probadorVibracionMando, testTeclado, testRaton, toneGenerator, refreshRateDetector, spectrumCanvas];
|
|
35
|
+
export const ALL_ENTRIES = [estimadorSaludBateria, pixelesPantalla, testMando, probadorVibracionMando, testTeclado, testRaton, mouseDoubleClickTest, toneGenerator, refreshRateDetector, monitorGhostingTest, spectrumCanvas];
|
package/src/index.ts
CHANGED
|
@@ -22,7 +22,9 @@ 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';
|
|
29
|
+
export { MONITOR_GHOSTING_TEST_TOOL } from './tool/monitorGhostingTest/index';
|
|
28
30
|
export { SPECTRUM_CANVAS_TOOL } from './tool/colorAccuracyTest/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 11 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(11);
|
|
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 { monitorGhostingTest } from './index';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const content = await monitorGhostingTest.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && content.bibliography.length > 0 && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'Blur Busters UFO Motion Tests: visual tests for ghosting, pursuit tracking, and motion clarity',
|
|
6
|
+
url: 'https://www.testufo.com/',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'RTINGS: monitor response time, overshoot, and motion handling methodology',
|
|
10
|
+
url: 'https://www.rtings.com/monitor/tests/motion/motion-blur-and-response-time',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'TFTCentral: response time, overdrive, overshoot, and pursuit camera monitor testing',
|
|
14
|
+
url: 'https://tftcentral.co.uk/articles/response_time',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'Blur Busters: G-SYNC 101 guide to frame pacing, VRR behavior, and display motion',
|
|
18
|
+
url: 'https://blurbusters.com/gsync/gsync101-input-lag-tests-and-settings/',
|
|
19
|
+
}
|
|
20
|
+
];
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { KnownLocale } from '../../types';
|
|
3
|
+
import type { MonitorGhostingTestUI } from './ui';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
locale?: KnownLocale;
|
|
7
|
+
ui?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { ui } = Astro.props;
|
|
11
|
+
const t = (ui ?? {}) as MonitorGhostingTestUI;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div id="mgt-root" class="mgt-root" data-config={JSON.stringify({
|
|
15
|
+
pixelsPerSecondUnit: t.pixelsPerSecondUnit,
|
|
16
|
+
pixelUnit: t.pixelUnit,
|
|
17
|
+
millisecondUnit: t.millisecondUnit,
|
|
18
|
+
fullscreen: t.fullscreen,
|
|
19
|
+
exitFullscreen: t.exitFullscreen,
|
|
20
|
+
pause: t.pause,
|
|
21
|
+
resume: t.resume,
|
|
22
|
+
})}>
|
|
23
|
+
<section class="mgt-card">
|
|
24
|
+
<div class="mgt-stage">
|
|
25
|
+
<div class="mgt-monitor" id="mgt-monitor">
|
|
26
|
+
<div class="mgt-screen">
|
|
27
|
+
<div class="mgt-ruler" aria-hidden="true"></div>
|
|
28
|
+
<div id="mgt-track" class="mgt-track">
|
|
29
|
+
<span class="mgt-ghost ghost-5"></span>
|
|
30
|
+
<span class="mgt-ghost ghost-4"></span>
|
|
31
|
+
<span class="mgt-ghost ghost-3"></span>
|
|
32
|
+
<span class="mgt-ghost ghost-2"></span>
|
|
33
|
+
<span class="mgt-ghost ghost-1"></span>
|
|
34
|
+
<span id="mgt-target-object" class="mgt-target-object">
|
|
35
|
+
<span class="mgt-bars" aria-hidden="true"></span>
|
|
36
|
+
<span class="mgt-word">{t.targetText}</span>
|
|
37
|
+
<span class="mgt-ufo" aria-hidden="true">
|
|
38
|
+
<span class="mgt-ufo-dome"></span>
|
|
39
|
+
<span class="mgt-ufo-body"></span>
|
|
40
|
+
<span class="mgt-ufo-light"></span>
|
|
41
|
+
</span>
|
|
42
|
+
</span>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="mgt-pursuit" aria-hidden="true"></div>
|
|
45
|
+
<div class="mgt-screen-actions">
|
|
46
|
+
<button id="mgt-fullscreen" type="button">{t.fullscreen}</button>
|
|
47
|
+
<button id="mgt-pause" type="button">{t.pause}</button>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="mgt-panel">
|
|
53
|
+
<div class="mgt-readout">
|
|
54
|
+
<span>{t.badge}</span>
|
|
55
|
+
<strong id="mgt-blur">--</strong>
|
|
56
|
+
<small>{t.estimatedBlur}</small>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<label class="mgt-control">
|
|
60
|
+
<span>{t.speedLabel}</span>
|
|
61
|
+
<b id="mgt-speed-value">--</b>
|
|
62
|
+
<input id="mgt-speed" type="range" min="240" max="1920" step="120" value="960" />
|
|
63
|
+
</label>
|
|
64
|
+
|
|
65
|
+
<label class="mgt-control">
|
|
66
|
+
<span>{t.trailLabel}</span>
|
|
67
|
+
<b id="mgt-trail-value">--</b>
|
|
68
|
+
<input id="mgt-trail" type="range" min="1" max="10" step="1" value="5" />
|
|
69
|
+
</label>
|
|
70
|
+
|
|
71
|
+
<label class="mgt-select">
|
|
72
|
+
<span>{t.backgroundLabel}</span>
|
|
73
|
+
<select id="mgt-background">
|
|
74
|
+
<option value="dark">{t.backgroundDark}</option>
|
|
75
|
+
<option value="light">{t.backgroundLight}</option>
|
|
76
|
+
<option value="grid">{t.backgroundGrid}</option>
|
|
77
|
+
</select>
|
|
78
|
+
</label>
|
|
79
|
+
|
|
80
|
+
<label class="mgt-select">
|
|
81
|
+
<span>{t.patternLabel}</span>
|
|
82
|
+
<select id="mgt-pattern">
|
|
83
|
+
<option value="bars">{t.patternBars}</option>
|
|
84
|
+
<option value="text">{t.patternText}</option>
|
|
85
|
+
<option value="ufo">{t.patternUfo}</option>
|
|
86
|
+
</select>
|
|
87
|
+
</label>
|
|
88
|
+
|
|
89
|
+
<label class="mgt-toggle">
|
|
90
|
+
<span>
|
|
91
|
+
<b>{t.pursuitLabel}</b>
|
|
92
|
+
<small>{t.pursuitOn} / {t.pursuitOff}</small>
|
|
93
|
+
</span>
|
|
94
|
+
<input id="mgt-pursuit" type="checkbox" checked />
|
|
95
|
+
</label>
|
|
96
|
+
|
|
97
|
+
<div class="mgt-metrics">
|
|
98
|
+
<div>
|
|
99
|
+
<span>{t.frameStep}</span>
|
|
100
|
+
<strong id="mgt-frame-step">--</strong>
|
|
101
|
+
</div>
|
|
102
|
+
<div>
|
|
103
|
+
<span>{t.persistence}</span>
|
|
104
|
+
<strong id="mgt-persistence">--</strong>
|
|
105
|
+
</div>
|
|
106
|
+
<div>
|
|
107
|
+
<span>{t.sampleCount}</span>
|
|
108
|
+
<strong id="mgt-sample-count">--</strong>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<p class="mgt-instructions">{t.instructions}</p>
|
|
113
|
+
<button id="mgt-reset" class="mgt-reset" type="button">{t.reset}</button>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</section>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<script>
|
|
120
|
+
import { MonitorGhostingLab } from './logic';
|
|
121
|
+
|
|
122
|
+
const root = document.getElementById('mgt-root');
|
|
123
|
+
const labels = JSON.parse(root?.dataset.config ?? '{}');
|
|
124
|
+
|
|
125
|
+
new MonitorGhostingLab(
|
|
126
|
+
{
|
|
127
|
+
root,
|
|
128
|
+
monitor: document.getElementById('mgt-monitor'),
|
|
129
|
+
track: document.getElementById('mgt-track'),
|
|
130
|
+
target: document.getElementById('mgt-target-object'),
|
|
131
|
+
speed: document.getElementById('mgt-speed') as HTMLInputElement | null,
|
|
132
|
+
trail: document.getElementById('mgt-trail') as HTMLInputElement | null,
|
|
133
|
+
background: document.getElementById('mgt-background') as HTMLSelectElement | null,
|
|
134
|
+
pattern: document.getElementById('mgt-pattern') as HTMLSelectElement | null,
|
|
135
|
+
pursuit: document.getElementById('mgt-pursuit') as HTMLInputElement | null,
|
|
136
|
+
fullscreen: document.getElementById('mgt-fullscreen'),
|
|
137
|
+
pause: document.getElementById('mgt-pause'),
|
|
138
|
+
reset: document.getElementById('mgt-reset'),
|
|
139
|
+
speedValue: document.getElementById('mgt-speed-value'),
|
|
140
|
+
trailValue: document.getElementById('mgt-trail-value'),
|
|
141
|
+
blur: document.getElementById('mgt-blur'),
|
|
142
|
+
frameStep: document.getElementById('mgt-frame-step'),
|
|
143
|
+
persistence: document.getElementById('mgt-persistence'),
|
|
144
|
+
sampleCount: document.getElementById('mgt-sample-count'),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
pixelsPerSecondUnit: labels.pixelsPerSecondUnit,
|
|
148
|
+
pixelUnit: labels.pixelUnit,
|
|
149
|
+
millisecondUnit: labels.millisecondUnit,
|
|
150
|
+
fullscreen: labels.fullscreen,
|
|
151
|
+
exitFullscreen: labels.exitFullscreen,
|
|
152
|
+
pause: labels.pause,
|
|
153
|
+
resume: labels.resume,
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
</script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HardwareToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
|
|
3
|
+
import type { MonitorGhostingTestUI } from './ui';
|
|
4
|
+
export type MonitorGhostingTestLocaleContent = ToolLocaleContent<MonitorGhostingTestUI>;
|
|
5
|
+
|
|
6
|
+
export const monitorGhostingTest: HardwareToolEntry<MonitorGhostingTestUI> = {
|
|
7
|
+
id: 'monitor-ghosting-test',
|
|
8
|
+
icons: {
|
|
9
|
+
bg: 'mdi:monitor',
|
|
10
|
+
fg: 'mdi:motion-play',
|
|
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
|
+
};
|