@suffolklitlab/alkiln 5.0.0-pre-7 → 5.0.0-pre-9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -59,16 +59,20 @@ Format:
59
59
  - Change interface for testing languages. See [#713](https://github.com/SuffolkLITLab/ALKiln/issues/713).
60
60
 
61
61
  ### Fixed
62
+ - DOCKER UPDATE (for ALKilnInThePlayground): Allows ALKilnInThePlayground to upload files. See https://github.com/SuffolkLITLab/docassemble-ALKilnInThePlayground/issues/18. This may require some developers to update their system (updating their docker image) if they are below [docassemble-os 1.0.8](https://github.com/jhpyle/docassemble-os/releases/tag/v1.0.8). You can tell if you are below docassemble-os 1.0.8 if you run `docker image inspect jhpyle/docassemble | grep Created` on your host server (Lightsail, etc.) and the created date is before 2023-04-09. You can update as described in [the documentation](https://suffolklitlab.org/legal-tech-class/docs/practical-guide-docassemble/maintaining-docassemble#updates-to-the-docassemble-container). Updating to a new docker images is something that all docassemble users will have to do sometime anyway, sometimes for security.
63
+ - Conform upload file behavior to that of other Story Table rows - avoid erroring when field isn't set properly unless using all variables is required.
62
64
  - Projects created in da each have a unique name. https://github.com/SuffolkLITLab/ALKiln/issues/663
63
65
  - Shorten path names to try to accommodate limitations of windows systems while still keeping enough useful information to help devs identify the test outputs. https://github.com/SuffolkLITLab/ALKiln/issues/618
64
66
  - Updated field decoding to handle new object field encoding. See [#711](https://github.com/SuffolkLITLab/ALKiln/issues/711)
65
67
  - Allow multiple languages to be tested again. See [#713](https://github.com/SuffolkLITLab/ALKiln/issues/713).
68
+ - Fill in time fields correctly. See [#726](https://github.com/SuffolkLITLab/ALKiln/pull/726).
66
69
 
67
70
  ### Security
68
71
  - Pass docassemble API keys through HTTP headers instead of as parameters.
69
72
  - Parameters to certain HTTP requests are printed directly in docassemble's
70
73
  uWSGI log, leaking API keys to actors with log access on your docassemble
71
74
  server
75
+ - Update dependencies. See [#727](https://github.com/SuffolkLITLab/ALKiln/pull/727).
72
76
 
73
77
  ## [4.11.1] - 2023-03-21
74
78
  ### Changed
package/lib/scope.js CHANGED
@@ -2,6 +2,7 @@ const { expect } = require('chai');
2
2
  const cheerio = require('cheerio');
3
3
  const path = require('path');
4
4
  const fs = require('fs');
5
+ const fg = require('fast-glob');
5
6
  const { AxePuppeteer } = require('@axe-core/puppeteer');
6
7
  const safe_filename = require("sanitize-filename");
7
8
  // "en" is faster to import: https://github.com/faker-js/faker/issues/1114#issuecomment-1169532948
@@ -1528,7 +1529,10 @@ module.exports = {
1528
1529
  let row = matching_input_rows[ matching_input_rows.length - 1 ];
1529
1530
  let answer = row.value;
1530
1531
 
1531
- await scope.funnel_the_answer( scope, { handle, answer, field });
1532
+ // Will implement `was_used` in cases as they come along. For now, this is
1533
+ // necessary for file uploads to be as flexible as any other Story Table row.
1534
+ let was_used = await scope.funnel_the_answer( scope, { handle, answer, field });
1535
+ if ( was_used === false ) { row = null; }
1532
1536
 
1533
1537
  return row;
1534
1538
  }, // Ends scope.setVariable()
@@ -1590,16 +1594,18 @@ module.exports = {
1590
1594
  custom_datatype = await scope.get_custom_datatype( scope, { handle });
1591
1595
  }
1592
1596
 
1597
+ let row_was_used = true;
1593
1598
  // If there is a custom datatype function available, use it to set the field
1594
1599
  if ( scope.setCustomDatatype[ custom_datatype ] !== undefined ) {
1595
1600
  // Set da package custom fields. TODO: find `custom_handle` in the setter?
1596
1601
  let custom_handle = await handle.evaluateHandle(( elem )=> { return elem.closest( `.da-form-group` ) });
1597
- await scope.setCustomDatatype[ custom_datatype ]( scope, { handle: custom_handle, answer });
1602
+ row_was_used = await scope.setCustomDatatype[ custom_datatype ]( scope, { handle: custom_handle, answer });
1598
1603
  } else {
1599
1604
  // If no custom datatype, set a regular field
1600
- await scope.enter_answer[ field.tag ]( scope, { handle, answer });
1605
+ row_was_used = await scope.enter_answer[ field.tag ]( scope, { handle, answer });
1601
1606
  }
1602
1607
 
1608
+ return row_was_used;
1603
1609
  }, // Ends scope.funnel_the_answer()
1604
1610
 
1605
1611
  // Handle setting values for da custom datatypes. E.g. `da-field-container-datatype-BirthDate`
@@ -1660,10 +1666,14 @@ module.exports = {
1660
1666
  }, // ends setCustomDatatype {}
1661
1667
 
1662
1668
  enter_answer: {
1663
- button: async function ( scope, { handle, answer }) { await scope.tapElement( scope, handle ); },
1669
+ button: async function ( scope, { handle, answer }) {
1670
+ await scope.tapElement( scope, handle );
1671
+ return true;
1672
+ },
1664
1673
  textarea: async function ( scope, { handle, answer }) {
1665
1674
  await scope.setText( scope, { handle, answer });
1666
1675
  await scope.afterStep(scope); // No showifs for this one?
1676
+ return true;
1667
1677
  },
1668
1678
  select: async function ( scope, { handle, answer }) {
1669
1679
  // Note: I don't remember what's really going on in here.
@@ -1692,13 +1702,16 @@ module.exports = {
1692
1702
  }
1693
1703
 
1694
1704
  await scope.afterStep(scope, { waitForShowIf: true });
1705
+ return true;
1695
1706
  },
1696
1707
  canvas: async function ( scope, { handle, answer }) {
1697
1708
  await scope.draw_signature( scope );
1698
1709
  await scope.afterStep( scope );
1710
+ return true;
1699
1711
  },
1700
1712
  input: async function ( scope, { handle, answer }) {
1701
1713
  /* Set value of some `input` element to the given value. */
1714
+ let row_was_used = true;
1702
1715
 
1703
1716
  let type = await handle.evaluate( elem => { return elem.getAttribute('type'); });
1704
1717
  if ( type === `radio` || type === `checkbox` ) {
@@ -1717,7 +1730,7 @@ module.exports = {
1717
1730
  await scope.afterStep(scope, { waitForShowIf: true });
1718
1731
 
1719
1732
  } else if ( type === `file` ) {
1720
- await scope.uploadFiles( scope, { handle, answer });
1733
+ row_was_used = await scope.uploadFiles( scope, { handle, answer });
1721
1734
  await scope.afterStep(scope, { waitForShowIf: true });
1722
1735
 
1723
1736
  } else if (type == `date`) {
@@ -1742,10 +1755,19 @@ module.exports = {
1742
1755
  }
1743
1756
  await scope.setText( scope, { handle, answer: answer_date });
1744
1757
  await scope.afterStep(scope, { waitForShowIf: true });
1758
+ } else if (type == `time`) {
1759
+ // Authors should enter the time like 12:34 PM, but we shouldn't type ":", space, or M.
1760
+ let answer_time = answer.replace(":", "").replace(" ", "").replace("M", "");
1761
+ await handle.focus();
1762
+ await handle.type( answer );
1763
+ await handle.press("Enter");
1764
+ await scope.afterStep(scope, { waitForShowIf: true });
1745
1765
  } else {
1746
1766
  await scope.setText( scope, { handle, answer });
1747
1767
  await scope.afterStep(scope, { waitForShowIf: true });
1748
1768
  }
1769
+
1770
+ return row_was_used;
1749
1771
  }, // Ends scope.enter_answer.input()
1750
1772
  hidden: async function ( scope, { handle, answer }) {
1751
1773
  // Do nothing. The only way the code should get here is if
@@ -1762,89 +1784,98 @@ module.exports = {
1762
1784
  uploadFiles: async function ( scope, { handle, answer }) {
1763
1785
  /* Try to upload one or more files to a file-type input field.
1764
1786
  * Does not submit, so no danger of timeout with server reload.
1787
+ *
1788
+ * Returns whether the row was used or not.
1765
1789
  */
1766
1790
 
1767
- // Tests should be one place: docassemble/*/data/sources, where * is the only folder in `docassemble`.
1768
- // Makes an absolute path from the folder in which `npm run` was used plus our internal folders
1769
- let docassemble_pkg_path = `${ process.cwd() }/docassemble`;
1770
- let subpackage_name = session_vars.get_package_name().substring(12);
1771
- let pkg_paths = [];
1772
- try {
1773
- fs.accessSync(docassemble_pkg_path);
1774
- // https://stackoverflow.com/a/24594123/11416267
1775
- let dirs = fs.readdirSync(docassemble_pkg_path, {withFileTypes: true}).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name);
1776
- if (dirs.length < 1) {
1777
- let msg = `You can store files to upload in 'docassemble/${ subpackage_name }/data/source'. These tests could not find a folder like that starting at ${ process.cwd() }.`;
1778
- await scope.addToReport( scope, { type: `warning`, value: msg});
1779
- throw Error(msg); // Most people will want to know the sources folder path;
1780
- }
1781
- for (let dir of dirs) {
1782
- pkg_paths.push(`${docassemble_pkg_path}/${dir}`);
1783
- }
1784
- } catch ( error_da ) {
1785
- let msg = `You can store files to upload in 'docassemble/${ subpackage_name }/data/source'. ` +
1786
- `These tests could not find a folder like that. The current directory is ` +
1787
- ` ${ process.cwd() }; is that in the top level folder of your docassemble package? (${ session_vars.get_package_name() })?`;
1788
- await scope.addToReport( scope, { type: `warning`, value: msg});
1789
- throw error_da; // Most people will want to know the sources folder path;
1791
+ // First make sure the list of files was defined. Don't remove spaces yet -
1792
+ // allow a typo where someone forgot to include a space after a comma.
1793
+ let uploads_names = answer.split(`,`);
1794
+ if ( uploads_names.length === 0 ) {
1795
+ await scope.addToReport( scope, {
1796
+ type: `error`,
1797
+ value: `Your list of files to upload is empty.`
1798
+ });
1799
+ return false; // The row wasn't used
1790
1800
  }
1791
1801
 
1792
- let chosen_dir = null;
1793
- let err_paths = [];
1794
- for (let pkg_path of pkg_paths) {
1795
- let dir = `${ pkg_path }/data/sources`;
1796
- try {
1797
- fs.accessSync(dir);
1798
- chosen_dir = dir;
1799
- break;
1800
- } catch ( error_full ) {
1801
- err_paths.push(`'${dir}'`);
1802
- }
1803
- }
1804
- if (chosen_dir === null) {
1805
- let msg = `You can store files to upload in ${err_paths.join(", or ")}. These tests could not find any of those folders.`;
1806
- await scope.addToReport( scope, { type: `warning`, value: msg });
1807
- throw Error(msg); // Most people will want to know the sources folder path;
1802
+ let dirs = [];
1803
+ if ( session_vars.get_origin() === `playground` ) {
1804
+ let files_playground = fg.sync(
1805
+ [ `/usr/share/docassemble/files/playgroundsources/${session_vars.get_user_id()}/${session_vars.get_user_project_name()}` ],
1806
+ { onlyFiles: false, suppressErrors: true }
1807
+ );
1808
+ let files_s3 = fg.sync(
1809
+ [ `/tmp/playgroundsources/${session_vars.get_user_id()}/${session_vars.get_user_project_name()}` ],
1810
+ { onlyFiles: false, suppressErrors: true }
1811
+ );
1812
+
1813
+ dirs = files_playground.concat( files_s3 );
1814
+ } else {
1815
+ dirs = fg.sync(
1816
+ [ `**/docassemble/**/data/sources` ],
1817
+ { onlyFiles: false, suppressErrors: true }
1818
+ );
1808
1819
  }
1809
1820
 
1810
- if (pkg_paths.length > 1 && pkg_paths.length - err_paths.length > 1) {
1811
- await scope.addToReport( scope, { type: `warning`, value: `You have multiple folders in the 'docassemble' folder (${ pkg_paths }). These tests will use the first folder with a "data/sources" subfolder (${chosen_dir}).` });
1821
+ if ( session_vars.get_debug() ) {
1822
+ console.log( `All discovered dirs to upload from:`, dirs );
1812
1823
  }
1813
1824
 
1814
- // Allow a typo where someone forgot to include a space after a comma
1815
- let file_names = answer.split(`,`);
1825
+ // Make sure sources directory exists and has files in it
1826
+ if ( dirs.length === 0 ) {
1827
+ await scope.addToReport( scope, {
1828
+ type: `warning`,
1829
+ value: `The test files need to be in the "sources" directory, `
1830
+ + `but ALKiln couldn't find the directory or it was empty.`
1831
+ });
1832
+ return false; // The row wasn't used
1833
+ }
1816
1834
 
1817
- // If a file exist, add it to a list of files to be uploaded. If it doesn't, add a warning.
1835
+ // Get the full paths of all the files
1836
+ let dir = null;
1818
1837
  let paths = [];
1819
- for ( let name of file_names ) {
1838
+ let missing_files = []; // For any user warnings
1839
+ for ( let name of uploads_names ) {
1840
+
1841
+ let trimmed_name = name.trim();
1842
+ let matching_paths = [];
1843
+
1844
+ for ( let one_dir of dirs ) {
1845
+ // accumulate all matching paths
1846
+ let new_paths = fg.sync([ `${ one_dir }/${ trimmed_name }` ]);
1847
+ matching_paths.push( ...new_paths );
1848
+ } // ends for each dir
1849
+
1850
+ if ( matching_paths.length === 0 ) {
1851
+ missing_files.push( trimmed_name );
1852
+ } else {
1853
+ paths.push( ...matching_paths );
1854
+ }
1820
1855
 
1821
- // If the developer correctly used a ", ", there will be a space at the
1822
- // start of the string. Remove it. If they forgot the space, that's fine too.
1823
- name = name.replace(/^\s/, '');
1824
- let file_path = path.join( chosen_dir, name );
1856
+ } // ends for each file
1825
1857
 
1826
- try {
1827
- fs.accessSync( file_path );
1828
- paths.push( file_path );
1829
- } catch ( err ) {
1830
- // TODO: Should this throw an error?
1831
- await scope.addToReport( scope, { type: `warning`, value: `Could not find "${ name }" in "${ chosen_dir }"` });
1832
- }
1858
+ if ( missing_files.length > 0 ) {
1859
+ await scope.addToReport( scope, {
1860
+ type: `warning`,
1861
+ value: `ALKiln could not find "${ missing_files.join('", "') }" in any of these folders: "${ dirs.join('", "') }".`
1862
+ });
1863
+ }
1864
+
1865
+ // If all files missing, return that the row wasn't used
1866
+ if ( missing_files.length === uploads_names.length ) {
1867
+ return false; // The row wasn't used
1833
1868
  }
1834
1869
 
1835
- if ( paths.length === 0 ) {
1836
- await scope.addToReport( scope, { type: `error`, value: `Could not find "${answer}".` });
1837
- throw ReferenceError(`Could not find "${answer}".`)
1838
- }
1839
1870
  // Prepare to wait for upload to complete
1840
1871
  let name = await handle.evaluate( elem => { return elem.getAttribute(`name`); });
1841
1872
  let selector = `*[name="${ name }"]`
1842
1873
  let disabled_selector = `${ selector }[disabled]`;
1843
1874
  let enabled_selector = `${ selector }:not([disabled])`;
1844
1875
 
1845
- // This comes before the upload starts
1846
- // Notice `await` is absent. Don't actually wait here.
1847
- // Just start looking for it now. That way we'll catch it before it's gone
1876
+ // This comes before the upload starts.
1877
+ // Notice `await` is absent. Don't actually wait here yet.
1878
+ // Start looking for it before it exists or we might miss it.
1848
1879
  let wait_for_disable = scope.page.waitForSelector( disabled_selector );
1849
1880
 
1850
1881
  // Upload
@@ -1855,8 +1886,9 @@ module.exports = {
1855
1886
  await wait_for_disable;
1856
1887
  // When it's again enabled, the files have finished loading. Or so I have observed.
1857
1888
  await scope.page.waitForSelector( enabled_selector );
1858
-
1859
1889
 
1890
+ // Return that the row was used
1891
+ return true;
1860
1892
  }, // Ends scope.uploadFiles()
1861
1893
 
1862
1894
  setCheckbox: async function ( scope, { label, answer }) {
@@ -2,14 +2,14 @@
2
2
 
3
3
  let _path = require('path');
4
4
  let fs = require('fs');
5
- let glob = require('glob');
5
+ const fg = require('fast-glob');
6
6
 
7
7
  let session_vars = require('./session_vars');
8
8
 
9
9
  // Get the glob filepaths to find all the test files
10
10
  let dir_root = process.env.INIT_CWD; // Path to root of project
11
11
  let test_glob = process.argv[2]; // String of glob (regex for filepaths)
12
- let filepaths = glob.sync( _path.resolve( dir_root, test_glob ) );
12
+ let filepaths = fg.sync( _path.resolve( dir_root, test_glob ) );
13
13
  let flag = '_lang_';
14
14
 
15
15
  let langs = session_vars.get_languages();