Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,190 @@
|
|||||||
|
/**
|
||||||
|
* DEVKIT — `/devkit/smoke` smoke test runner.
|
||||||
|
*
|
||||||
|
* Scripted suite that exercises the store + demo pipeline without a headset.
|
||||||
|
* Run before sending a build to the Quest tester. All steps must be ✅.
|
||||||
|
*/
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Link, Navigate } from 'react-router-dom';
|
||||||
|
import { useModelStore } from '@/stores/useModelStore';
|
||||||
|
import { generateDemoBeamGLB } from '@/lib/generateDemoBeam';
|
||||||
|
import { useDevKit } from './useDevKit';
|
||||||
|
import { fakeInput } from './fakeInputStore';
|
||||||
|
import { startRecording, stopRecording, macroState } from './macroStore';
|
||||||
|
|
||||||
|
interface StepResult { name: string; ok: boolean; detail: string; ms: number; }
|
||||||
|
|
||||||
|
type Step = { name: string; run: () => Promise<string> };
|
||||||
|
|
||||||
|
function buildSteps(): Step[] {
|
||||||
|
const store = useModelStore.getState;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'gerar demo beam (IPE 200)',
|
||||||
|
run: async () => {
|
||||||
|
const { blob, fileName, fileSize } = await generateDemoBeamGLB();
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
store().setModel({ url, fileName, fileSize });
|
||||||
|
return `${fileName} • ${(fileSize / 1024).toFixed(1)} KB`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'modelo no store',
|
||||||
|
run: async () => {
|
||||||
|
const m = store().model;
|
||||||
|
if (!m?.url) throw new Error('model.url ausente');
|
||||||
|
return m.fileName;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fineTuning reset',
|
||||||
|
run: async () => {
|
||||||
|
store().resetFineTuning();
|
||||||
|
const ft = store().fineTuning;
|
||||||
|
if (ft.posX !== 0 || ft.scale !== 1) throw new Error('reset falhou');
|
||||||
|
return 'ok';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'medição cria distância correta',
|
||||||
|
run: async () => {
|
||||||
|
store().clearMeasurements();
|
||||||
|
store().addMeasurePoint({ x: 0, y: 0, z: 0 });
|
||||||
|
store().addMeasurePoint({ x: 0.1, y: 0, z: 0 });
|
||||||
|
const ms = store().measurements;
|
||||||
|
if (ms.length !== 1) throw new Error(`esperado 1 medição, obteve ${ms.length}`);
|
||||||
|
const d = ms[0].distanceMM;
|
||||||
|
if (Math.abs(d - 100) > 0.01) throw new Error(`esperado 100mm, obteve ${d.toFixed(2)}`);
|
||||||
|
return `${d.toFixed(2)} mm`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'checklist defaults',
|
||||||
|
run: async () => {
|
||||||
|
store().resetChecklist();
|
||||||
|
const cl = store().checklist;
|
||||||
|
if (cl.length !== 4) throw new Error(`esperado 4 itens, obteve ${cl.length}`);
|
||||||
|
if (!cl.every(i => i.status === 'pending')) throw new Error('itens não-pending');
|
||||||
|
return `${cl.length} itens`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fake input grip simulado',
|
||||||
|
run: async () => {
|
||||||
|
fakeInput.left.gripValue = 1;
|
||||||
|
await new Promise(r => setTimeout(r, 50));
|
||||||
|
if (fakeInput.left.gripValue !== 1) throw new Error('grip não foi setado');
|
||||||
|
fakeInput.left.gripValue = 0;
|
||||||
|
return 'ok';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'macro recorder ciclo curto',
|
||||||
|
run: async () => {
|
||||||
|
startRecording();
|
||||||
|
await new Promise(r => setTimeout(r, 200));
|
||||||
|
const m = stopRecording();
|
||||||
|
if (!m) throw new Error('macro vazia');
|
||||||
|
if (macroState.phase !== 'idle') throw new Error('fase não voltou a idle');
|
||||||
|
return `${m.frames.length} frames • ${m.duration.toFixed(0)}ms`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'navigator.xr disponível',
|
||||||
|
run: async () => {
|
||||||
|
if (!('xr' in navigator)) throw new Error('navigator.xr ausente');
|
||||||
|
return 'ok';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'HTTPS ativo',
|
||||||
|
run: async () => {
|
||||||
|
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
|
||||||
|
throw new Error(`protocolo ${location.protocol}`);
|
||||||
|
}
|
||||||
|
return location.protocol;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SmokeTest() {
|
||||||
|
const devkit = useDevKit();
|
||||||
|
const [results, setResults] = useState<StepResult[]>([]);
|
||||||
|
const [running, setRunning] = useState(false);
|
||||||
|
|
||||||
|
if (!devkit) return <Navigate to="/" replace />;
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
setRunning(true);
|
||||||
|
setResults([]);
|
||||||
|
const steps = buildSteps();
|
||||||
|
const out: StepResult[] = [];
|
||||||
|
for (const s of steps) {
|
||||||
|
const t0 = performance.now();
|
||||||
|
try {
|
||||||
|
const detail = await s.run();
|
||||||
|
out.push({ name: s.name, ok: true, detail, ms: performance.now() - t0 });
|
||||||
|
} catch (e) {
|
||||||
|
out.push({ name: s.name, ok: false, detail: (e as Error).message, ms: performance.now() - t0 });
|
||||||
|
}
|
||||||
|
setResults([...out]);
|
||||||
|
}
|
||||||
|
setRunning(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const passed = results.filter(r => r.ok).length;
|
||||||
|
const failed = results.filter(r => !r.ok).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background p-6 font-mono text-foreground">
|
||||||
|
<div className="mx-auto max-w-2xl space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-bold">🧪 DevKit · Smoke Test</h1>
|
||||||
|
<Link to="/" className="text-xs text-primary hover:underline">← Home</Link>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Roteiro automático que exercita store, demo, medição, checklist, fake input e gravador.
|
||||||
|
Execute antes de enviar build para o testador com Quest.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={run}
|
||||||
|
disabled={running}
|
||||||
|
className="rounded bg-primary px-4 py-2 text-sm text-primary-foreground hover:opacity-90 disabled:opacity-40"
|
||||||
|
>
|
||||||
|
{running ? 'Executando…' : '▶ Rodar suite'}
|
||||||
|
</button>
|
||||||
|
{results.length > 0 && (
|
||||||
|
<span className="text-sm">
|
||||||
|
<span className="text-green-400">✓ {passed}</span>
|
||||||
|
{' · '}
|
||||||
|
<span className={failed ? 'text-red-400' : 'text-muted-foreground'}>✕ {failed}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1 rounded border border-border bg-card p-3">
|
||||||
|
{results.length === 0 && (
|
||||||
|
<div className="text-xs text-muted-foreground italic">aguardando execução…</div>
|
||||||
|
)}
|
||||||
|
{results.map((r, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`flex items-start justify-between gap-3 border-b border-border/50 py-1 text-xs ${
|
||||||
|
r.ok ? 'text-green-400' : 'text-red-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<span className="mr-2">{r.ok ? '✓' : '✕'}</span>
|
||||||
|
{r.name}
|
||||||
|
<div className="ml-5 text-muted-foreground">{r.detail}</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-muted-foreground">{r.ms.toFixed(0)}ms</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user