import test from 'node:test'; import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { JSDOM } from 'jsdom'; import { parse, compileScript, compileTemplate } from '@vue/compiler-sfc'; import ts from 'typescript'; // Mount the actual uni-app checkbox, group and label; do not replace their click behavior. const dom = new JSDOM('', { url: 'http://localhost/', }); for (const key of ['window', 'document', 'Element', 'HTMLElement', 'SVGElement', 'Node']) { globalThis[key] = dom.window[key]; } globalThis.__uniConfig = {}; globalThis.getCurrentPages = () => []; const require = createRequire(import.meta.url); const vue = require('vue'); const { Checkbox, CheckboxGroup, Label, Input } = require('@dcloudio/uni-h5'); const source = await readFile(new URL('../src/pages/account/login.vue', import.meta.url), 'utf8'); const { descriptor } = parse(source, { filename: 'login.vue' }); const script = compileScript(descriptor, { id: 'login-agreement-dom' }); const components = { checkbox: 'UniCheckbox', 'checkbox-group': 'UniCheckboxGroup', label: 'UniLabel', input: 'UniInput', }; const template = compileTemplate({ source: descriptor.template.content.replace( /<(\/?)(checkbox-group|checkbox|label|input)(?=[\s/>])/g, (_, closing, tag) => '<' + closing + components[tag], ), filename: 'login.vue', id: 'login-agreement-dom', compilerOptions: { bindingMetadata: script.bindings, isNativeTag: (tag) => ['view', 'text', 'button'].includes(tag), }, }); assert.deepEqual(template.errors, []); function evaluate(code, dependencies) { const { outputText } = ts.transpileModule(code, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }, }); const exports = {}; new Function('require', 'exports', outputText)((name) => { if (name in dependencies) return dependencies[name]; throw new Error('Unexpected dependency: ' + name); }, exports); return exports; } async function click(element) { assert.ok(element, 'click target exists'); element.click(); await vue.nextTick(); await Promise.resolve(); } async function fixture(t, code = '', phone = '13012345678') { const posts = [], messages = [], accepted = [], navigations = []; globalThis.uni = { reLaunch: ({ url }) => navigations.push(url) }; const configuration = { privacy_url: 'https://example.test/privacy', capabilities: { sms_login: true, test_phone_login: true }, }; const login = { access_token: 'fixture-token', user_id: '42', identities: [] }; const component = evaluate(script.content, { vue, '@dcloudio/uni-app': { onShow() {} }, '../../stores/session': { session: vue.reactive({ configuration }), loadConfiguration() {}, acceptLogin: (value) => accepted.push(value), accountPage: () => '/pages/home/index', }, '../../api/client': {}, '../../utils/format': { toast: (message) => messages.push(message), go: (url) => navigations.push(url), }, '../../utils/login-flow': { isTestPhone: (phone) => ['13012345678', '13012345679', '13012345680'].includes(phone), requestPhoneCode: async () => true, loginByPhoneCode: async (phone, code) => { posts.push({ phone, code }); return login; }, }, }).default; component.render = evaluate(template.code, { vue }).render; const root = document.createElement('div'); document.body.append(root); const app = vue.createApp(component); app.component('UniCheckbox', Checkbox); app.component('UniCheckboxGroup', CheckboxGroup); app.component('UniLabel', Label); app.component('UniInput', Input); app.mount(root); t.after(() => { app.unmount(); root.remove(); }); await click(root.querySelector('.login-option')); const phoneInput = root.querySelector('.phone-login input'); phoneInput.value = phone; phoneInput.dispatchEvent(new window.Event('input', { bubbles: true })); await vue.nextTick(); const codeInput = root.querySelector('.code-field input'); if (codeInput) { codeInput.value = code; codeInput.dispatchEvent(new window.Event('input', { bubbles: true })); } await vue.nextTick(); const consent = () => [...root.querySelectorAll('uni-label text')].find((element) => element.textContent.includes('我已阅读并同意'), ); const checked = () => Boolean(root.querySelector('uni-checkbox svg')); const submit = () => click(root.querySelector('.phone-login .primary')); return { root, posts, messages, accepted, navigations, consent, checked, submit }; } test('clicking the agreement text updates the actual checkbox and permits login', async (t) => { const app = await fixture(t); await app.submit(); assert.match(app.messages.at(-1), /请先阅读并同意/); await click(app.consent()); assert.equal(app.checked(), true); await app.submit(); assert.equal(app.root.querySelector('.code-field'), null); assert.deepEqual(app.posts, [{ phone: '13012345678', code: '' }]); assert.equal(app.accepted.length, 1); }); test('checkbox and label clicks remain synchronized when repeatedly selecting and clearing', async (t) => { const app = await fixture(t); await click(app.root.querySelector('uni-checkbox')); assert.equal(app.checked(), true); await click(app.consent()); assert.equal(app.checked(), false); await app.submit(); assert.equal(app.posts.length, 0); assert.match(app.messages.at(-1), /请先阅读并同意/); await click(app.consent()); await app.submit(); assert.equal(app.posts.length, 1); }); test('opening the privacy link never changes agreement selection', async (t) => { const app = await fixture(t); await click(app.root.querySelector('.check-row .link')); assert.equal(app.checked(), false); await app.submit(); assert.equal(app.posts.length, 0); await click(app.consent()); await click(app.root.querySelector('.check-row .link')); assert.equal(app.checked(), true); await app.submit(); assert.equal(app.posts.length, 1); }); test('an agreed real phone login still rejects an invalid SMS code', async (t) => { const app = await fixture(t, '66666', '13800138000'); await click(app.consent()); await app.submit(); assert.equal(app.posts.length, 0); assert.equal(app.messages.at(-1), '请输入6位短信验证码'); });