release: prepare August 1 Android RC
This commit is contained in:
@@ -4,6 +4,11 @@ import { readFile } from 'node:fs/promises';
|
||||
import { test } from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import {
|
||||
renderAndroidPrivacy,
|
||||
validateAndroidPrivacyDocument,
|
||||
validateLegalUrlPair
|
||||
} from '../scripts/app-legal-config.mjs';
|
||||
|
||||
const mobileRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const readJson = async (path) => JSON.parse(await readFile(new URL(path, import.meta.url), 'utf8'));
|
||||
@@ -30,3 +35,128 @@ test('App 平台编译器已声明且与其他 uni 包同版本,避免 build:a
|
||||
assert.equal(appPlus, pkg.dependencies['@dcloudio/uni-h5']);
|
||||
assert.equal(appPlus, pkg.devDependencies['@dcloudio/vite-plugin-uni']);
|
||||
});
|
||||
|
||||
test('Android 自定义基座显式面向 Android 15 权限模型', async () => {
|
||||
const manifest = await readJson('../src/manifest.json');
|
||||
const android = manifest['app-plus']?.distribute?.android;
|
||||
|
||||
assert.equal(android?.packagename, 'com.yincheng.wygj');
|
||||
assert.equal(android?.targetSdkVersion, 35, '不得回退到 DCloud 默认的 targetSdkVersion 28 兼容模式');
|
||||
});
|
||||
|
||||
test('Android 启动阶段不主动索取设备信息或外部存储权限', async () => {
|
||||
const manifest = await readJson('../src/manifest.json');
|
||||
const android = manifest['app-plus']?.distribute?.android;
|
||||
const modules = manifest['app-plus']?.modules;
|
||||
|
||||
assert.equal(manifest['app-plus']?.distribute?.splashscreen?.useOriginalMsgbox, true);
|
||||
assert.equal(android?.permissionPhoneState?.request, 'none');
|
||||
assert.equal(android?.permissionExternalStorage?.request, 'none');
|
||||
assert.ok(Object.hasOwn(modules || {}, 'Camera'), '媒体功能仍需按用户操作动态申请相机权限');
|
||||
assert.ok(Object.hasOwn(modules || {}, 'Record'), '语音功能仍需按用户操作动态申请录音权限');
|
||||
});
|
||||
|
||||
test('正式法务地址必须使用不同的公网 HTTPS 页面', () => {
|
||||
assert.throws(() => validateLegalUrlPair('', 'https://legal.example.cn/privacy'), /required/);
|
||||
assert.throws(
|
||||
() => validateLegalUrlPair('http://legal.example.cn/terms', 'https://legal.example.cn/privacy'),
|
||||
/HTTPS/
|
||||
);
|
||||
assert.throws(
|
||||
() => validateLegalUrlPair('https://127.0.0.1/terms', 'https://legal.example.cn/privacy'),
|
||||
/public hostname/
|
||||
);
|
||||
assert.throws(
|
||||
() => validateLegalUrlPair('https://legal.example.cn/document', 'https://legal.example.cn/document'),
|
||||
/must be different/
|
||||
);
|
||||
assert.throws(
|
||||
() => validateLegalUrlPair('https://198.18.0.1/terms', 'https://legal.example.cn/privacy'),
|
||||
/public hostname/
|
||||
);
|
||||
assert.throws(
|
||||
() => validateLegalUrlPair('https://legal.invalid/terms', 'https://legal.example.cn/privacy'),
|
||||
/reserved hostname/
|
||||
);
|
||||
assert.doesNotThrow(() => validateLegalUrlPair(
|
||||
'https://198.51.99.1/terms',
|
||||
'https://fd-company.cn/privacy'
|
||||
));
|
||||
assert.doesNotThrow(() => validateLegalUrlPair(
|
||||
'https://fd-company.cn/terms',
|
||||
'https://fd-company.cn/privacy'
|
||||
));
|
||||
});
|
||||
|
||||
test('Android 隐私模板的首次与二次弹窗使用同一组最终链接', async () => {
|
||||
const template = await readFile(new URL('../androidPrivacy.json.example', import.meta.url), 'utf8');
|
||||
const rendered = renderAndroidPrivacy(
|
||||
template,
|
||||
'https://legal.company.cn/bangdao/terms',
|
||||
'https://legal.company.cn/bangdao/privacy'
|
||||
);
|
||||
const privacy = JSON.parse(rendered);
|
||||
|
||||
assert.deepEqual(validateAndroidPrivacyDocument(privacy), {
|
||||
termsUrl: 'https://legal.company.cn/bangdao/terms',
|
||||
privacyUrl: 'https://legal.company.cn/bangdao/privacy'
|
||||
});
|
||||
assert.doesNotMatch(rendered, /__TERMS_URL__|__PRIVACY_URL__/);
|
||||
|
||||
privacy.second.message = privacy.second.message.replace('/privacy', '/other-privacy');
|
||||
assert.throws(() => validateAndroidPrivacyDocument(privacy), /must use the same legal links/);
|
||||
});
|
||||
|
||||
test('Android 隐私配置写入 uni-app CLI 输入目录并校验编译产物', async () => {
|
||||
const configureScript = await readFile(
|
||||
new URL('../scripts/configure-android-privacy.mjs', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
const verifier = await readFile(
|
||||
new URL('../scripts/verify-app-assets.mjs', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
assert.match(configureScript, /resolve\(projectRoot, 'src', 'androidPrivacy\.json'\)/);
|
||||
assert.match(verifier, /dist\/build\/app\/androidPrivacy\.json/);
|
||||
});
|
||||
|
||||
test('App 登录页法务地址校验不依赖浏览器 URL 构造器', async () => {
|
||||
const legalService = await readFile(
|
||||
new URL('../src/services/legal.ts', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
assert.doesNotMatch(legalService, /new URL\(/);
|
||||
assert.match(legalService, /\^https:\\\/\\\/\(\[\^\/\?#\]\+\)/);
|
||||
assert.match(legalService, /legalLinksReady/);
|
||||
});
|
||||
|
||||
test('APK 门禁区分自定义调试基座与内置业务资源的测试包', async () => {
|
||||
const verifier = await readFile(
|
||||
new URL('../scripts/verify-android-apk.ps1', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
assert.match(verifier, /ValidateSet\('custom-base', 'dcloud-test', 'release'\)/);
|
||||
assert.match(verifier, /assets\\apps/);
|
||||
assert.match(verifier, /bundled androidPrivacy\.json/);
|
||||
assert.match(verifier, /DCloud custom base APK contains only the native debug runtime/);
|
||||
assert.match(verifier, /dist\\build\\app\\app-service\.js/);
|
||||
assert.match(verifier, /dist\\build\\app-plus\\app-service\.js/);
|
||||
assert.match(verifier, /fixed test SMS code/);
|
||||
assert.match(verifier, /'mobile number'\s*=/);
|
||||
});
|
||||
|
||||
test('8 月 1 日 RC 脚本默认拒绝脏工作区且记录完整构建证据', async () => {
|
||||
const script = await readFile(
|
||||
new URL('../../scripts/prepare-aug1-rc.ps1', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
assert.match(script, /requires a clean worktree/);
|
||||
assert.match(script, /AllowDirtyRehearsal/);
|
||||
assert.match(script, /RC build changed the previously clean worktree/);
|
||||
assert.match(script, /releaseEligible = \$releaseEligible/);
|
||||
assert.match(script, /APK metadata, legal links, signature, content match and sensitive-value scan/);
|
||||
});
|
||||
|
||||
@@ -92,9 +92,9 @@ test('候选端刷新失败不保留上一轮真实记录', async () => {
|
||||
pageSource('../src/pages/candidate/progress/index.vue')
|
||||
]);
|
||||
|
||||
assert.match(home, /catch \(error\) \{\n\s*clearData\(\);/);
|
||||
assert.match(materials, /catch \(error\) \{\n\s*clearData\(\);/);
|
||||
assert.match(progress, /catch \(error\) \{\n\s*clearData\(\);/);
|
||||
assert.match(home, /catch \(error\) \{\r?\n\s*clearData\(\);/);
|
||||
assert.match(materials, /catch \(error\) \{\r?\n\s*clearData\(\);/);
|
||||
assert.match(progress, /catch \(error\) \{\r?\n\s*clearData\(\);/);
|
||||
});
|
||||
|
||||
test('面试、资料、进度和预习页各自只承载一类真实任务', async () => {
|
||||
@@ -136,7 +136,7 @@ test('五个候选页面登录都使用 redirectTo 并保留当前任务页', as
|
||||
]);
|
||||
|
||||
sources.forEach((source) => {
|
||||
const goLogin = source.match(/const goLogin = \(\) => \{\n[\s\S]*?\n\};/)?.[0] || '';
|
||||
const goLogin = source.match(/const goLogin = \(\) => \{\r?\n[\s\S]*?\r?\n\};/)?.[0] || '';
|
||||
assert.match(goLogin, /rememberLoginRedirect\(currentPath\)/);
|
||||
assert.match(goLogin, /uni\.redirectTo\(\{ url: '\/pages\/auth\/login\/index' \}\)/);
|
||||
assert.doesNotMatch(goLogin, /uni\.navigateTo\(/);
|
||||
|
||||
@@ -161,6 +161,28 @@ test('移动端短信登录不再让客户端选择租户', async () => {
|
||||
assert.doesNotMatch(loginRequest, /tenantId:/);
|
||||
});
|
||||
|
||||
test('登录前协议默认不勾选且协议未配置时禁止发送验证码和登录', async () => {
|
||||
const source = await pageSource('../src/pages/auth/login/index.vue');
|
||||
|
||||
assert.match(source, /const agreed = ref\(false\)/);
|
||||
assert.match(source, /openLegalDocument\('terms'\)/);
|
||||
assert.match(source, /openLegalDocument\('privacy'\)/);
|
||||
assert.match(source, /const ensureLegalConsent = \(\) =>/);
|
||||
assert.match(source, /const sendCode = async \(\) => \{[\s\S]*?if \(!ensureLegalConsent\(\)\) return;/);
|
||||
assert.match(source, /const login = async \(\) => \{[\s\S]*?if \(!ensureLegalConsent\(\)\) return;/);
|
||||
assert.match(source, /协议地址尚未完成法务配置/);
|
||||
});
|
||||
|
||||
test('测试验证码提示必须由开发环境显式开关启用', async () => {
|
||||
const source = await pageSource('../src/pages/auth/login/index.vue');
|
||||
|
||||
assert.match(source, /v-if="showDevSmsHint"/);
|
||||
assert.match(source, /import\.meta\.env\.VITE_DEV_SMS_HINT_VALUE/);
|
||||
assert.match(source, /import\.meta\.env\.VITE_DEV_SMS_HINT_ENABLED === 'true'/);
|
||||
assert.match(source, /\/\^\\d\{4,6\}\$\/\.test\(devSmsHintValue\)/);
|
||||
assert.doesNotMatch(source, /123456/);
|
||||
});
|
||||
|
||||
test('共享视觉基座包含员工端主色和卡片圆角 token', async () => {
|
||||
const source = await pageSource('../src/styles/base.css');
|
||||
|
||||
@@ -691,9 +713,9 @@ test('直通车页支持语音输入并管控切页清理与字数上限', async
|
||||
assert.match(source, /slice\(0, MAX_CONTENT_LENGTH\)/);
|
||||
|
||||
// stop→onCapture 异步窗口:stopping 状态禁用重复 stop,避免 App 端二次 stop 使录音丢失。
|
||||
const stopBody = bodyOf(/const stopVoiceCapture = \(\) => \{([\s\S]*?)\};\n/);
|
||||
const stopBody = bodyOf(/const stopVoiceCapture = \(\) => \{([\s\S]*?)\};\r?\n/);
|
||||
assert.match(stopBody, /voiceStopping\.value = true/);
|
||||
const toggleBody = bodyOf(/const toggleVoiceCapture = \(\) => \{([\s\S]*?)\};\n/);
|
||||
const toggleBody = bodyOf(/const toggleVoiceCapture = \(\) => \{([\s\S]*?)\};\r?\n/);
|
||||
assert.match(toggleBody, /voiceStopping\.value/);
|
||||
|
||||
// 切页清理:onHide/onUnload 各自函数体内必须直接调用 teardownVoice(不允许跨函数误匹配)。
|
||||
@@ -705,7 +727,7 @@ test('直通车页支持语音输入并管控切页清理与字数上限', async
|
||||
assert.match(onUnloadBody, /teardownVoice\(\)/);
|
||||
|
||||
// teardown 必须同时处理:generation 递增、转写 abort、录音 cancel、状态复位、计时器停止。
|
||||
const teardownBody = bodyOf(/const teardownVoice = \(\) => \{([\s\S]*?)\};\n/);
|
||||
const teardownBody = bodyOf(/const teardownVoice = \(\) => \{([\s\S]*?)\};\r?\n/);
|
||||
assert.match(teardownBody, /voiceGeneration \+= 1/);
|
||||
assert.match(teardownBody, /abortVoiceTranscription\?\.\(\)/);
|
||||
assert.match(teardownBody, /voiceCapture\?\.cancel\(\)/);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
|
||||
const read = (path) => readFile(new URL(path, import.meta.url), 'utf8');
|
||||
|
||||
test('问师傅首屏提供政策制度快捷问题且继续走统一 Agent 请求', async () => {
|
||||
const [page, service] = await Promise.all([
|
||||
read('../src/pages/user/sop/index.vue'),
|
||||
read('../src/services/agent.ts')
|
||||
]);
|
||||
|
||||
assert.match(page, /const policyQuickQuestion = '问制度:请说明公司请假和考勤的适用规定'/);
|
||||
assert.match(page, /void sendQuestion\(value\)/);
|
||||
assert.match(service, /\/api\/aihr\/agent\/messages/);
|
||||
assert.doesNotMatch(page, /toolCode:\s*['"]POLICY/);
|
||||
});
|
||||
|
||||
test('直通车系统回执明确区分已收到、待处理和人工正式回复', async () => {
|
||||
const page = await read('../src/pages/user/direct/index.vue');
|
||||
|
||||
assert.match(page, /已收到,当前状态为“待处理”;可在“我的反馈”查看人工正式回复/);
|
||||
assert.match(page, /item\.status === 'REPLIED' \? '已回复' : '待处理'/);
|
||||
assert.match(page, /<text class="reply-label">正式回复<\/text>/);
|
||||
});
|
||||
|
||||
test('内置兜底与管理端演示不宣称帮道自动创建或派发工单', async () => {
|
||||
const [sopSeed, modelSeed, adminDemo] = await Promise.all([
|
||||
read('../../backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java'),
|
||||
read('../../backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java'),
|
||||
read('../../frontend/src/views/knowledge/sop.vue')
|
||||
]);
|
||||
const combined = [sopSeed, modelSeed, adminDemo].join('\n');
|
||||
|
||||
assert.match(combined, /既有工单系统/);
|
||||
assert.doesNotMatch(combined, /再创建工单/);
|
||||
assert.doesNotMatch(combined, /再生成工单闭环/);
|
||||
assert.match(modelSeed, /当前系统不会代为创建或派发工单/);
|
||||
});
|
||||
@@ -559,7 +559,7 @@ test('五个主管页面登录都使用 redirectTo 避免返回栈重复', () =>
|
||||
];
|
||||
|
||||
pages.forEach((source) => {
|
||||
const goLogin = source.match(/const goLogin = \(\) => \{\n[\s\S]*?\n\};/)?.[0] || '';
|
||||
const goLogin = source.match(/const goLogin = \(\) => \{\r?\n[\s\S]*?\r?\n\};/)?.[0] || '';
|
||||
assert.match(goLogin, /rememberLoginRedirect\(/);
|
||||
assert.match(goLogin, /uni\.redirectTo\(\{ url: '\/pages\/auth\/login\/index' \}\)/);
|
||||
assert.doesNotMatch(goLogin, /uni\.navigateTo\(/);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const source = (path) => readFile(new URL(path, import.meta.url), 'utf8');
|
||||
|
||||
@@ -141,13 +142,13 @@ test('工作上报服务声明整理契约并在正式提交携带 requestId', a
|
||||
|
||||
test('幂等键与异步账号守卫是可执行行为,不只是源码字符串', async (context) => {
|
||||
const { createServer } = await import('vite');
|
||||
const root = new URL('../', import.meta.url).pathname;
|
||||
const root = fileURLToPath(new URL('../', import.meta.url));
|
||||
const server = await createServer({
|
||||
root,
|
||||
configFile: false,
|
||||
appType: 'custom',
|
||||
logLevel: 'silent',
|
||||
resolve: { alias: { '@': new URL('../src', import.meta.url).pathname } },
|
||||
resolve: { alias: { '@': fileURLToPath(new URL('../src', import.meta.url)) } },
|
||||
server: { middlewareMode: true }
|
||||
});
|
||||
context.after(() => server.close());
|
||||
|
||||
Reference in New Issue
Block a user