Skip to content

Commit 617dc87

Browse files
committed
feat(app): add initial block-script component
1 parent 3c6b141 commit 617dc87

File tree

3 files changed

+328
-0
lines changed

3 files changed

+328
-0
lines changed
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { useMountWrapper } from '__tests__/fixtures/component'
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3+
import { MarkdownNodeComponent } from '@language-kit/markdown'
4+
import { useBlockStub } from '../__tests__/stubs'
5+
6+
import BlockScript from './BlockScript.vue'
7+
import VBtn from '@components/VBtn.vue'
8+
import ANSICard from '@modules/evaluation/components/ANSICard.vue'
9+
import { waitFor } from '@composables/utils'
10+
import * as Evaluation from '@modules/evaluation/composables/use-evaluation'
11+
import MonacoEditor from '@components/MonacoEditor.vue'
12+
13+
describe('BlockScript (unit)', () => {
14+
const component = useMountWrapper(BlockScript, {
15+
shallow: true,
16+
global: {
17+
stubs: {
18+
Block: useBlockStub(),
19+
},
20+
},
21+
})
22+
23+
afterEach(() => {
24+
component.unmount()
25+
26+
vi.resetAllMocks()
27+
})
28+
29+
function createNode(data?: Partial<MarkdownNodeComponent>) {
30+
const node = new MarkdownNodeComponent()
31+
32+
node.name = 'script'
33+
node.attrs = data?.attrs ?? {}
34+
node.body = data?.body ?? ''
35+
36+
return node
37+
}
38+
39+
function createRuntimeMock() {
40+
const callbacks = {
41+
stdout: [] as Function[],
42+
stderr: [] as Function[],
43+
}
44+
45+
return {
46+
run: vi.fn(),
47+
onDone: vi.fn().mockResolvedValue(undefined),
48+
on: (event: keyof typeof callbacks, callback: any) => callbacks[event]?.push(callback),
49+
emit: (event: keyof typeof callbacks, ...args: any[]) => {
50+
callbacks[event].forEach((cb) => cb(...args))
51+
},
52+
}
53+
}
54+
55+
function createEvaluationMock() {
56+
const spy = vi.fn()
57+
58+
const runtime = createRuntimeMock()
59+
60+
spy.mockReturnValue(runtime as any)
61+
62+
vi.spyOn(Evaluation, 'useEvaluation').mockReturnValue({
63+
run: spy,
64+
} as any)
65+
66+
return { spy, runtime }
67+
}
68+
69+
function findRunButton() {
70+
return component.wrapper!.findComponent<typeof VBtn>('[data-test-id="run-button"]')
71+
}
72+
73+
function findClearButton() {
74+
return component.wrapper!.findComponent('[data-test-id="clear-button"]')
75+
}
76+
77+
function findEditor() {
78+
return component.wrapper!.findComponent<typeof MonacoEditor>('[data-test-id="editor"]')
79+
}
80+
81+
function findANSIComponent() {
82+
return component.wrapper!.findComponent(ANSICard)
83+
}
84+
85+
it('should render run button', () => {
86+
component.mount({
87+
props: {
88+
modelValue: createNode(),
89+
},
90+
})
91+
92+
expect(findRunButton().exists()).toBe(true)
93+
})
94+
95+
it('should mount runtime with node body run button', async () => {
96+
const node = createNode({
97+
body: 'console.log("Hello world!")',
98+
})
99+
100+
const running = ref(false)
101+
102+
const { spy } = createEvaluationMock()
103+
104+
component.mount({
105+
props: {
106+
'modelValue': node,
107+
'running': running.value,
108+
'onUpdate:running': (v: boolean) => (running.value = v),
109+
},
110+
})
111+
112+
await findRunButton().trigger('click')
113+
114+
await waitFor(() => !running.value)
115+
116+
expect(spy).toHaveBeenCalledWith(node.body, { immediate: false, timeout: 10000 })
117+
})
118+
119+
it('should execute script on button click', async () => {
120+
const node = createNode({
121+
body: 'console.log("Hello world!")',
122+
})
123+
124+
const running = ref(false)
125+
126+
const { runtime } = createEvaluationMock()
127+
128+
component.mount({
129+
props: {
130+
'modelValue': node,
131+
'running': running.value,
132+
'onUpdate:running': (v: boolean) => (running.value = v),
133+
},
134+
})
135+
136+
let done = false
137+
138+
const runButton = findRunButton()
139+
const ANSIComponent = findANSIComponent()
140+
141+
runtime.onDone.mockImplementation(() => waitFor(() => done))
142+
143+
await runButton.trigger('click')
144+
145+
runtime.emit('stdout', 'Hello world!')
146+
147+
done = true
148+
149+
await waitFor(() => !running.value)
150+
151+
expect(ANSIComponent.exists()).toBe(true)
152+
153+
expect(ANSIComponent.props('modelValue')).toEqual([
154+
'🔥 Running code...',
155+
'',
156+
'Hello world!',
157+
'',
158+
'🎉 Code executed successfully!',
159+
])
160+
})
161+
162+
it('should update node when edit script', async () => {
163+
const node = createNode({
164+
body: 'console.log("Hello world!")',
165+
})
166+
167+
createEvaluationMock()
168+
169+
component.mount({
170+
props: {
171+
'modelValue': node,
172+
'onUpdate:modelValue': (n: MarkdownNodeComponent) => {
173+
node.body = n.body
174+
},
175+
},
176+
})
177+
178+
const editor = findEditor()
179+
180+
expect(editor.exists()).toBe(true)
181+
182+
await editor.setValue('console.log("Update!")')
183+
184+
await editor.trigger('keydown.ctrl.s')
185+
186+
expect(node.body).toBe('console.log("Update!")')
187+
})
188+
189+
it('should clear output on clean-button click', async () => {
190+
const node = createNode({
191+
body: 'console.log("Hello world!")',
192+
})
193+
194+
createEvaluationMock()
195+
196+
component.mount({
197+
props: {
198+
modelValue: node,
199+
},
200+
})
201+
202+
const ANSIComponent = findANSIComponent()
203+
const clearButton = findClearButton()
204+
205+
await ANSIComponent.setValue(['Hello world!'])
206+
207+
expect(ANSIComponent.props('modelValue')).toEqual(['Hello world!'])
208+
209+
await clearButton.trigger('click')
210+
211+
expect(ANSIComponent.props('modelValue')).toEqual([])
212+
})
213+
})
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<script setup lang="ts">
2+
import { MarkdownNodeComponent } from '@language-kit/markdown'
3+
import { useEvaluation } from '@modules/evaluation/composables/use-evaluation'
4+
5+
import Block from './Block.vue'
6+
import ANSICard from '@modules/evaluation/components/ANSICard.vue'
7+
// import MonacoEditor from '@modules/monaco/components/MEditor.vue'
8+
9+
const MonacoEditor = defineAsyncComponent(() => import('@modules/monaco/components/MEditor.vue'))
10+
11+
const node = defineModel({
12+
type: MarkdownNodeComponent,
13+
required: true,
14+
validator: (n: MarkdownNodeComponent) => n.name === 'script',
15+
})
16+
17+
const running = defineModel('running', {
18+
type: Boolean,
19+
default: false,
20+
local: true,
21+
})
22+
23+
const code = ref(node.value.body)
24+
const evaluation = useEvaluation()
25+
const output = ref<string[]>([])
26+
27+
async function run() {
28+
output.value = []
29+
30+
output.value.push('🔥 Running code...')
31+
output.value.push('')
32+
33+
const runtime = await evaluation.run(code.value, {
34+
immediate: false,
35+
timeout: 10000,
36+
})
37+
38+
runtime.on('stdout', (data) => output.value.push(data))
39+
40+
runtime.on('stderr', (data) => output.value.push(data))
41+
42+
runtime.run()
43+
44+
runtime
45+
.onDone()
46+
.then(() => {
47+
output.value.push('')
48+
49+
output.value.push('🎉 Code executed successfully!')
50+
51+
running.value = false
52+
})
53+
.catch(() => {
54+
output.value.push('')
55+
56+
output.value.push('🚨 Code execution failed!')
57+
58+
running.value = false
59+
})
60+
}
61+
62+
async function update() {
63+
node.value.body = code.value
64+
65+
node.value = node.value
66+
}
67+
</script>
68+
<template>
69+
<block v-model="node">
70+
<v-btn data-test-id="run-button" @click="run">
71+
{{ $t('run') }}
72+
</v-btn>
73+
74+
<v-btn data-test-id="clear-button" @click="output = []">
75+
{{ $t('clear') }}
76+
</v-btn>
77+
78+
<MonacoEditor
79+
v-model="code"
80+
data-test-id="editor"
81+
:line-options="{
82+
show: 'off',
83+
decorationsWidth: 16,
84+
numbersMinChars: 0,
85+
}"
86+
:padding="{
87+
top: 8,
88+
bottom: 8,
89+
}"
90+
:scrollbar="{
91+
verticalScrollbarSize: 0,
92+
horizontalScrollbarSize: 0,
93+
useShadows: false,
94+
horizontal: 'hidden',
95+
vertical: 'hidden',
96+
}"
97+
render-line-highlight="none"
98+
@keydown.ctrl.s="update"
99+
/>
100+
101+
<ANSICard v-model="output" />
102+
</block>
103+
</template>

packages/app/vitest.config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { mergeConfig } from 'vite'
2+
import { resolve } from 'path'
23
import { defineConfig, configDefaults } from 'vitest/config'
34
import viteConfig from './vite.config'
45

6+
const root = resolve(__dirname, '..', '..')
7+
58
export default mergeConfig(
69
viteConfig,
710
defineConfig({
@@ -11,6 +14,15 @@ export default mergeConfig(
1114
exclude: [...configDefaults.exclude, 'packages/core/*'],
1215
setupFiles: ['__tests__/setup.ts'],
1316
reporters: 'verbose',
17+
alias: [
18+
{
19+
find: /^monaco-editor$/,
20+
replacement: resolve(
21+
root,
22+
'node_modules/monaco-editor/esm/vs/editor/editor.api'
23+
),
24+
},
25+
],
1426
},
1527
})
1628
)

0 commit comments

Comments
 (0)