test(velxio): add comprehensive unit test suites for velxio-manager, hooks redirect, and backend markdown parsing

This commit is contained in:
a2nr 2026-09-05 18:06:19 +07:00
parent ea167b45a7
commit 812ede0acc
5 changed files with 273 additions and 2 deletions

View File

@ -0,0 +1,56 @@
import { describe, it, expect, vi } from 'vitest';
import { handle } from './hooks.server';
describe('hooks.server handle', () => {
it('should redirect /editor to /velxio/editor with 302', async () => {
const mockEvent: any = {
request: {
method: 'GET',
headers: new Headers()
},
url: new URL('https://sinau-c-dev.manakin-gentoo.ts.net/editor')
};
const resolve = vi.fn();
const response = await handle({ event: mockEvent, resolve });
expect(response.status).toBe(302);
expect(response.headers.get('location')).toBe('https://sinau-c-dev.manakin-gentoo.ts.net/velxio/editor');
expect(resolve).not.toHaveBeenCalled();
});
it('should redirect /editor/ with query parameters to /velxio/editor?foo=bar with 302', async () => {
const mockEvent: any = {
request: {
method: 'GET',
headers: new Headers()
},
url: new URL('https://sinau-c-dev.manakin-gentoo.ts.net/editor/?embed=true&desktopLayout=true')
};
const resolve = vi.fn();
const response = await handle({ event: mockEvent, resolve });
expect(response.status).toBe(302);
expect(response.headers.get('location')).toBe('https://sinau-c-dev.manakin-gentoo.ts.net/velxio/editor?embed=true&desktopLayout=true');
expect(resolve).not.toHaveBeenCalled();
});
it('should pass through normal page routes to resolve()', async () => {
const mockEvent: any = {
request: {
method: 'GET',
headers: new Headers()
},
url: new URL('https://sinau-c-dev.manakin-gentoo.ts.net/teacher/content')
};
const mockResponse = new Response('ok');
const resolve = vi.fn().mockResolvedValue(mockResponse);
const response = await handle({ event: mockEvent, resolve });
expect(resolve).toHaveBeenCalledWith(mockEvent);
expect(response).toBe(mockResponse);
});
});

View File

@ -0,0 +1,140 @@
import { describe, it, expect } from 'vitest';
import { getVelxioState } from './velxio-manager';
describe('velxio-manager', () => {
describe('getVelxioState', () => {
it('should return null when iframe is null or contentWindow is missing', () => {
expect(getVelxioState(null)).toBeNull();
const mockIframeWithoutWin = {} as HTMLIFrameElement;
expect(getVelxioState(mockIframeWithoutWin)).toBeNull();
});
it('should return null when Velxio Zustand stores are not attached to window', () => {
const mockIframe = {
contentWindow: {}
} as HTMLIFrameElement;
expect(getVelxioState(mockIframe)).toBeNull();
});
it('should extract code and circuit structure from Velxio stores correctly', () => {
const mockEditorStore = {
fileGroups: {
'grp-1': [
{ name: 'header.h', content: '#define LED 13' },
{ name: 'sketch.ino', content: 'void setup() { pinMode(LED, OUTPUT); }\nvoid loop() {}' }
]
},
files: []
};
const mockSimStore = {
activeBoardId: 'board-1',
boards: [
{
id: 'board-1',
boardKind: 'arduino-uno',
activeFileGroupId: 'grp-1'
}
],
components: [
{
id: 'uno-1',
metadataId: 'arduino-uno',
x: 100.45,
y: 200.89,
properties: { rotation: 90, color: 'blue' }
},
{
id: 'led-1',
metadataId: 'led',
x: 50,
y: 80,
properties: { color: 'red' }
}
],
wires: [
{
start: { componentId: 'uno-1', pinName: '13' },
end: { componentId: 'led-1', pinName: 'A' }
},
null
]
};
const mockIframe = {
contentWindow: {
__VELXIO_EDITOR_STORE__: { getState: () => mockEditorStore },
__VELXIO_SIMULATOR_STORE__: { getState: () => mockSimStore }
}
} as unknown as HTMLIFrameElement;
const result = getVelxioState(mockIframe);
expect(result).not.toBeNull();
expect(result?.code).toBe('void setup() { pinMode(LED, OUTPUT); }\nvoid loop() {}');
const parsedCircuit = JSON.parse(result!.circuit);
expect(parsedCircuit.board).toBe('arduino:avr:uno');
expect(parsedCircuit.components).toHaveLength(2);
expect(parsedCircuit.components[0]).toEqual({
id: 'uno-1',
type: 'arduino-uno',
x: 100.5,
y: 200.9,
rotation: 90,
props: { rotation: 90, color: 'blue' }
});
expect(parsedCircuit.components[1]).toEqual({
id: 'led-1',
type: 'led',
x: 50,
y: 80,
rotation: 0,
props: { color: 'red' }
});
expect(parsedCircuit.wires).toHaveLength(1);
expect(parsedCircuit.wires[0]).toEqual({
start: { componentId: 'uno-1', pinName: '13' },
end: { componentId: 'led-1', pinName: 'A' }
});
});
it('should fallback to default files list and handle custom board FQBN', () => {
const mockEditorStore = {
fileGroups: {},
files: [
{ name: 'main.cpp', content: 'int main() { return 0; }' }
]
};
const mockSimStore = {
activeBoardId: 'custom-board',
boards: [
{
id: 'custom-board',
boardKind: 'custom:arch:myboard'
}
],
components: [],
wires: []
};
const mockIframe = {
contentWindow: {
__VELXIO_EDITOR_STORE__: { getState: () => mockEditorStore },
__VELXIO_SIMULATOR_STORE__: { getState: () => mockSimStore }
}
} as unknown as HTMLIFrameElement;
const result = getVelxioState(mockIframe);
expect(result).not.toBeNull();
expect(result?.code).toBe('int main() { return 0; }');
const parsedCircuit = JSON.parse(result!.circuit);
expect(parsedCircuit.board).toBe('custom:arch:myboard');
expect(parsedCircuit.components).toEqual([]);
expect(parsedCircuit.wires).toEqual([]);
});
});
});

