shopify-checkout-test

Automatically fill in test data on Shopify's checkout page.

  1. // ==UserScript==
  2. // @name shopify-checkout-test
  3. // @namespace https://github.com/pansong291/
  4. // @version 0.0.3
  5. // @description Automatically fill in test data on Shopify's checkout page.
  6. // @description:zh 在 Shopify 的结账页面上自动填充测试数据。
  7. // @author paso
  8. // @license Apache-2.0
  9. // @match *://*.myshopify.com/checkouts/*
  10. // @match *://checkout.shopifycs.com/*
  11. // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20109.5%20124.5%22%3E%3Cpath%20fill%3D%22%2395BF47%22%20d%3D%22M74.7%2C14.8c0%2C0-1.4%2C0.4-3.7%2C1.1c-0.4-1.3-1-2.8-1.8-4.4c-2.6-5-6.5-7.7-11.1-7.7c0%2C0%2C0%2C0%2C0%2C0c-0.3%2C0-0.6%2C0-1%2C0.1c-0.1-0.2-0.3-0.3-0.4-0.5c-2-2.2-4.6-3.2-7.7-3.1c-6%2C0.2-12%2C4.5-16.8%2C12.2c-3.4%2C5.4-6%2C12.2-6.7%2C17.5c-6.9%2C2.1-11.7%2C3.6-11.8%2C3.7c-3.5%2C1.1-3.6%2C1.2-4%2C4.5C9.1%2C40.7%2C0%2C111.2%2C0%2C111.2l75.6%2C13.1V14.6C75.2%2C14.7%2C74.9%2C14.7%2C74.7%2C14.8zM57.2%2C20.2c-4%2C1.2-8.4%2C2.6-12.7%2C3.9c1.2-4.7%2C3.6-9.4%2C6.4-12.5c1.1-1.1%2C2.6-2.4%2C4.3-3.2C56.9%2C12%2C57.3%2C16.9%2C57.2%2C20.2zM49.1%2C4.3c1.4%2C0%2C2.6%2C0.3%2C3.6%2C0.9c-1.6%2C0.8-3.2%2C2.1-4.7%2C3.6c-3.8%2C4.1-6.7%2C10.5-7.9%2C16.6c-3.6%2C1.1-7.2%2C2.2-10.5%2C3.2C31.7%2C19.1%2C39.8%2C4.6%2C49.1%2C4.3zM37.4%2C59.3c0.4%2C6.4%2C17.3%2C7.8%2C18.3%2C22.9c0.7%2C11.9-6.3%2C20-16.4%2C20.6c-12.2%2C0.8-18.9-6.4-18.9-6.4l2.6-11c0%2C0%2C6.7%2C5.1%2C12.1%2C4.7c3.5-0.2%2C4.8-3.1%2C4.7-5.1c-0.5-8.4-14.3-7.9-15.2-21.7C23.8%2C51.8%2C31.4%2C40.1%2C48.2%2C39c6.5-0.4%2C9.8%2C1.2%2C9.8%2C1.2l-3.8%2C14.4c0%2C0-4.3-2-9.4-1.6C37.4%2C53.5%2C37.3%2C58.2%2C37.4%2C59.3zM61.2%2C19c0-3-0.4-7.3-1.8-10.9c4.6%2C0.9%2C6.8%2C6%2C7.8%2C9.1C65.4%2C17.7%2C63.4%2C18.3%2C61.2%2C19z%22%2F%3E%3Cpath%20fill%3D%22%235E8E3E%22%20d%3D%22M78.1%2C123.9l31.4-7.8c0%2C0-13.5-91.3-13.6-91.9c-0.1-0.6-0.6-1-1.1-1c-0.5%2C0-9.3-0.2-9.3-0.2s-5.4-5.2-7.4-7.2V123.9z%22%2F%3E%3C%2Fsvg%3E
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // @run-at document-start
  15. // @require https://update.greasyfork.org/scripts/473443/1374764/popup-inject.js
  16. // ==/UserScript==
  17.  
  18. ;(function () {
  19. 'use strict'
  20. const namespace = 'paso-shopify-checkout-test'
  21. const qs = document.querySelector.bind(document)
  22. const defData = {
  23. email: '1234567890@qq.com',
  24. countryCode: 'CN',
  25. firstName: '叙乐',
  26. lastName: '欧阳',
  27. address1: '南边大街15号',
  28. address2: 'A幢5楼',
  29. city: '东城区',
  30. zone: 'BJ',
  31. postalCode: '111000',
  32. billingAddress: true,
  33. number: '1',
  34. expiry: '12 / 2099',
  35. verification_value: '123',
  36. name: 'Bogus Gateway',
  37. '居民身份证编号': '110101200012011238'
  38. }
  39. const dataFields = Object.keys(defData)
  40.  
  41. if (window.location.host === 'checkout.shopifycs.com') {
  42. if (window.parent) hookIframe(window.location.pathname.substring(1))
  43. } else {
  44. hookMain()
  45. }
  46.  
  47. function hookIframe(field) {
  48. waitSelector(`#${field}[name=${field}],[name=${field}]`).then((iptElm) => {
  49. setTimeout((elm) => {
  50. const data = getStorageData()
  51. elm.value = data[field] || ''
  52. dispatchEvent(elm, 'input')
  53. dispatchEvent(elm, 'change')
  54. }, 500, iptElm)
  55. })
  56. }
  57.  
  58. function hookMain() {
  59. const injectHtml = `
  60. <div class="table monospace" id="div-table"></div>
  61. <div class="flex aj-c">
  62. <button type="button" class="button" id="btn-save">保存</button>
  63. </div>`
  64. const injectStyle = `
  65. <style>
  66. .popup {
  67. gap: 4px;
  68. }
  69. .aj-c {
  70. align-items: center;
  71. justify-content: center;
  72. }
  73. .table {
  74. display: table;
  75. border-spacing: 8px 4px;
  76. }
  77. .table-row {
  78. display: table-row;
  79. }
  80. .table-cell {
  81. display: table-cell;
  82. }
  83. .align-right {
  84. text-align: right;
  85. }
  86. .input[readonly] {
  87. color: #888;
  88. }
  89. </style>`
  90. document.addEventListener('DOMContentLoaded', () => {
  91. window.paso.injectPopup({
  92. namespace,
  93. actionName: 'Checkout Test',
  94. collapse: '80%',
  95. content: injectHtml,
  96. style: injectStyle
  97. }).then((result) => {
  98. const { container, popup } = result.elem
  99. const { createElement } = result.func
  100. const element = {
  101. div_table: popup.querySelector('#div-table'),
  102. btn_save: popup.querySelector('#btn-save')
  103. }
  104. const data = getStorageData()
  105. const inputs = []
  106. for (const field of dataFields) {
  107. const ipt = createElement('input', { class: 'input', name: field })
  108. if (field === 'billingAddress') ipt.setAttribute('readonly', 'readonly')
  109. ipt.value = data[field]
  110. element.div_table.append(createElement('div', { class: 'table-row' }, [
  111. createElement('div', { class: 'table-cell align-right' }, [field]),
  112. createElement('div', { class: 'table-cell' }, [ipt])
  113. ]))
  114. inputs.push(ipt)
  115. }
  116. element.btn_save.addEventListener('click', () => {
  117. const d = {}
  118. inputs.forEach((ipt) => d[ipt.name] = ipt.value)
  119. GM_setValue(namespace, d)
  120. container.classList.remove('open')
  121. })
  122. })
  123. })
  124. waitSelector('#shippingAddressForm > div > div:first-child, #billingAddressForm > div > div:first-child').then((div) => {
  125. const fillData = createDebounce(() => {
  126. const data = getStorageData()
  127. for (const field of dataFields) {
  128. const iptElm = qs(`[name=${field}]`)
  129. if (!iptElm) continue
  130. if (iptElm.tagName.toLowerCase() === 'select') {
  131. iptElm.value = data[field]
  132. dispatchEvent(iptElm, 'change')
  133. } else if (iptElm.type === 'checkbox') {
  134. iptElm.checked = !!data[field]
  135. dispatchEvent(iptElm, 'change')
  136. } else {
  137. iptElm.value = data[field]
  138. dispatchEvent(iptElm, 'input')
  139. dispatchEvent(iptElm, 'change')
  140. }
  141. }
  142. let elm = qs('#basic')
  143. while (elm) {
  144. if (elm.tagName.toLowerCase() === 'section') break
  145. elm = elm.parentElement
  146. }
  147. if (elm) {
  148. const idCardObr = new MutationObserver((mutations) => {
  149. mutations.forEach((m) => {
  150. m.addedNodes?.forEach((n) => {
  151. if (n.tagName.toLowerCase() === 'section') {
  152. const ipt = n.querySelector('[name=居民身份证编号]')
  153. if (ipt) {
  154. ipt.value = data['居民身份证编号']
  155. dispatchEvent(ipt, 'input')
  156. dispatchEvent(ipt, 'change')
  157. }
  158. }
  159. })
  160. })
  161. })
  162. idCardObr.observe(elm, { childList: true })
  163. }
  164. })
  165. const divObr = new MutationObserver(() => {
  166. divObr.disconnect()
  167. fillData()
  168. })
  169. divObr.observe(div, { childList: true })
  170. fillData()
  171. })
  172. }
  173.  
  174. function dispatchEvent(elm, type) {
  175. elm.dispatchEvent(new InputEvent(type, { bubbles: true }))
  176. }
  177.  
  178. function createDebounce(func, ms = 500) {
  179. let delayId
  180. return function (...args) {
  181. clearTimeout(delayId)
  182. delayId = setTimeout(() => {
  183. func.apply(this, args)
  184. }, ms)
  185. }
  186. }
  187.  
  188. function getStorageData() {
  189. return GM_getValue(namespace, defData)
  190. }
  191.  
  192. function waitSelector(selector) {
  193. return new Promise((resolve) => {
  194. const loopId = setInterval(() => {
  195. const elm = qs(selector)
  196. if (elm) {
  197. clearInterval(loopId)
  198. resolve(elm)
  199. }
  200. }, 500)
  201. })
  202. }
  203. })()