import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' const read = path => readFile(new URL(`../${path}`, import.meta.url), 'utf8') const frameEditor = await read('src/views/components/element/FrameElement/index.vue') const frameBase = await read('src/views/components/element/FrameElement/BaseFrameElement.vue') const operate = await read('src/views/Editor/Canvas/Operate/index.vue') const canvasTool = await read('src/views/Editor/CanvasTool/index2.vue') const canvas = await read('src/views/Editor/Canvas/index.vue') const toolbars = await Promise.all([ read('src/components/CollapsibleToolbar/index.vue'), read('src/components/CollapsibleToolbar/index2.vue'), read('src/components/CollapsibleToolbar/index22.vue'), ]) // 编辑态与只读/学生态都必须把 772 路由到文章预览 assert.match(frameEditor, /ArticleReadingPreview/) assert.match(frameEditor, /isArticleReadingTool/) assert.match(frameBase, /ArticleReadingPreview/) assert.match(frameBase, /isArticleReadingTool/) assert.match(frameBase, /ARTICLE_READING_TOOL_TYPE/) // 锁定、重置、滚轮放行 assert.match(operate, /isArticleReadingTool/) assert.match(canvasTool, /ARTICLE_READING_TOOL_TYPE/) assert.match(canvasTool, /articleStore\.requestResetPreview/) assert.match(canvas, /article-reading-preview/) // 三个工具栏名称映射 for (const toolbar of toolbars) { assert.match(toolbar, /ARTICLE_READING_TOOL_TYPE/) assert.match(toolbar, /ssArticleReadingTool/) } // 编号只能来自命名常量:代码里不得出现 772 字面量(注释中说明编号是允许的) function stripComments(source) { return source .replace(//g, '') .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/(^|[^:])\/\/.*$/gm, '$1') } const codeFiles = { frameEditor, frameBase, operate, canvasTool, canvas, toolbar0: toolbars[0], toolbar1: toolbars[1], toolbar2: toolbars[2], } for (const [name, source] of Object.entries(codeFiles)) { assert.doesNotMatch( stripComments(source), /\b772\b/, `${name} must reference ARTICLE_READING_TOOL_TYPE, not the literal 772`, ) } // 话题讨论与句子跟读的既有编号不得被改写 assert.match(frameBase, /77: 'ssEnglishSpeakingTool'/) assert.match(frameBase, /81: 'ssH5Page'/) // 文章分支不得回落到话题讨论组件 const articleBranch = frameEditor.slice(frameEditor.indexOf('ArticleReadingPreview')) assert.doesNotMatch(articleBranch.slice(0, 200), /TopicDiscussionPreview/) // 老师端 Student runtime 按工具类型分流 const studentView = await read('src/views/Student/index.vue') const articleClassComposable = await read( 'src/views/Student/components/ArticleReadingClassPanel/useArticleClassSummary.ts', ) assert.match(studentView, /ArticleReadingClassPanel/) assert.match(studentView, /ARTICLE_READING_TOOL_TYPE/) assert.match(studentView, /TOPIC_DISCUSSION_TOOL_TYPE/) assert.match(articleClassComposable, /listArticleSessionsByConfig/) assert.match(articleClassComposable, /generateArticleClassSummary/) // 作业记录必须带真实工具号,不能写死 77 assert.match(studentView, /atool: String\(activityToolType\)/) assert.doesNotMatch(studentView, /atool: '77'/) // 通用结果弹窗/提交按钮对文章与话题一视同仁地抑制 assert.doesNotMatch(studentView, /currentSlideToolType !== 77/) assert.match(studentView, /articleReadingPanelRef\.value\?\.scheduleRefetch/) // 三个语言包都要有工具名与班级面板文案 for (const loc of ['cn', 'en', 'hk']) { const pack = JSON.parse(await read(`src/views/lang/${loc}.json`)) for (const key of [ 'ssArticleReadingTool', 'ssArticleReading', 'ssArticleClassSummary', 'ssArticleFilterCompleted', 'ssArticleFilterReading', ]) { assert.ok(pack[key], `${loc}.json is missing ${key}`) } } // ---- 编辑态老师预览不进统计(spec §4.5):三道门都要在 ArticleReadingPreview 里 ---- const articlePreview = await read('src/views/Editor/EnglishSpeaking/preview/ArticleReadingPreview.vue') // 门 1:runtime 判定不能只看 userid —— 编辑器 URL 里也有 userid。 // 身份判据用项目既有的 type(Student/index.vue:625:老师端 1 / 学生端 2)。 assert.match(articlePreview, /isStudentRuntime/) assert.match(articlePreview, /params\.get\('type'\)/) assert.match(articlePreview, /role === '2'/) // 门 2:编辑态把 null 传给引擎,不是空串 assert.match(articlePreview, /sessionUserId/) // 门 3:resume 探针与进度广播只在学生态跑 assert.match(articlePreview, /if \(!isStudentRuntime\.value\) return/) // 反向:不得再出现「只要有 userid 就当学生」的旧判据 assert.doesNotMatch(articlePreview, /startDisabled = computed\(\(\) => !hasArticle\.value \|\| !runtimeUserId\.value\)/) // 反向:不得拿 mode 当身份 —— mode=student 是视图,老师上课时也在这个视图里, // 用它会把上课页的老师当成学生(编辑态那个 bug 换个地方复发),还得再去和 // App.vue 的 localStorage viewMode 对账。type 是身份,与渲染哪个视图无关。 assert.doesNotMatch(articlePreview, /'student'/) assert.doesNotMatch(articlePreview, /currentViewMode/) assert.doesNotMatch(await read('src/App.vue'), /currentViewMode/) // ---- 报告的词级形状必须跟住后端契约(spec §6):段落下面是**扁平的 words**。 // 后端 c62d35a「flatten scoring/report to paragraph→words」把句层删了,前端的类型和 // 取值路径没跟着改,于是任何一张已完成的段落卡片一展开就 `paragraph.sentences` 是 // undefined、`.flatMap` 抛异常。类型说了谎,所以 type-check 也发现不了 —— 只能在这里钉。 const articleTypes = await read('src/types/articleReading.ts') assert.match(articleTypes, /words: ArticleWordResult\[\]/) assert.doesNotMatch(articleTypes, /sentences/) assert.doesNotMatch(articleTypes, /ArticleSentenceResult/) const paragraphCard = await read('src/views/Editor/EnglishSpeaking/preview/ArticleParagraphCard.vue') assert.match(paragraphCard, /props\.paragraph\.words/) assert.doesNotMatch(paragraphCard, /flattenParagraphWords/) assert.doesNotMatch( await read('src/views/Editor/EnglishSpeaking/preview/articleReadingModel.ts'), /sentences/, ) console.log('article reading routing: all assertions passed')