|
@@ -0,0 +1,170 @@
|
|
|
|
|
+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('<!doctype html><html><body></body></html>', {
|
|
|
|
|
+ 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 = '66666') {
|
|
|
|
|
+ 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 inputs = root.querySelectorAll('.phone-login input');
|
|
|
|
|
+ for (const [index, value] of ['13012345678', code].entries()) {
|
|
|
|
|
+ inputs[index].value = value;
|
|
|
|
|
+ inputs[index].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.deepEqual(app.posts, [{ phone: '13012345678', code: '66666' }]);
|
|
|
|
|
+ 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 test login with a six-digit code shows a code error instead of an agreement error', async (t) => {
|
|
|
|
|
+ const app = await fixture(t, '666666');
|
|
|
|
|
+ await click(app.consent());
|
|
|
|
|
+ await app.submit();
|
|
|
|
|
+ assert.equal(app.posts.length, 0);
|
|
|
|
|
+ assert.equal(app.messages.at(-1), '测试账号请输入5位测试验证码 66666');
|
|
|
|
|
+});
|