login-agreement-dom.test.mjs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import test from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import { readFile } from 'node:fs/promises';
  4. import { createRequire } from 'node:module';
  5. import { JSDOM } from 'jsdom';
  6. import { parse, compileScript, compileTemplate } from '@vue/compiler-sfc';
  7. import ts from 'typescript';
  8. // Mount the actual uni-app checkbox, group and label; do not replace their click behavior.
  9. const dom = new JSDOM('<!doctype html><html><body></body></html>', {
  10. url: 'http://localhost/',
  11. });
  12. for (const key of ['window', 'document', 'Element', 'HTMLElement', 'SVGElement', 'Node']) {
  13. globalThis[key] = dom.window[key];
  14. }
  15. globalThis.__uniConfig = {};
  16. globalThis.getCurrentPages = () => [];
  17. const require = createRequire(import.meta.url);
  18. const vue = require('vue');
  19. const { Checkbox, CheckboxGroup, Label, Input } = require('@dcloudio/uni-h5');
  20. const source = await readFile(new URL('../src/pages/account/login.vue', import.meta.url), 'utf8');
  21. const { descriptor } = parse(source, { filename: 'login.vue' });
  22. const script = compileScript(descriptor, { id: 'login-agreement-dom' });
  23. const components = {
  24. checkbox: 'UniCheckbox',
  25. 'checkbox-group': 'UniCheckboxGroup',
  26. label: 'UniLabel',
  27. input: 'UniInput',
  28. };
  29. const template = compileTemplate({
  30. source: descriptor.template.content.replace(
  31. /<(\/?)(checkbox-group|checkbox|label|input)(?=[\s/>])/g,
  32. (_, closing, tag) => '<' + closing + components[tag],
  33. ),
  34. filename: 'login.vue',
  35. id: 'login-agreement-dom',
  36. compilerOptions: {
  37. bindingMetadata: script.bindings,
  38. isNativeTag: (tag) => ['view', 'text', 'button'].includes(tag),
  39. },
  40. });
  41. assert.deepEqual(template.errors, []);
  42. function evaluate(code, dependencies) {
  43. const { outputText } = ts.transpileModule(code, {
  44. compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS },
  45. });
  46. const exports = {};
  47. new Function('require', 'exports', outputText)((name) => {
  48. if (name in dependencies) return dependencies[name];
  49. throw new Error('Unexpected dependency: ' + name);
  50. }, exports);
  51. return exports;
  52. }
  53. async function click(element) {
  54. assert.ok(element, 'click target exists');
  55. element.click();
  56. await vue.nextTick();
  57. await Promise.resolve();
  58. }
  59. async function fixture(t, code = '', phone = '13012345678') {
  60. const posts = [],
  61. messages = [],
  62. accepted = [],
  63. navigations = [];
  64. globalThis.uni = { reLaunch: ({ url }) => navigations.push(url) };
  65. const configuration = {
  66. privacy_url: 'https://example.test/privacy',
  67. capabilities: { sms_login: true, test_phone_login: true },
  68. };
  69. const login = { access_token: 'fixture-token', user_id: '42', identities: [] };
  70. const component = evaluate(script.content, {
  71. vue,
  72. '@dcloudio/uni-app': { onShow() {} },
  73. '../../stores/session': {
  74. session: vue.reactive({ configuration }),
  75. loadConfiguration() {},
  76. acceptLogin: (value) => accepted.push(value),
  77. accountPage: () => '/pages/home/index',
  78. },
  79. '../../api/client': {},
  80. '../../utils/format': {
  81. toast: (message) => messages.push(message),
  82. go: (url) => navigations.push(url),
  83. },
  84. '../../utils/login-flow': {
  85. isTestPhone: (phone) => ['13012345678', '13012345679', '13012345680'].includes(phone),
  86. requestPhoneCode: async () => true,
  87. loginByPhoneCode: async (phone, code) => {
  88. posts.push({ phone, code });
  89. return login;
  90. },
  91. },
  92. }).default;
  93. component.render = evaluate(template.code, { vue }).render;
  94. const root = document.createElement('div');
  95. document.body.append(root);
  96. const app = vue.createApp(component);
  97. app.component('UniCheckbox', Checkbox);
  98. app.component('UniCheckboxGroup', CheckboxGroup);
  99. app.component('UniLabel', Label);
  100. app.component('UniInput', Input);
  101. app.mount(root);
  102. t.after(() => {
  103. app.unmount();
  104. root.remove();
  105. });
  106. await click(root.querySelector('.login-option'));
  107. const phoneInput = root.querySelector('.phone-login input');
  108. phoneInput.value = phone;
  109. phoneInput.dispatchEvent(new window.Event('input', { bubbles: true }));
  110. await vue.nextTick();
  111. const codeInput = root.querySelector('.code-field input');
  112. if (codeInput) {
  113. codeInput.value = code;
  114. codeInput.dispatchEvent(new window.Event('input', { bubbles: true }));
  115. }
  116. await vue.nextTick();
  117. const consent = () =>
  118. [...root.querySelectorAll('uni-label text')].find((element) =>
  119. element.textContent.includes('我已阅读并同意'),
  120. );
  121. const checked = () => Boolean(root.querySelector('uni-checkbox svg'));
  122. const submit = () => click(root.querySelector('.phone-login .primary'));
  123. return { root, posts, messages, accepted, navigations, consent, checked, submit };
  124. }
  125. test('clicking the agreement text updates the actual checkbox and permits login', async (t) => {
  126. const app = await fixture(t);
  127. await app.submit();
  128. assert.match(app.messages.at(-1), /请先阅读并同意/);
  129. await click(app.consent());
  130. assert.equal(app.checked(), true);
  131. await app.submit();
  132. assert.equal(app.root.querySelector('.code-field'), null);
  133. assert.deepEqual(app.posts, [{ phone: '13012345678', code: '' }]);
  134. assert.equal(app.accepted.length, 1);
  135. });
  136. test('checkbox and label clicks remain synchronized when repeatedly selecting and clearing', async (t) => {
  137. const app = await fixture(t);
  138. await click(app.root.querySelector('uni-checkbox'));
  139. assert.equal(app.checked(), true);
  140. await click(app.consent());
  141. assert.equal(app.checked(), false);
  142. await app.submit();
  143. assert.equal(app.posts.length, 0);
  144. assert.match(app.messages.at(-1), /请先阅读并同意/);
  145. await click(app.consent());
  146. await app.submit();
  147. assert.equal(app.posts.length, 1);
  148. });
  149. test('opening the privacy link never changes agreement selection', async (t) => {
  150. const app = await fixture(t);
  151. await click(app.root.querySelector('.check-row .link'));
  152. assert.equal(app.checked(), false);
  153. await app.submit();
  154. assert.equal(app.posts.length, 0);
  155. await click(app.consent());
  156. await click(app.root.querySelector('.check-row .link'));
  157. assert.equal(app.checked(), true);
  158. await app.submit();
  159. assert.equal(app.posts.length, 1);
  160. });
  161. test('an agreed real phone login still rejects an invalid SMS code', async (t) => {
  162. const app = await fixture(t, '66666', '13800138000');
  163. await click(app.consent());
  164. await app.submit();
  165. assert.equal(app.posts.length, 0);
  166. assert.equal(app.messages.at(-1), '请输入6位短信验证码');
  167. });