项目 7 情景控制:虚拟时钟、项目资源、Windows Media Player、TrackBar
- 虚拟时钟:实训台时钟面板可拨时间、1×/10×/60× 倍速、恢复真实时间;编译时把 DateTime.Now / Today / UtcNow 改写成 SimClock;评分规则加 clock 步骤 - 项目资源(Properties.Resources):平台素材库(自绘开关图片、合成门铃声),"项目资源"对话框勾选, 平台生成 Properties/Resources.Designer.cs;设计器图片框的"图片"属性可选资源;程序目录下的 Resources 指向素材库;未添加资源时给中文提示 - Windows Media Player(AxWMPLib):设置 URL 自动播放、Ctlcontrols.play/stop,浏览器里播放声音, 实训台加广播喇叭;编译时给窗体嵌空资源,OcxState 取到 null 不报错;图片和声音统一按内容缓存(blob) - TrackBar:拖动、点轨道、方向键,先 ValueChanged 再 Scroll;设计器加"滑动条""媒体播放器" - 评分规则加 value、media 检查;示例项目 7 任务 1–3(修正书上开关灯判断写反、漏填串口列表) - 修正:Properties/ 下的资源类不当作窗体;带命名空间的 System.AppDomain.CurrentDomain.BaseDirectory 放行 - 测试:虚拟时钟、TrackBar、项目 7 示例评分、作业起点带资源类 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a9af26c2d6
commit
eedf1aa2fb
66 files changed
+3538
-43
No files matched your search
@@ -1,5 +1,6 @@
|
||||
// HTTP 接口。除登录外都要先登录;/api/teacher/* 只有老师能用。
|
||||
import type { Database } from 'bun:sqlite'
|
||||
import { dirname } from 'node:path'
|
||||
import { Hono, type Context } from 'hono'
|
||||
import { bearerToken, login, logout, refresh, tokenUser, tooManyFailures, type Tokens, type User } from './auth'
|
||||
import { createClass, deleteClass, deleteStudent, importStudents, listClasses, listStudents, renameClass, resetPassword, type ImportRow } from './classes'
|
||||
@@ -71,6 +72,15 @@ export function createApp(db: Database, gradingQueue: GradingQueue, aiQueue: AiQ
|
||||
return c.json({})
|
||||
})
|
||||
|
||||
// 项目资源素材库(Properties.Resources 的图片、声音):设计器预览用 <img> 直接显示,不需要登录
|
||||
app.get('/resources/:file', async (c) => {
|
||||
const name = c.req.param('file')
|
||||
if (!/^[\w.-]+$/.test(name)) return c.notFound()
|
||||
const file = Bun.file(`${dirname(config.runnerDll)}/Resources/gallery/${name}`)
|
||||
if (!(await file.exists())) return c.notFound()
|
||||
return new Response(file, { headers: { 'Cache-Control': 'public, max-age=3600' } })
|
||||
})
|
||||
|
||||
// 其余接口都要先登录(请求头 Authorization: Bearer <令牌>)
|
||||
app.use('/api/*', async (c, next) => {
|
||||
const user = await tokenUser(db, bearerToken(c.req.header('authorization')))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
clockWall,
|
||||
ANALOG_CHANNELS,
|
||||
ANALOG_INFO,
|
||||
type BoardState,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
type RunnerInput,
|
||||
type RunnerMessage,
|
||||
} from '@iot/shared'
|
||||
import { config } from './config'
|
||||
|
||||
export function defaultBoardState(): BoardState {
|
||||
return {
|
||||
@@ -16,6 +18,7 @@ export function defaultBoardState(): BoardState {
|
||||
zigbee: [false, false],
|
||||
ledText: '',
|
||||
camera: { on: false, x: 3, y: 1 },
|
||||
clock: null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +32,7 @@ export class Board {
|
||||
type: 'board',
|
||||
analog: ANALOG_CHANNELS.map((key) => this.state.analog[key]),
|
||||
digital: DIGITAL_CHANNELS.map((key) => this.state.digital[key]),
|
||||
clock: this.state.clock,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +53,31 @@ export class Board {
|
||||
this.state.digital[name] = value
|
||||
changed = true
|
||||
}
|
||||
if (message.clock === null) {
|
||||
changed ||= this.state.clock !== null
|
||||
this.state.clock = null
|
||||
} else if (message.clock && typeof message.clock === 'object') {
|
||||
changed = this.setClock(message.clock.time, message.clock.speed) || changed
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
/**
|
||||
* 拨虚拟时钟:time 是墙上时间(yyyy-MM-ddTHH:mm:ss,不给就从现在的时刻接着走),speed 是倍速(不给就保持原来的)。
|
||||
* 恢复真实时间是 clock = null。
|
||||
*/
|
||||
setClock(time: string | undefined, speed: number | undefined) {
|
||||
if (time !== undefined && (typeof time !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(time) || Number.isNaN(Date.parse(`${time}Z`)))) return false
|
||||
if (speed !== undefined && (typeof speed !== 'number' || !Number.isFinite(speed) || speed < 1 || speed > 3600)) return false
|
||||
const now = Date.now()
|
||||
this.state.clock = {
|
||||
wall: time ?? (this.state.clock ? clockWall(this.state.clock, now) : wallNow()),
|
||||
realAt: now,
|
||||
speed: speed ?? this.state.clock?.speed ?? 1,
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** 学生程序控制了输出设备;返回 true 表示实训台状态有变化 */
|
||||
applyDevice(message: Extract<RunnerMessage, { type: 'device' }>) {
|
||||
switch (`${message.device}/${message.action}`) {
|
||||
@@ -93,3 +119,11 @@ export class Board {
|
||||
this.state = defaultBoardState()
|
||||
}
|
||||
}
|
||||
|
||||
/** 服务器所在时区(config.timeZone)现在的墙上时间 yyyy-MM-ddTHH:mm:ss */
|
||||
export function wallNow(now = new Date()) {
|
||||
return new Intl.DateTimeFormat('sv-SE', {
|
||||
timeZone: config.timeZone,
|
||||
year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23',
|
||||
}).format(now).replace(' ', 'T')
|
||||
}
|
||||
@@ -17,13 +17,15 @@ export type Step =
|
||||
| { kind: 'edit'; control: string; row: number; column: number | string; value: string }
|
||||
/** 在日期时间选择器里选日期(和时间):2024-01-15 或 2024-01-15 08:30:00 */
|
||||
| { kind: 'pick'; control: string; value: string }
|
||||
/** 拨虚拟时钟到今天的某个时刻(HH:mm:ss),可以同时设倍速 */
|
||||
| { kind: 'clock'; time: string; speed?: number }
|
||||
| { kind: 'board'; analog: Partial<Record<AnalogKey, number>>; digital: Partial<Record<DigitalKey, boolean>> }
|
||||
| { kind: 'wait'; ms: number }
|
||||
/** desc:给学生看的期望说明,如"显示当前时间",不写就按期望自动生成 */
|
||||
| { kind: 'expect'; expect: Expectation; within: number; desc?: string }
|
||||
|
||||
export type Expectation =
|
||||
/** 控件属性等于某个值:text → Text,ledText → LedText(LED 控件),enabled → Enabled,checked → Checked,on → IsOn(灯泡、报警灯) */
|
||||
/** 控件属性等于某个值:text → Text,ledText → LedText(LED 控件),enabled → Enabled,checked → Checked,on → IsOn(灯泡、报警灯),value → Value(滑动条) */
|
||||
| { kind: 'prop'; control: string; prop: string; value: unknown }
|
||||
/** 控件文字包含某段文字;列表框是任意一个列表项包含 */
|
||||
| { kind: 'contains'; control: string; text: string }
|
||||
@@ -38,6 +40,8 @@ export type Expectation =
|
||||
| { kind: 'image'; control: string }
|
||||
/** 摄像头在传画面(on)/ 已关闭(off),或云台比开始时朝某个方向转过 */
|
||||
| { kind: 'camera'; state: 'on' | 'off' | 'up' | 'down' | 'left' | 'right' }
|
||||
/** 程序用媒体播放器播放了这个文件(如 ring.mp3) */
|
||||
| { kind: 'media'; name: string }
|
||||
/** 程序用 Image.Save 保存了图片,路径包含这段文字(如 Camera/) */
|
||||
| { kind: 'savedImage'; path: string }
|
||||
/** 数据表格里有(present)/ 没有一行同时满足这些"列标题=值" */
|
||||
@@ -47,8 +51,9 @@ export type Expectation =
|
||||
/** 期望默认最多等多久(毫秒);依赖定时器的检查在规则里写 within */
|
||||
const DEFAULT_WITHIN = 2000
|
||||
|
||||
const PROP_KEYS: Record<string, string> = { text: 'Text', ledText: 'LedText', enabled: 'Enabled', checked: 'Checked', on: 'IsOn' }
|
||||
const PROP_KEYS: Record<string, string> = { text: 'Text', ledText: 'LedText', enabled: 'Enabled', checked: 'Checked', on: 'IsOn', value: 'Value' }
|
||||
const STRING_PROPS = new Set(['Text', 'LedText'])
|
||||
const NUMBER_PROPS = new Set(['Value'])
|
||||
|
||||
/** "6s"、"500ms" 或毫秒数 */
|
||||
function duration(value: unknown, where: string): number {
|
||||
@@ -78,6 +83,7 @@ function parseExpectation(raw: Record<string, unknown>, where: string): Expectat
|
||||
if (!['on', 'off', 'up', 'down', 'left', 'right'].includes(state)) throw new Error(`${where}:camera 要写 on、off、up、down、left 或 right`)
|
||||
return { kind: 'camera', state: state as Extract<Expectation, { kind: 'camera' }>['state'] }
|
||||
}
|
||||
if ('media' in raw) return { kind: 'media', name: str(raw.media, where, '播放的文件名') }
|
||||
if ('savedImage' in raw) return { kind: 'savedImage', path: str(raw.savedImage, where, '保存的图片路径应包含的文字') }
|
||||
if ('relay' in raw) return { kind: 'relay', index: Number(raw.relay), on: bool(raw.on, where, 'on') }
|
||||
if ('zigbee' in raw) return { kind: 'zigbee', index: Number(raw.zigbee), on: bool(raw.on, where, 'on') }
|
||||
@@ -102,7 +108,10 @@ function parseExpectation(raw: Record<string, unknown>, where: string): Expectat
|
||||
}
|
||||
if ('image' in raw) return { kind: 'image', control }
|
||||
for (const [key, prop] of Object.entries(PROP_KEYS)) {
|
||||
if (key in raw) return { kind: 'prop', control, prop, value: STRING_PROPS.has(prop) ? String(raw[key]) : bool(raw[key], where, key) }
|
||||
if (key in raw) {
|
||||
if (NUMBER_PROPS.has(prop) && typeof raw[key] !== 'number') throw new Error(`${where}:${key} 要写数字`)
|
||||
return { kind: 'prop', control, prop, value: STRING_PROPS.has(prop) ? String(raw[key]) : NUMBER_PROPS.has(prop) ? raw[key] : bool(raw[key], where, key) }
|
||||
}
|
||||
}
|
||||
throw new Error(`${where}:不认识的期望 ${JSON.stringify(raw)}`)
|
||||
}
|
||||
@@ -140,6 +149,14 @@ function parseStep(raw: unknown, where: string): Step {
|
||||
return { kind, control: str(obj.control, where, '控件名 control'), value: String(obj.value ?? '') }
|
||||
case 'select':
|
||||
return { kind, control: str(obj.control, where, '控件名 control'), item: str(obj.item, where, '要选的项 item') }
|
||||
case 'clock': {
|
||||
// clock: 06:44:50 或 clock: { time: 06:44:50, speed: 60 }
|
||||
const time = typeof value === 'string' ? value : str(obj.time, where, '时刻 time')
|
||||
if (!/^\d{2}:\d{2}:\d{2}$/.test(time)) throw new Error(`${where}:时刻要写成 06:44:50(加引号)`)
|
||||
const speed = typeof value === 'object' && obj.speed !== undefined ? Number(obj.speed) : undefined
|
||||
if (speed !== undefined && !(speed >= 1 && speed <= 3600)) throw new Error(`${where}:倍速 speed 要在 1 到 3600 之间`)
|
||||
return { kind, time, ...(speed === undefined ? {} : { speed }) }
|
||||
}
|
||||
case 'pick': {
|
||||
const value = str(obj.value, where, '日期 value')
|
||||
if (!/^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$/.test(value)) throw new Error(`${where}:日期要写成 2024-01-15 或 2024-01-15 08:30:00`)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// 提示框自动点第一个按钮("确定"/"是"),免得程序一直等在 MessageBox.Show 上。
|
||||
import type { Subprocess } from 'bun'
|
||||
import type { ControlNode, RunnerInput, RunnerMessage, UserAction } from '@iot/shared'
|
||||
import { Board } from '../board'
|
||||
import { Board, wallNow } from '../board'
|
||||
import { readLines } from '../runner-process'
|
||||
import { runCommand } from '../sandbox'
|
||||
import type { Step } from './checks'
|
||||
@@ -18,6 +18,8 @@ export class ProgramDriver {
|
||||
readonly messageBoxes: { text: string; caption: string }[] = []
|
||||
/** 程序用 Image.Save 保存的图片路径 */
|
||||
readonly savedImages: string[] = []
|
||||
/** 媒体播放器播放过的文件名 */
|
||||
readonly played: string[] = []
|
||||
readonly exceptions: string[] = []
|
||||
console = ''
|
||||
exited = false
|
||||
@@ -111,6 +113,9 @@ export class ProgramDriver {
|
||||
case 'imageSaved':
|
||||
this.savedImages.push(message.path)
|
||||
break
|
||||
case 'media':
|
||||
if (message.action === 'play' && message.name) this.played.push(message.name)
|
||||
break
|
||||
case 'console':
|
||||
this.console += message.text
|
||||
break
|
||||
@@ -139,6 +144,12 @@ export class ProgramDriver {
|
||||
this.send({ type: 'action', id: node.id, ...action })
|
||||
}
|
||||
|
||||
/** 拨虚拟时钟到今天的某个时刻(服务器所在时区) */
|
||||
setClock(time: string, speed?: number) {
|
||||
this.board.setClock(`${wallNow().slice(0, 10)}T${time}`, speed)
|
||||
this.send(this.board.runnerInputs())
|
||||
}
|
||||
|
||||
setBoard(step: BoardStep) {
|
||||
this.board.applyInput({ type: 'boardInput', analog: step.analog, digital: step.digital })
|
||||
this.send(this.board.runnerInputs())
|
||||
|
||||
@@ -141,6 +141,10 @@ async function runStep(driver: ProgramDriver, step: Step) {
|
||||
driver.setBoard(step)
|
||||
await driver.settle()
|
||||
break
|
||||
case 'clock':
|
||||
driver.setClock(step.time, step.speed)
|
||||
await driver.settle()
|
||||
break
|
||||
case 'wait':
|
||||
await Bun.sleep(step.ms)
|
||||
break
|
||||
@@ -227,6 +231,8 @@ function satisfied(driver: ProgramDriver, e: Exclude<Expectation, { kind: 'uncha
|
||||
}
|
||||
case 'savedImage':
|
||||
return driver.savedImages.some((path) => path.includes(e.path))
|
||||
case 'media':
|
||||
return driver.played.includes(e.name)
|
||||
case 'row': {
|
||||
const node = driver.find(e.control)
|
||||
return node !== undefined && rowMatches(node, e.cells) === e.present
|
||||
@@ -244,6 +250,7 @@ function describeFailure(driver: ProgramDriver, e: Exclude<Expectation, { kind:
|
||||
if (e.prop === 'Enabled') return `期望${prefix} ${e.control} ${desc ?? (e.value ? '可用' : '变成灰色不可用')},实际${actual === false ? '不可用' : '可用'}`
|
||||
if (e.prop === 'Checked') return `期望${prefix} ${e.control} ${desc ?? (e.value ? '被选中' : '没有被选中')},实际${actual ? '被选中' : '没有被选中'}`
|
||||
if (e.prop === 'IsOn') return `期望${prefix}窗体上的 ${e.control} ${desc ?? (e.value ? '亮' : '灭')},实际是${actual ? '亮' : '灭'}的`
|
||||
if (e.prop === 'Value') return `期望${prefix} ${e.control} ${desc ?? `的值是 ${String(e.value)}`},实际是 ${String(actual)}`
|
||||
return `期望${prefix} ${e.control} 的 ${e.prop} 是 ${String(e.value)},实际是 ${String(actual)}`
|
||||
}
|
||||
case 'contains': {
|
||||
@@ -274,6 +281,8 @@ function describeFailure(driver: ProgramDriver, e: Exclude<Expectation, { kind:
|
||||
const saved = driver.savedImages.map((p) => `"${p}"`).join('、')
|
||||
return `期望${prefix}${desc ?? `保存图片到"${e.path}"`},${saved ? `实际保存的是 ${saved}` : '实际没有保存图片'}`
|
||||
}
|
||||
case 'media':
|
||||
return `期望${prefix}${desc ?? `播放 ${e.name}`},${driver.played.length ? `实际播放的是 ${driver.played.join('、')}` : '实际没有播放声音'}`
|
||||
case 'row': {
|
||||
const node = need(driver, e.control)
|
||||
const wanted = Object.entries(e.cells).map(([k, v]) => `${k}=${v}`).join(',')
|
||||
|
||||
@@ -35,7 +35,7 @@ async function prepare(id: string, name: string, edit?: { file: string; from: st
|
||||
|
||||
describe('参考答案全部通过', async () => {
|
||||
const ids = (await Array.fromAsync(new Bun.Glob('*/checks.yaml').scan({ cwd: SAMPLES }))).map((p) => p.split('/')[0]).sort()
|
||||
test('每个示例都有评分规则', () => expect(ids.length).toBe(15))
|
||||
test('每个示例都有评分规则', () => expect(ids.length).toBe(18))
|
||||
for (const id of ids) {
|
||||
test(id, async () => {
|
||||
const result = await grade(await prepare(id, id), await checksOf(id), id)
|
||||
@@ -110,6 +110,22 @@ describe('改坏的代码', () => {
|
||||
expect(result.results[0].message).toStartWith('程序出错:System.ArgumentNullException')
|
||||
}, 60000)
|
||||
|
||||
test('情景控制:照书上把手动开关灯的判断写反,点了灯不亮', async () => {
|
||||
const id = 'p7-t1-智慧教室'
|
||||
const dir = await prepare(id, 'lamp-reversed', { file: '教室情景模式.cs', from: ' if (IsLampOpen)\n', to: ' if (!IsLampOpen)\n' })
|
||||
const checks = [
|
||||
...(await checksOf(id)),
|
||||
...parseChecks(`
|
||||
- title: 能手动开关灯
|
||||
steps:
|
||||
- click: pic_lightsStatus
|
||||
- expect: { control: lamp, on: true, desc: 点开关后灯亮 }
|
||||
`),
|
||||
]
|
||||
const result = await grade(dir, checks, id)
|
||||
expect(result.results.at(-1)).toMatchObject({ passed: false, message: '期望窗体上的 lamp 点开关后灯亮,实际是灭的' })
|
||||
}, 60000)
|
||||
|
||||
test('关闭时忘了挂起线程', async () => {
|
||||
const id = 'p5-t2-烟雾'
|
||||
const dir = await prepare(id, 'thread', { file: 'Form1.cs', from: 'thd.Suspend();//挂起线程', to: '' })
|
||||
|
||||
@@ -4,6 +4,7 @@ import { afterAll, afterEach, describe, expect, test } from 'bun:test'
|
||||
import { cp, mkdir, rm } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import type { ControlNode, RunnerInput, RunnerMessage } from '@iot/shared'
|
||||
import { wallNow } from '../src/board'
|
||||
import { compileService } from '../src/compile-service'
|
||||
import { config, ROOT } from '../src/config'
|
||||
import { cpuLimitMessage, runCommand } from '../src/sandbox'
|
||||
@@ -332,7 +333,7 @@ test('项目4 任务2:打开摄像机显示画面,转动后画面变化,
|
||||
const center = imageOf(await first)
|
||||
expect(center).toMatchObject({ width: 640, height: 480 })
|
||||
// 图片数据先于(或随)属性变化发给浏览器,同一张图只发一次
|
||||
const sent = runner.messages.filter((m) => m.type === 'image' && m.key === center.key)
|
||||
const sent = runner.messages.filter((m) => m.type === 'blob' && m.key === center.key)
|
||||
expect(sent.length).toBe(1)
|
||||
expect(sent[0]).toMatchObject({ mime: 'image/jpeg' })
|
||||
|
||||
@@ -630,6 +631,56 @@ test('数据库:照书上的写法建库建表、插入、查询、SqlCommandB
|
||||
}
|
||||
}, 30000)
|
||||
|
||||
test('虚拟时钟:拨时间、倍速后 DateTime.Now / Today 跟着走,恢复后是真实时间', async () => {
|
||||
const { runner } = await runMain(
|
||||
'clock',
|
||||
`
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
Console.WriteLine("now=" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " today=" + DateTime.Today.ToString("yyyy-MM-dd"));
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}`,
|
||||
)
|
||||
expect((await runner.waitFor('compiled')).ok).toBe(true)
|
||||
await runner.waitFor('console')
|
||||
runner.send({ type: 'board', analog: ANALOG_DEFAULT, digital: DIGITAL_OFF, clock: { wall: '2024-01-16T06:44:58', realAt: Date.now(), speed: 60 } })
|
||||
// 60 倍速:真实的 1 秒是虚拟的 1 分钟
|
||||
await runner.next('console', (m) => /now=2024-01-16 06:4[6-9]/.test(m.text), 5000)
|
||||
runner.send({ type: 'board', analog: ANALOG_DEFAULT, digital: DIGITAL_OFF, clock: null })
|
||||
const real = await runner.next('console', (m) => !m.text.includes('2024-01-16'), 3000)
|
||||
// 学生程序按服务器配置的时区(config.timeZone)计时,CI 机器本身是 UTC
|
||||
expect(real.text).toContain(`today=${wallNow().slice(0, 10)}`)
|
||||
expect(runner.messages.some((m) => m.type === 'console' && m.text.includes('today=2024-01-16'))).toBe(true)
|
||||
}, 30000)
|
||||
|
||||
test('TrackBar:拖动时先 ValueChanged 再 Scroll,超出范围的值截到两端;代码设超出范围的 Value 报错', async () => {
|
||||
const { runner } = await runMain(
|
||||
'trackbar',
|
||||
`
|
||||
var form = new Form();
|
||||
var bar = new TrackBar { Name = "bar", Minimum = 18, Maximum = 32 };
|
||||
bar.ValueChanged += (s, e) => Console.WriteLine("changed=" + bar.Value);
|
||||
bar.Scroll += (s, e) => Console.WriteLine("scroll=" + bar.Value);
|
||||
form.Load += (s, e) =>
|
||||
{
|
||||
try { bar.Value = 40; } catch (ArgumentOutOfRangeException ex) { Console.WriteLine("error=" + ex.Message); }
|
||||
};
|
||||
form.Controls.Add(bar);
|
||||
Application.Run(form);`,
|
||||
'using System.Windows.Forms;',
|
||||
)
|
||||
const { form } = await runner.waitFor('formShown')
|
||||
const bar = findByName(form, 'bar')
|
||||
expect(bar.props).toMatchObject({ Minimum: 18, Maximum: 32, Value: 18 })
|
||||
runner.send({ type: 'action', id: bar.id, action: 'scroll', value: '26' })
|
||||
await runner.waitFor('console', (m) => m.text === 'scroll=26\n')
|
||||
runner.send({ type: 'action', id: bar.id, action: 'scroll', value: '99' })
|
||||
await runner.waitFor('console', (m) => m.text === 'scroll=32\n')
|
||||
const output = runner.messages.flatMap((m) => (m.type === 'console' ? [m.text] : [])).join('')
|
||||
expect(output).toContain("error=“40”的值对于“Value”无效。")
|
||||
expect(output).toContain('changed=26\nscroll=26\nchanged=32\nscroll=32\n')
|
||||
}, 30000)
|
||||
|
||||
test.skipIf(config.sandbox === 'none')('沙箱:看不到服务器文件、别的运行目录、环境变量和其他进程,工程目录只读', async () => {
|
||||
process.env.SIM_TEST_SECRET = '不该被看到'
|
||||
const { runner } = await runMain(
|
||||
|
||||
Reference in new issue
Block a user