|
@@ -1,38 +1,33 @@
|
|
|
/**
|
|
/**
|
|
|
- * 契约级单测 —— 手签名实现方式重写(竖屏直签 + canvas 2d,能力键 signature.canvas2d.rewrite)
|
|
|
|
|
|
|
+ * 契约级单测 —— 恢复进入签名页自动横屏(用户知情地推翻 11-09 竖屏直签)+ canvas 2d 不回归
|
|
|
*
|
|
*
|
|
|
* 溯源:
|
|
* 溯源:
|
|
|
- * SRS: docs/superpowers/specs/2026-11-09-signature-canvas2d-rewrite.md(DOC-2026-11-09-signature-canvas2d-rewrite,待开发)
|
|
|
|
|
- * 取代: docs/superpowers/specs/2026-11-08-signature-landscape-fix.md(整份作废,勿按旧实现断言)
|
|
|
|
|
|
|
+ * - 本轮恢复: 进入签名页自动强制横屏(wx.setPageOrientation landscape),退出恢复竖屏;
|
|
|
|
|
+ * H5 等不支持的环境降级为竖屏直签(不切横屏)。
|
|
|
|
|
+ * - 保留: 11-09 canvas 2d 重写(type="2d" + getContext('2d'))契约不回退。
|
|
|
* 台账: tests/test-cases/signature.md(TC-SIG2D-01~08)
|
|
* 台账: tests/test-cases/signature.md(TC-SIG2D-01~08)
|
|
|
*
|
|
*
|
|
|
- * 覆盖(v2 任务书 §三):
|
|
|
|
|
- * S4-1 竖屏直签(signature.vue)
|
|
|
|
|
- * TC-SIG2D-01 删除 enterLandscape()/restorePortrait() 方法
|
|
|
|
|
- * TC-SIG2D-01b onLoad/onUnload 不再调用横屏方法;模板删 h5DegradeHint/.rotate-hint
|
|
|
|
|
- * TC-SIG2D-02 computeCanvasHeight 保留竖屏计算(174/214 两档 + 160 下限)
|
|
|
|
|
- * S4-2 canvas 2d(hand-signature.vue)
|
|
|
|
|
- * TC-SIG2D-03 模板 :id + type="2d"(非 canvas-id)
|
|
|
|
|
- * TC-SIG2D-03b data 含 canvasId='hsCanvas_'+(_uid||0) 与 _canvasNode:null
|
|
|
|
|
- * TC-SIG2D-03c data 含 canvasWidth/canvasHeight/dpr/_destroyed/_initTimer/_rectLeft/_rectTop(2d 绘制与初始化防御所需)
|
|
|
|
|
- * TC-SIG2D-04 mounted 走 createSelectorQuery().in(this).select().fields({node:true,...})
|
|
|
|
|
- * TC-SIG2D-05 触摸坐标优先 t.x/t.y,兜底 clientX-rectLeft(不依赖旧 getPos 缓存 rect)
|
|
|
|
|
- * TC-SIG2D-06 onTouchMove 同步 ctx.stroke() 绘制(无 ctx.draw)
|
|
|
|
|
- * TC-SIG2D-07 导出 canvasToTempFilePath({canvas:_canvasNode});清空 ctx.clearRect
|
|
|
|
|
- * TC-SIG2D-01-E 2d 初始化失败重试 + _destroyed 防销毁竞态(mounted 重试链、beforeDestroy 清理、initCanvas 守卫)
|
|
|
|
|
- * TC-SIG2D-02-E 导出降级链路保持:H5 直接透传 dataURL、小程序 fs.readFile 转 base64(既有链路不回归)
|
|
|
|
|
|
|
+ * 覆盖:
|
|
|
|
|
+ * S4-1 恢复自动横屏(signature.vue)
|
|
|
|
|
+ * TC-SIG2D-01 恢复 enterLandscape/restorePortrait 并正确接线(onLoad/onUnload/doBack)
|
|
|
|
|
+ * TC-SIG2D-01b 模板不渲染 h5DegradeHint/.rotate-hint(本轮未重新引入)
|
|
|
|
|
+ * TC-SIG2D-01c 调 wx.setPageOrientation(landscape + portrait)
|
|
|
|
|
+ * TC-SIG2D-01d~01j 行为级:H5 降级不切横屏 / success+complete 重算高度 / 窗口监听成对注册注销 / doBack 恢复竖屏 / onUnload 清理
|
|
|
|
|
+ * TC-SIG2D-02 computeCanvasHeight 保留(174/214 两档 + 160 下限)
|
|
|
|
|
+ * S4-2 canvas 2d(hand-signature.vue,不回归)
|
|
|
|
|
+ * TC-SIG2D-03~07、01-E、02-E 保持不变
|
|
|
|
|
+ * TC-SIG2D-07b height watch 仅在值变化且非 savingLive/销毁时重建(书写中不丢笔迹)
|
|
|
* S4-3 不回归
|
|
* S4-3 不回归
|
|
|
- * TC-SIG2D-08 组件对外契约 props/emit 不变(value/strokeColor/strokeWidth/height/requireAgree/agreeText + confirm/clear/agree-change)
|
|
|
|
|
|
|
+ * TC-SIG2D-08 组件对外契约 props/emit 不变(value/strokeColor/strokeWidth/height/requireAgree/agreeText + confirm/clear/agree-change)
|
|
|
*
|
|
*
|
|
|
* 本文件只读源码并求值 script 段,不修改任何业务代码。
|
|
* 本文件只读源码并求值 script 段,不修改任何业务代码。
|
|
|
- * 当前实现 = v2「待开发」,本文件内新契约断言应为 RED(除 02/08/02-E 保留不变的既有契约)。
|
|
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
const fs = require('fs')
|
|
const fs = require('fs')
|
|
|
const path = require('path')
|
|
const path = require('path')
|
|
|
|
|
|
|
|
const PAGE = path.join(__dirname, '../../../pages/signature/signature.vue')
|
|
const PAGE = path.join(__dirname, '../../../pages/signature/signature.vue')
|
|
|
-const PAD = path.join(__dirname, '../../../components/hand-signature/hand-signature.vue')
|
|
|
|
|
|
|
+const PAD = path.join(__dirname, '../../../pages/signature/components/hand-signature/hand-signature.vue')
|
|
|
|
|
|
|
|
function readScript(file) {
|
|
function readScript(file) {
|
|
|
const src = fs.readFileSync(file, 'utf8')
|
|
const src = fs.readFileSync(file, 'utf8')
|
|
@@ -100,6 +95,24 @@ function makePage(windowHeight, requireAgree) {
|
|
|
return vm
|
|
return vm
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 行为级测试用的页面实例构造器:可注入 wx/window/uni 桩,便于验证横屏/监听/回退链路
|
|
|
|
|
+function makePageWith(sandbox) {
|
|
|
|
|
+ const base = {
|
|
|
|
|
+ uni: { getWindowInfo: function () { return { windowHeight: 390, windowWidth: 844 } } },
|
|
|
|
|
+ wx: undefined,
|
|
|
|
|
+ window: undefined,
|
|
|
|
|
+ }
|
|
|
|
|
+ const merged = Object.assign({}, base, sandbox || {})
|
|
|
|
|
+ if (sandbox && sandbox.uni) {
|
|
|
|
|
+ merged.uni = Object.assign({}, base.uni, sandbox.uni)
|
|
|
|
|
+ }
|
|
|
|
|
+ const opts = loadOptions(PAGE, merged)
|
|
|
|
|
+ const data = typeof opts.data === 'function' ? opts.data() : opts.data
|
|
|
|
|
+ const vm = Object.assign({}, data, opts.methods)
|
|
|
|
|
+ vm.$refs = {}
|
|
|
|
|
+ return vm
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function styleBlock(selector) {
|
|
function styleBlock(selector) {
|
|
|
const src = fs.readFileSync(PAGE, 'utf8')
|
|
const src = fs.readFileSync(PAGE, 'utf8')
|
|
|
const i = src.indexOf(selector + ' {')
|
|
const i = src.indexOf(selector + ' {')
|
|
@@ -108,36 +121,149 @@ function styleBlock(selector) {
|
|
|
return src.slice(open + 1, src.indexOf('}', open))
|
|
return src.slice(open + 1, src.indexOf('}', open))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-describe('签名实现重写 v2(竖屏直签 + canvas 2d)· signature.canvas2d.rewrite', function () {
|
|
|
|
|
|
|
+describe('恢复签名页自动横屏 + canvas 2d 不回归 · signature.canvas2d.rewrite', function () {
|
|
|
const pageScript = readScript(PAGE)
|
|
const pageScript = readScript(PAGE)
|
|
|
const pageTpl = readTemplate(PAGE)
|
|
const pageTpl = readTemplate(PAGE)
|
|
|
const padScript = readScript(PAD)
|
|
const padScript = readScript(PAD)
|
|
|
const padTpl = readTemplate(PAD)
|
|
const padTpl = readTemplate(PAD)
|
|
|
|
|
|
|
|
- // ---------- S4-1 竖屏直签(signature.vue) ----------
|
|
|
|
|
|
|
+ // ---------- S4-1 恢复自动横屏(signature.vue) ----------
|
|
|
|
|
|
|
|
- test('TC-SIG2D-01 删除横屏方法 enterLandscape/restorePortrait', function () {
|
|
|
|
|
- expect(pageScript).not.toMatch(/enterLandscape\s*:/)
|
|
|
|
|
- expect(pageScript).not.toMatch(/restorePortrait\s*:/)
|
|
|
|
|
- // 全文件不应再引用该方法名(含 onLoad/onUnload 调用点与注释外的 toString)
|
|
|
|
|
- expect(pageScript).not.toMatch(/enterLandscape\(/)
|
|
|
|
|
- expect(pageScript).not.toMatch(/restorePortrait\(/)
|
|
|
|
|
|
|
+ test('TC-SIG2D-01 恢复横屏方法 enterLandscape/restorePortrait 并正确接线', function () {
|
|
|
|
|
+ expect(pageScript).toMatch(/enterLandscape\s*:/)
|
|
|
|
|
+ expect(pageScript).toMatch(/restorePortrait\s*:/)
|
|
|
|
|
+ // onLoad 末尾进入横屏并注册尺寸监听
|
|
|
|
|
+ expect(pageScript).toMatch(/onLoad\s*:\s*function[\s\S]*?enterLandscape\(\)/)
|
|
|
|
|
+ expect(pageScript).toMatch(/onLoad\s*:\s*function[\s\S]*?bindResizeListener\(\)/)
|
|
|
|
|
+ // onUnload 注销监听并恢复竖屏
|
|
|
|
|
+ expect(pageScript).toMatch(/onUnload\s*:\s*function[\s\S]*?unbindResizeListener\(\)/)
|
|
|
|
|
+ expect(pageScript).toMatch(/onUnload\s*:\s*function[\s\S]*?restorePortrait\(\)/)
|
|
|
|
|
+ // doBack 在 navigateBack 前恢复竖屏
|
|
|
|
|
+ expect(pageScript).toMatch(/doBack\s*:\s*function[\s\S]*?restorePortrait\(\)[\s\S]*?navigateBack\(\)/)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- test('TC-SIG2D-01b onLoad/onUnload 不再调用横屏依赖;模板删除 h5DegradeHint/.rotate-hint', function () {
|
|
|
|
|
- // 页面模板不再渲染 H5 降级提示块
|
|
|
|
|
|
|
+ test('TC-SIG2D-01b 模板不渲染 H5 降级提示块 h5DegradeHint/.rotate-hint', function () {
|
|
|
|
|
+ // 本轮恢复横屏未重新引入 H5 降级提示块
|
|
|
expect(pageTpl).not.toMatch(/h5DegradeHint/)
|
|
expect(pageTpl).not.toMatch(/h5DegradeHint/)
|
|
|
expect(pageTpl).not.toMatch(/rotate-hint/)
|
|
expect(pageTpl).not.toMatch(/rotate-hint/)
|
|
|
// 样式块中无 .rotate-hint
|
|
// 样式块中无 .rotate-hint
|
|
|
expect(styleBlock('.rotate-hint')).toBe('')
|
|
expect(styleBlock('.rotate-hint')).toBe('')
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- test('TC-SIG2D-01c 不再调用 wx.setPageOrientation / screen.orientation.lock', function () {
|
|
|
|
|
- // 只匹配「调用形式」(带括号),排除注释中的引用文字(v2 实现注释明确写"不再调用…")
|
|
|
|
|
- expect(pageScript).not.toMatch(/setPageOrientation\s*\(/)
|
|
|
|
|
|
|
+ test('TC-SIG2D-01c 调用 wx.setPageOrientation 切横屏/竖屏(landscape + portrait)', function () {
|
|
|
|
|
+ // 只匹配「调用形式」(带括号),排除注释中的引用文字
|
|
|
|
|
+ expect(pageScript).toMatch(/setPageOrientation\s*\(\s*\{/)
|
|
|
|
|
+ expect(pageScript).toMatch(/orientation\s*:\s*['"]landscape['"]/)
|
|
|
|
|
+ expect(pageScript).toMatch(/orientation\s*:\s*['"]portrait['"]/)
|
|
|
|
|
+ // 不使用 screen.orientation.lock(H5 降级为 wx 判断不切横屏)
|
|
|
expect(pageScript).not.toMatch(/orientation\.lock\s*\(/)
|
|
expect(pageScript).not.toMatch(/orientation\.lock\s*\(/)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ test('TC-SIG2D-01d enterLandscape 在 H5(无 wx)降级不切横屏且不抛错', function () {
|
|
|
|
|
+ const vm = makePageWith({ wx: undefined })
|
|
|
|
|
+ expect(function () { vm.enterLandscape() }).not.toThrow()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ test('TC-SIG2D-01e enterLandscape 调 setPageOrientation(landscape),complete 回调重算高度(success 不重复)', function () {
|
|
|
|
|
+ const calls = []
|
|
|
|
|
+ const vm = makePageWith({
|
|
|
|
|
+ wx: { setPageOrientation: function (cfg) { calls.push(cfg) } }
|
|
|
|
|
+ })
|
|
|
|
|
+ let refresh = 0
|
|
|
|
|
+ vm.refreshCanvasHeight = function () { refresh += 1 }
|
|
|
|
|
+ vm.enterLandscape()
|
|
|
|
|
+ expect(calls.length).toBe(1)
|
|
|
|
|
+ expect(calls[0].orientation).toBe('landscape')
|
|
|
|
|
+ // 加固后:仅 complete 重算一次(success 与 complete 不再重复计算)
|
|
|
|
|
+ expect(calls[0].success).toBeUndefined()
|
|
|
|
|
+ calls[0].complete()
|
|
|
|
|
+ expect(refresh).toBe(1)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ test('TC-SIG2D-01f restorePortrait 调 setPageOrientation(portrait);H5 降级不抛错', function () {
|
|
|
|
|
+ const calls = []
|
|
|
|
|
+ const vm = makePageWith({
|
|
|
|
|
+ wx: { setPageOrientation: function (cfg) { calls.push(cfg) } }
|
|
|
|
|
+ })
|
|
|
|
|
+ vm.restorePortrait()
|
|
|
|
|
+ expect(calls.length).toBe(1)
|
|
|
|
|
+ expect(calls[0].orientation).toBe('portrait')
|
|
|
|
|
+
|
|
|
|
|
+ const h5 = makePageWith({ wx: undefined })
|
|
|
|
|
+ expect(function () { h5.restorePortrait() }).not.toThrow()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ test('TC-SIG2D-01g 窗口监听成对注册/注销(wx 通道,无泄漏)', function () {
|
|
|
|
|
+ const on = []
|
|
|
|
|
+ const off = []
|
|
|
|
|
+ const vm = makePageWith({
|
|
|
|
|
+ wx: {
|
|
|
|
|
+ onWindowResize: function (h) { on.push(h) },
|
|
|
|
|
+ offWindowResize: function (h) { off.push(h) }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ vm.bindResizeListener()
|
|
|
|
|
+ expect(on.length).toBe(1)
|
|
|
|
|
+ expect(vm._resizeHandler).toBeTruthy()
|
|
|
|
|
+ vm.unbindResizeListener()
|
|
|
|
|
+ expect(off.length).toBe(1)
|
|
|
|
|
+ expect(off[0]).toBe(on[0])
|
|
|
|
|
+ expect(vm._resizeHandler).toBeNull()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ test('TC-SIG2D-01h 窗口监听成对注册/注销(H5 window 通道,无泄漏)', function () {
|
|
|
|
|
+ const on = []
|
|
|
|
|
+ const off = []
|
|
|
|
|
+ const vm = makePageWith({
|
|
|
|
|
+ wx: undefined,
|
|
|
|
|
+ window: {
|
|
|
|
|
+ addEventListener: function (name, h) { on.push([name, h]) },
|
|
|
|
|
+ removeEventListener: function (name, h) { off.push([name, h]) }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ vm.bindResizeListener()
|
|
|
|
|
+ expect(on.length).toBe(1)
|
|
|
|
|
+ expect(on[0][0]).toBe('resize')
|
|
|
|
|
+ vm.unbindResizeListener()
|
|
|
|
|
+ expect(off.length).toBe(1)
|
|
|
|
|
+ expect(off[0][1]).toBe(on[0][1])
|
|
|
|
|
+ expect(vm._resizeHandler).toBeNull()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ test('TC-SIG2D-01i doBack 在 navigateBack 前恢复竖屏,_backed 防重复返回', function () {
|
|
|
|
|
+ const seq = []
|
|
|
|
|
+ const vm = makePageWith({
|
|
|
|
|
+ wx: { setPageOrientation: function (cfg) { seq.push('orient:' + cfg.orientation) } },
|
|
|
|
|
+ uni: {
|
|
|
|
|
+ getWindowInfo: function () { return { windowHeight: 390 } },
|
|
|
|
|
+ navigateBack: function () { seq.push('navigateBack') }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ vm.doBack()
|
|
|
|
|
+ expect(seq).toEqual(['orient:portrait', 'navigateBack'])
|
|
|
|
|
+ vm.doBack()
|
|
|
|
|
+ expect(seq).toEqual(['orient:portrait', 'navigateBack'])
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ test('TC-SIG2D-01j onUnload 注销监听并恢复竖屏', function () {
|
|
|
|
|
+ const seq = []
|
|
|
|
|
+ const sandbox = {
|
|
|
|
|
+ wx: {
|
|
|
|
|
+ setPageOrientation: function (cfg) { seq.push('orient:' + cfg.orientation) },
|
|
|
|
|
+ onWindowResize: function () {},
|
|
|
|
|
+ offWindowResize: function () { seq.push('offResize') }
|
|
|
|
|
+ },
|
|
|
|
|
+ uni: { getWindowInfo: function () { return { windowHeight: 390 } } }
|
|
|
|
|
+ }
|
|
|
|
|
+ const opts = loadOptions(PAGE, sandbox)
|
|
|
|
|
+ const data = typeof opts.data === 'function' ? opts.data() : opts.data
|
|
|
|
|
+ const vm = Object.assign({}, data, opts.methods)
|
|
|
|
|
+ vm.$refs = {}
|
|
|
|
|
+ vm.bindResizeListener()
|
|
|
|
|
+ opts.onUnload.call(vm)
|
|
|
|
|
+ expect(seq).toEqual(['offResize', 'orient:portrait'])
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
test('TC-SIG2D-02 computeCanvasHeight 保留竖屏计算(174/214 两档 + 160 下限)', function () {
|
|
test('TC-SIG2D-02 computeCanvasHeight 保留竖屏计算(174/214 两档 + 160 下限)', function () {
|
|
|
expect(makePage(390, false).computeCanvasHeight()).toBe(390 - 174)
|
|
expect(makePage(390, false).computeCanvasHeight()).toBe(390 - 174)
|
|
|
expect(makePage(390, true).computeCanvasHeight()).toBe(390 - 214)
|
|
expect(makePage(390, true).computeCanvasHeight()).toBe(390 - 214)
|
|
@@ -205,6 +331,38 @@ describe('签名实现重写 v2(竖屏直签 + canvas 2d)· signature.canvas
|
|
|
expect(padScript).not.toMatch(/clearRect[\s\S]{0,80}ctx\.draw\(\)/)
|
|
expect(padScript).not.toMatch(/clearRect[\s\S]{0,80}ctx\.draw\(\)/)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ test('TC-SIG2D-07b height watch 仅在值变化且非 savingLive/销毁/已书写时重建 2d 位图', function () {
|
|
|
|
|
+ const opts = loadOptions(PAD, { uni: {}, wx: undefined, window: undefined })
|
|
|
|
|
+ const watchFn = opts.watch && opts.watch.height
|
|
|
|
|
+ expect(typeof watchFn).toBe('function')
|
|
|
|
|
+
|
|
|
|
|
+ let initCalls = 0
|
|
|
|
|
+ function makeSelf(overrides) {
|
|
|
|
|
+ return Object.assign({
|
|
|
|
|
+ savingLive: false,
|
|
|
|
|
+ _destroyed: false,
|
|
|
|
|
+ hasDrawn: false,
|
|
|
|
|
+ $nextTick: function (cb) { cb() },
|
|
|
|
|
+ initCanvas: function (cb) { initCalls += 1; if (cb) cb(true) }
|
|
|
|
|
+ }, overrides || {})
|
|
|
|
|
+ }
|
|
|
|
|
+ // 值未变化:不重建
|
|
|
|
|
+ watchFn.call(makeSelf(), 200, 200)
|
|
|
|
|
+ expect(initCalls).toBe(0)
|
|
|
|
|
+ // 导出中(savingLive=true):不重建,避免清空书写中的笔迹
|
|
|
|
|
+ watchFn.call(makeSelf({ savingLive: true }), 220, 200)
|
|
|
|
|
+ expect(initCalls).toBe(0)
|
|
|
|
|
+ // 组件已销毁:不重建
|
|
|
|
|
+ watchFn.call(makeSelf({ _destroyed: true }), 220, 200)
|
|
|
|
|
+ expect(initCalls).toBe(0)
|
|
|
|
|
+ // 加固:已书写(hasDrawn=true)时跳过重建,避免清空笔迹却仍标记 hasContent 导致导出空白
|
|
|
|
|
+ watchFn.call(makeSelf({ hasDrawn: true }), 220, 200)
|
|
|
|
|
+ expect(initCalls).toBe(0)
|
|
|
|
|
+ // 正常值变化:$nextTick 后重建一次
|
|
|
|
|
+ watchFn.call(makeSelf(), 220, 200)
|
|
|
|
|
+ expect(initCalls).toBe(1)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
// ---------- 异常流契约(台账 TC-SIG2D-01-E / 02-E) ----------
|
|
// ---------- 异常流契约(台账 TC-SIG2D-01-E / 02-E) ----------
|
|
|
|
|
|
|
|
test('TC-SIG2D-01-E 2d 初始化失败重试 + _destroyed 防销毁竞态', function () {
|
|
test('TC-SIG2D-01-E 2d 初始化失败重试 + _destroyed 防销毁竞态', function () {
|