|
|
@@ -0,0 +1,177 @@
|
|
|
+/**
|
|
|
+ * 契约级单测 —— 签名页横屏适配(能力键 family.signature.landscape)
|
|
|
+ *
|
|
|
+ * 溯源:
|
|
|
+ * SRS: docs/superpowers/specs/2026-11-07-signature-page-landscape.md
|
|
|
+ * §3 AC-R2(签名页为横屏全屏;加载时切横屏、离开时恢复竖屏,H5 不支持时优雅降级)
|
|
|
+ * §5 WI-2(进入/退出横屏适配:微信 wx.setPageOrientation 切横屏;H5 降级不切横屏)
|
|
|
+ * 设计锚点: pages/signature/signature.vue 的 enterLandscape / restorePortrait 接线
|
|
|
+ * (onLoad / onUnload / doBack)+ wx.setPageOrientation('landscape' / 'portrait')
|
|
|
+ * 台账: tests/test-cases/signature.md(TC-LANDSCAPE-01~08)
|
|
|
+ *
|
|
|
+ * 覆盖(独立契约,与 signature.canvas2d.test.js / signature.landscape.superseded.js 不重名):
|
|
|
+ * TC-LANDSCAPE-01 enterLandscape / restorePortrait 方法存在
|
|
|
+ * TC-LANDSCAPE-02 onLoad 进入即切横屏(调 enterLandscape)
|
|
|
+ * TC-LANDSCAPE-03 onUnload 退出恢复竖屏(调 restorePortrait)
|
|
|
+ * TC-LANDSCAPE-04 doBack 在 navigateBack 前恢复竖屏
|
|
|
+ * TC-LANDSCAPE-05 enterLandscape 调 wx.setPageOrientation({ orientation: 'landscape' })
|
|
|
+ * TC-LANDSCAPE-06 restorePortrait 调 wx.setPageOrientation({ orientation: 'portrait' })
|
|
|
+ * TC-LANDSCAPE-07 H5(无 wx)降级不切横屏且不抛错
|
|
|
+ * TC-LANDSCAPE-08 doBack 防重复返回(_backed 标志),onUnload 恢复竖屏
|
|
|
+ *
|
|
|
+ * 本文件只读源码并求值 script 段,不修改任何业务代码。
|
|
|
+ */
|
|
|
+
|
|
|
+const fs = require('fs')
|
|
|
+const path = require('path')
|
|
|
+
|
|
|
+const PAGE = path.join(__dirname, '../../../pages/signature/signature.vue')
|
|
|
+
|
|
|
+function readScript(file) {
|
|
|
+ const src = fs.readFileSync(file, 'utf8')
|
|
|
+ const m = src.match(/<script>([\s\S]*?)<\/script>/)
|
|
|
+ if (!m) throw new Error('no <script> in ' + file)
|
|
|
+ return m[1]
|
|
|
+}
|
|
|
+
|
|
|
+// import 语句 -> 同名空桩变量(保留脚本内引用,避免 ReferenceError)
|
|
|
+function stubImports(code) {
|
|
|
+ return code.replace(/import\s+([\s\S]*?)\s+from\s+['"][^'"]+['"];?/g, function (_all, clause) {
|
|
|
+ const ids = String(clause)
|
|
|
+ .replace(/[{}]/g, ' ')
|
|
|
+ .split(',')
|
|
|
+ .map(function (x) {
|
|
|
+ const parts = x.trim().split(/\s+as\s+/)
|
|
|
+ return parts[parts.length - 1].trim()
|
|
|
+ })
|
|
|
+ .filter(function (x) {
|
|
|
+ return /^[A-Za-z_$][\w$]*$/.test(x)
|
|
|
+ })
|
|
|
+ return ids
|
|
|
+ .map(function (id) {
|
|
|
+ return 'var ' + id + ' = function () {};'
|
|
|
+ })
|
|
|
+ .join(' ')
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function loadOptions(file, sandbox) {
|
|
|
+ let code = stubImports(readScript(file)).replace(/export\s+default/, 'var __opts__ =')
|
|
|
+ code = code + '\nreturn __opts__'
|
|
|
+ const names = Object.keys(sandbox)
|
|
|
+ // eslint-disable-next-line no-new-func
|
|
|
+ const fn = new Function(names.join(','), code)
|
|
|
+ return fn.apply(
|
|
|
+ null,
|
|
|
+ names.map(function (n) {
|
|
|
+ return sandbox[n]
|
|
|
+ })
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+// 行为级测试用页面实例构造器:可注入 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
|
|
|
+}
|
|
|
+
|
|
|
+describe('签名页横屏适配契约 · family.signature.landscape', function () {
|
|
|
+ const pageScript = readScript(PAGE)
|
|
|
+
|
|
|
+ test('TC-LANDSCAPE-01 enterLandscape / restorePortrait 方法存在', function () {
|
|
|
+ expect(pageScript).toMatch(/enterLandscape\s*:/)
|
|
|
+ expect(pageScript).toMatch(/restorePortrait\s*:/)
|
|
|
+ // 两方法须为真实函数(行为可调)
|
|
|
+ const vm = makePageWith({ wx: undefined })
|
|
|
+ expect(typeof vm.enterLandscape).toBe('function')
|
|
|
+ expect(typeof vm.restorePortrait).toBe('function')
|
|
|
+ })
|
|
|
+
|
|
|
+ test('TC-LANDSCAPE-02 onLoad 进入即切横屏(调 enterLandscape)', function () {
|
|
|
+ expect(pageScript).toMatch(/onLoad\s*:\s*function[\s\S]*?enterLandscape\(\)/)
|
|
|
+ })
|
|
|
+
|
|
|
+ test('TC-LANDSCAPE-03 onUnload 退出恢复竖屏(调 restorePortrait)', function () {
|
|
|
+ expect(pageScript).toMatch(/onUnload\s*:\s*function[\s\S]*?restorePortrait\(\)/)
|
|
|
+ })
|
|
|
+
|
|
|
+ test('TC-LANDSCAPE-04 doBack 在 navigateBack 前恢复竖屏', function () {
|
|
|
+ expect(pageScript).toMatch(/doBack\s*:\s*function[\s\S]*?restorePortrait\(\)[\s\S]*?navigateBack\(\)/)
|
|
|
+ })
|
|
|
+
|
|
|
+ test('TC-LANDSCAPE-05 enterLandscape 调 wx.setPageOrientation landscape', function () {
|
|
|
+ const calls = []
|
|
|
+ const vm = makePageWith({
|
|
|
+ wx: { setPageOrientation: function (cfg) { calls.push(cfg) } }
|
|
|
+ })
|
|
|
+ vm.enterLandscape()
|
|
|
+ expect(calls.length).toBe(1)
|
|
|
+ expect(calls[0].orientation).toBe('landscape')
|
|
|
+ // 旋转后重算画布高度(complete 回调)
|
|
|
+ expect(typeof calls[0].complete).toBe('function')
|
|
|
+ })
|
|
|
+
|
|
|
+ test('TC-LANDSCAPE-06 restorePortrait 调 wx.setPageOrientation portrait', 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')
|
|
|
+ })
|
|
|
+
|
|
|
+ test('TC-LANDSCAPE-07 H5(无 wx)降级不切横屏且不抛错', function () {
|
|
|
+ let setCalled = false
|
|
|
+ const vm = makePageWith({
|
|
|
+ wx: {
|
|
|
+ setPageOrientation: function () { setCalled = true }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ // H5:wx 为 undefined
|
|
|
+ const h5 = makePageWith({ wx: undefined })
|
|
|
+ expect(function () { h5.enterLandscape() }).not.toThrow()
|
|
|
+ expect(function () { h5.restorePortrait() }).not.toThrow()
|
|
|
+ // 降级路径不得调用 setPageOrientation(不切横屏)
|
|
|
+ expect(setCalled).toBe(false)
|
|
|
+ })
|
|
|
+
|
|
|
+ test('TC-LANDSCAPE-08 doBack 防重复返回 + onUnload 恢复竖屏', 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'])
|
|
|
+ // onUnload 独立恢复竖屏(onUnload 走 opts.onUnload,单独构造)
|
|
|
+ const seq2 = []
|
|
|
+ const opts = loadOptions(PAGE, {
|
|
|
+ wx: { setPageOrientation: function (cfg) { seq2.push('orient:' + cfg.orientation) } },
|
|
|
+ uni: { getWindowInfo: function () { return { windowHeight: 390 } } }
|
|
|
+ })
|
|
|
+ const data = typeof opts.data === 'function' ? opts.data() : opts.data
|
|
|
+ const vm2 = Object.assign({}, data, opts.methods)
|
|
|
+ vm2.$refs = {}
|
|
|
+ opts.onUnload.call(vm2)
|
|
|
+ expect(seq2).toEqual(['orient:portrait'])
|
|
|
+ })
|
|
|
+})
|