diff --git a/frontend/src/hooks.server.test.ts b/frontend/src/hooks.server.test.ts new file mode 100644 index 0000000..0882709 --- /dev/null +++ b/frontend/src/hooks.server.test.ts @@ -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); + }); +}); diff --git a/frontend/src/lib/services/velxio-manager.test.ts b/frontend/src/lib/services/velxio-manager.test.ts new file mode 100644 index 0000000..148935a --- /dev/null +++ b/frontend/src/lib/services/velxio-manager.test.ts @@ -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([]); + }); + }); +}); diff --git a/frontend/src/lib/services/velxio-manager.ts b/frontend/src/lib/services/velxio-manager.ts index ba3db10..1222ae2 100644 --- a/frontend/src/lib/services/velxio-manager.ts +++ b/frontend/src/lib/services/velxio-manager.ts @@ -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 = { 'arduino-uno': 'arduino:avr:uno', diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index 9b11f7a..2332903 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -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'] } }); + diff --git a/services/tests/test_lesson_service_velxio.py b/services/tests/test_lesson_service_velxio.py new file mode 100644 index 0000000..ff3d0d3 --- /dev/null +++ b/services/tests/test_lesson_service_velxio.py @@ -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']