PasswordPeek.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. 'use strict';
  2. require('../common');
  3. const fc = require('fast-check');
  4. describe('PasswordPeek', function () {
  5. const iconHref = 'img/bootstrap-icons.svg#eye';
  6. /**
  7. * input group with a password field and its reveal button, as in the
  8. * bootstrap5 template
  9. *
  10. * @param {string} id
  11. * @returns {string}
  12. */
  13. function passwordGroup(id) {
  14. return (
  15. '<div class="input-group">' +
  16. '<input id="' + id + '" type="password" class="form-control input-password" />' +
  17. '<button class="btn btn-outline-secondary toggle-password" type="button" ' +
  18. 'title="Show password" aria-label="Show password">' +
  19. '<svg width="16" height="16" fill="currentColor" aria-hidden="true">' +
  20. '<use href="' + iconHref + '" /></svg>' +
  21. '</button></div>'
  22. );
  23. }
  24. /**
  25. * asserts that the input and its reveal button are in the expected state
  26. *
  27. * @param {string} id
  28. * @param {boolean} isRevealed
  29. */
  30. function assertState(id, isRevealed) {
  31. const input = document.getElementById(id);
  32. const button = input.closest('.input-group').querySelector('.toggle-password');
  33. const label = isRevealed ? 'Hide password' : 'Show password';
  34. assert.strictEqual(input.getAttribute('type'), isRevealed ? 'text' : 'password', id + ' type');
  35. assert.strictEqual(button.getAttribute('title'), label, id + ' title');
  36. assert.strictEqual(button.getAttribute('aria-label'), label, id + ' aria-label');
  37. assert.strictEqual(
  38. button.querySelector('use').getAttribute('href'),
  39. isRevealed ? iconHref + '-slash' : iconHref,
  40. id + ' icon'
  41. );
  42. }
  43. /**
  44. * clicks the reveal button belonging to the given input
  45. *
  46. * @param {string} id
  47. */
  48. function clickButton(id) {
  49. document.getElementById(id).closest('.input-group')
  50. .querySelector('.toggle-password').click();
  51. }
  52. beforeEach(function () {
  53. globalThis.cleanup();
  54. // same order as in the template: the decryption prompt comes before
  55. // the password input of the create form
  56. document.body.innerHTML = (
  57. '<div id="passwordmodal"><form id="passwordform">' +
  58. passwordGroup('passworddecrypt') +
  59. '</form></div>' +
  60. '<nav><div id="password">' +
  61. passwordGroup('passwordinput') +
  62. '</div></nav>'
  63. );
  64. PrivateBin.PasswordPeek.init();
  65. });
  66. after(function () {
  67. globalThis.cleanup();
  68. });
  69. it('toggles the password prompt when opening a document', function () {
  70. clickButton('passworddecrypt');
  71. assertState('passworddecrypt', true);
  72. assertState('passwordinput', false);
  73. clickButton('passworddecrypt');
  74. assertState('passworddecrypt', false);
  75. assertState('passwordinput', false);
  76. });
  77. it('toggles the password input when creating a document', function () {
  78. clickButton('passwordinput');
  79. assertState('passwordinput', true);
  80. assertState('passworddecrypt', false);
  81. clickButton('passwordinput');
  82. assertState('passwordinput', false);
  83. assertState('passworddecrypt', false);
  84. });
  85. it('toggles each input independently, in any click order', function () {
  86. fc.assert(fc.property(
  87. fc.integer({min: 1, max: 5}).chain(function (count) {
  88. return fc.tuple(
  89. fc.constant(count),
  90. fc.array(fc.nat({max: count - 1}), {maxLength: 20})
  91. );
  92. }),
  93. function ([count, clicks]) {
  94. // replacing the markup is enough, init binds to the new elements
  95. const ids = [];
  96. for (let i = 0; i < count; ++i) {
  97. ids.push('password' + i);
  98. }
  99. document.body.innerHTML = ids.map(passwordGroup).join('');
  100. PrivateBin.PasswordPeek.init();
  101. const revealed = ids.map(() => false);
  102. for (const index of clicks) {
  103. clickButton(ids[index]);
  104. revealed[index] = !revealed[index];
  105. }
  106. ids.forEach(function (id, index) {
  107. assertState(id, revealed[index]);
  108. });
  109. }
  110. ), {numRuns: 50});
  111. });
  112. it('ignores a reveal button without a password input', function () {
  113. globalThis.cleanup();
  114. document.body.innerHTML = (
  115. '<div class="input-group">' +
  116. '<button class="toggle-password" type="button" title="Show password">' +
  117. '<svg><use href="' + iconHref + '" /></svg></button></div>' +
  118. '<button class="toggle-password" id="orphan" type="button" title="Show password"></button>'
  119. );
  120. PrivateBin.PasswordPeek.init();
  121. document.querySelectorAll('.toggle-password').forEach(button => button.click());
  122. document.querySelectorAll('.toggle-password').forEach(function (button) {
  123. assert.strictEqual(button.getAttribute('title'), 'Show password');
  124. });
  125. assert.strictEqual(document.querySelector('use').getAttribute('href'), iconHref);
  126. });
  127. });