View File

@ -1,5 +1,5 @@
import { VelxioBridge } from '$services/velxio-bridge';
import type { LessonContent } from '$types/lesson';
import { VelxioBridge } from './velxio-bridge';
import type { LessonContent } from '../types/lesson';
const BOARD_KIND_MAP: Record<string, string> = {
'arduino-uno': 'arduino:avr:uno',

View File

@ -1,8 +1,20 @@
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
$components: path.resolve(__dirname, 'src/lib/components'),
$stores: path.resolve(__dirname, 'src/lib/stores'),
$services: path.resolve(__dirname, 'src/lib/services'),
$types: path.resolve(__dirname, 'src/lib/types'),
$actions: path.resolve(__dirname, 'src/lib/actions'),
$lib: path.resolve(__dirname, 'src/lib')
}
},
test: {
environment: 'node',
include: ['src/**/*.test.ts']
}
});

View File

@ -0,0 +1,63 @@
import pytest
pytestmark = pytest.mark.unit
from services.lesson_service import _render_markdown_string
def test_render_markdown_velxio_with_code_and_circuit():
content = """# LED Blink
Berikut adalah materi kendali LED pada Arduino.
---INITIAL_CODE_ARDUINO---
void setup() {
pinMode(13, OUTPUT);
}
void loop() {
digitalWrite(13, HIGH);
delay(500);
digitalWrite(13, LOW);
delay(500);
}
---END_INITIAL_CODE_ARDUINO---
---VELXIO_CIRCUIT---
{
"board": "arduino:avr:uno",
"components": [
{ "id": "uno-1", "type": "arduino-uno", "x": 0, "y": 0 }
]
}
---END_VELXIO_CIRCUIT---
"""
parsed = _render_markdown_string(content)
assert 'velxio' in parsed['active_tabs']
assert parsed['initial_code_arduino'].startswith('void setup()')
assert 'arduino:avr:uno' in parsed['velxio_circuit']
def test_render_markdown_velxio_circuit_only():
content = """# Sirkuit Saja
---VELXIO_CIRCUIT---
{"board": "arduino:avr:uno"}
---END_VELXIO_CIRCUIT---
"""
parsed = _render_markdown_string(content)
assert parsed['active_tabs'] == ['velxio']
assert parsed.get('velxio_circuit') == '{"board": "arduino:avr:uno"}'
def test_render_markdown_velxio_exclusive_priority_over_c():
content = """# Prioritas Arduino
---INITIAL_CODE_ARDUINO---
void setup() {}
---END_INITIAL_CODE_ARDUINO---
---INITIAL_CODE---
int main() { return 0; }
---END_INITIAL_CODE---
"""
parsed = _render_markdown_string(content)
# INITIAL_CODE_ARDUINO harus menjadikan mode velxio eksklusif tanpa tab 'c'
assert 'velxio' in parsed['active_tabs']
assert 'c' not in parsed['active_tabs']