@suffolklitlab/alkiln 5.0.0-pre-6 → 5.0.0-pre-8

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
@@ -56,11 +56,15 @@ Format:
56
56
  - don't print the ["publish this cucumber report" message](https://github.com/cucumber/cucumber-js/blob/main/docs/configuration.md#options)
57
57
  - Adjusted validation of some environment variables to account for Playground vs. GitHub or local test runs. [Issue #661](https://github.com/SuffolkLITLab/ALKiln/issues/661)
58
58
  - Docassemble Project name prefix now includes ALKiln in it for clarity
59
+ - Change interface for testing languages. See [#713](https://github.com/SuffolkLITLab/ALKiln/issues/713).
59
60
 
60
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.
61
64
  - Projects created in da each have a unique name. https://github.com/SuffolkLITLab/ALKiln/issues/663
62
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
63
66
  - Updated field decoding to handle new object field encoding. See [#711](https://github.com/SuffolkLITLab/ALKiln/issues/711)
67
+ - Allow multiple languages to be tested again. See [#713](https://github.com/SuffolkLITLab/ALKiln/issues/713).
64
68
 
65
69
  ### Security
66
70
  - Pass docassemble API keys through HTTP headers instead of as parameters.
@@ -33,20 +33,23 @@ const cucumber_api = require('@cucumber/cucumber/api');
33
33
  }
34
34
 
35
35
  if (tags.length > 0) {
36
+ let joined_tags = '';
36
37
  // Cucumber expects tags in boolean expressions, like (@a0 or @a1) and @random.
37
38
  // If we get a list of only tags, then "or" them all together
38
39
  if (tags.every((val) => val.startsWith("@"))) {
39
- provided_config.tags = tags.join(' or ');
40
+ joined_tags = tags.join(' or ');
40
41
  }
41
42
  // If we get a list of seemingly no tags, assume they meant to use tags
42
43
  else if (tags.every((val) => !val.startsWith("@"))) {
43
- provided_config.tags = tags.map((val) => "@" + val).join(' or ');
44
+ joined_tags = tags.map((val) => "@" + val).join(' or ');
44
45
  }
45
46
  // If it's a mix of tags and no tags, it's probably a boolean expression!
46
47
  else {
47
- provided_config.tags = tags.join(' ');
48
+ joined_tags = tags.join(' ');
48
49
  }
49
- }
50
+
51
+ provided_config.tags = joined_tags;
52
+ } // end if there are tags
50
53
 
51
54
  const { runConfiguration } = await cucumber_api.loadConfiguration({
52
55
  provided: provided_config
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`) {
@@ -1746,6 +1759,8 @@ module.exports = {
1746
1759
  await scope.setText( scope, { handle, answer });
1747
1760
  await scope.afterStep(scope, { waitForShowIf: true });
1748
1761
  }
1762
+
1763
+ return row_was_used;
1749
1764
  }, // Ends scope.enter_answer.input()
1750
1765
  hidden: async function ( scope, { handle, answer }) {
1751
1766
  // Do nothing. The only way the code should get here is if
@@ -1762,89 +1777,98 @@ module.exports = {
1762
1777
  uploadFiles: async function ( scope, { handle, answer }) {
1763
1778
  /* Try to upload one or more files to a file-type input field.
1764
1779
  * Does not submit, so no danger of timeout with server reload.
1780
+ *
1781
+ * Returns whether the row was used or not.
1765
1782
  */
1766
1783
 
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;
1784
+ // First make sure the list of files was defined. Don't remove spaces yet -
1785
+ // allow a typo where someone forgot to include a space after a comma.
1786
+ let uploads_names = answer.split(`,`);
1787
+ if ( uploads_names.length === 0 ) {
1788
+ await scope.addToReport( scope, {
1789
+ type: `error`,
1790
+ value: `Your list of files to upload is empty.`
1791
+ });
1792
+ return false; // The row wasn't used
1790
1793
  }
1791
1794
 
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;
1795
+ let dirs = [];
1796
+ if ( session_vars.get_origin() === `playground` ) {
1797
+ let files_playground = fg.sync(
1798
+ [ `/usr/share/docassemble/files/playgroundsources/${session_vars.get_user_id()}/${session_vars.get_user_project_name()}` ],
1799
+ { onlyFiles: false, suppressErrors: true }
1800
+ );
1801
+ let files_s3 = fg.sync(
1802
+ [ `/tmp/playgroundsources/${session_vars.get_user_id()}/${session_vars.get_user_project_name()}` ],
1803
+ { onlyFiles: false, suppressErrors: true }
1804
+ );
1805
+
1806
+ dirs = files_playground.concat( files_s3 );
1807
+ } else {
1808
+ dirs = fg.sync(
1809
+ [ `**/docassemble/**/data/sources` ],
1810
+ { onlyFiles: false, suppressErrors: true }
1811
+ );
1808
1812
  }
1809
1813
 
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}).` });
1814
+ if ( session_vars.get_debug() ) {
1815
+ console.log( `All discovered dirs to upload from:`, dirs );
1812
1816
  }
1813
1817
 
1814
- // Allow a typo where someone forgot to include a space after a comma
1815
- let file_names = answer.split(`,`);
1818
+ // Make sure sources directory exists and has files in it
1819
+ if ( dirs.length === 0 ) {
1820
+ await scope.addToReport( scope, {
1821
+ type: `warning`,
1822
+ value: `The test files need to be in the "sources" directory, `
1823
+ + `but ALKiln couldn't find the directory or it was empty.`
1824
+ });
1825
+ return false; // The row wasn't used
1826
+ }
1816
1827
 
1817
- // If a file exist, add it to a list of files to be uploaded. If it doesn't, add a warning.
1828
+ // Get the full paths of all the files
1829
+ let dir = null;
1818
1830
  let paths = [];
1819
- for ( let name of file_names ) {
1831
+ let missing_files = []; // For any user warnings
1832
+ for ( let name of uploads_names ) {
1833
+
1834
+ let trimmed_name = name.trim();
1835
+ let matching_paths = [];
1836
+
1837
+ for ( let one_dir of dirs ) {
1838
+ // accumulate all matching paths
1839
+ let new_paths = fg.sync([ `${ one_dir }/${ trimmed_name }` ]);
1840
+ matching_paths.push( ...new_paths );
1841
+ } // ends for each dir
1842
+
1843
+ if ( matching_paths.length === 0 ) {
1844
+ missing_files.push( trimmed_name );
1845
+ } else {
1846
+ paths.push( ...matching_paths );
1847
+ }
1820
1848
 
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 );
1849
+ } // ends for each file
1825
1850
 
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
- }
1851
+ if ( missing_files.length > 0 ) {
1852
+ await scope.addToReport( scope, {
1853
+ type: `warning`,
1854
+ value: `ALKiln could not find "${ missing_files.join('", "') }" in any of these folders: "${ dirs.join('", "') }".`
1855
+ });
1856
+ }
1857
+
1858
+ // If all files missing, return that the row wasn't used
1859
+ if ( missing_files.length === uploads_names.length ) {
1860
+ return false; // The row wasn't used
1833
1861
  }
1834
1862
 
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
1863
  // Prepare to wait for upload to complete
1840
1864
  let name = await handle.evaluate( elem => { return elem.getAttribute(`name`); });
1841
1865
  let selector = `*[name="${ name }"]`
1842
1866
  let disabled_selector = `${ selector }[disabled]`;
1843
1867
  let enabled_selector = `${ selector }:not([disabled])`;
1844
1868
 
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
1869
+ // This comes before the upload starts.
1870
+ // Notice `await` is absent. Don't actually wait here yet.
1871
+ // Start looking for it before it exists or we might miss it.
1848
1872
  let wait_for_disable = scope.page.waitForSelector( disabled_selector );
1849
1873
 
1850
1874
  // Upload
@@ -1855,8 +1879,9 @@ module.exports = {
1855
1879
  await wait_for_disable;
1856
1880
  // When it's again enabled, the files have finished loading. Or so I have observed.
1857
1881
  await scope.page.waitForSelector( enabled_selector );
1858
-
1859
1882
 
1883
+ // Return that the row was used
1884
+ return true;
1860
1885
  }, // Ends scope.uploadFiles()
1861
1886
 
1862
1887
  setCheckbox: async function ( scope, { label, answer }) {
package/lib/steps.js CHANGED
@@ -160,7 +160,7 @@ Before(async (scenario) => {
160
160
  //#####################################
161
161
  //#####################################
162
162
 
163
- Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}, async (file_name, language) => { // √
163
+ Given(/I start the interview at "([^"]+)"/, {timeout: -1}, async (file_name) => { // √
164
164
 
165
165
  // Load the right interview page
166
166
  await scope.load( scope, file_name );
@@ -180,7 +180,7 @@ Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}
180
180
  // I've seen stuff take an extra moment or two. Shame to have it everywhere
181
181
  try {
182
182
  // Interview files should get downloaded to downloads folder
183
- await scope.page._client.send('Page.setDownloadBehavior', {
183
+ await scope.page._client().send('Page.setDownloadBehavior', {
184
184
  behavior: 'allow',
185
185
  downloadPath: path.resolve( scope.paths.scenario ), // Save in root of this scenario
186
186
  });
@@ -203,7 +203,7 @@ Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}
203
203
  // at any one time since each step completes synchronously
204
204
 
205
205
  // If response has a file in it that matches the file we are downloading
206
- if ( response._headers['content-disposition'] && (response._headers['content-disposition']).includes(`attachment;filename=${scope.toDownload}`) ) {
206
+ if ( response.headers()['content-disposition'] && (response.headers()['content-disposition']).includes(`attachment;filename=${scope.toDownload}`) ) {
207
207
  // We're choosing to avoid removing the event listener in case removal
208
208
  // would prevent downloading other files. The listener should get
209
209
  // destroyed when the page closes anyway.
@@ -211,7 +211,7 @@ Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}
211
211
  // Watch event on download folder or file
212
212
  await fs.watchFile( scope.paths.scenario, function (curr, prev) {
213
213
  // If current size eq to size from response then close
214
- if (parseInt(curr.size) === parseInt(response._headers['content-length'])) {
214
+ if (parseInt(curr.size) === parseInt(response.headers()['content-length'])) {
215
215
  // Lets the related function in `scope` know that the download
216
216
  // is done so that the next step can start.
217
217
  scope.downloadComplete = true;
@@ -224,25 +224,6 @@ Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}
224
224
  } // ends if this is the file the developer expected
225
225
  }); // ends page.on 'response'
226
226
 
227
-
228
- // TODO: Move language into a different step
229
- scope.language = language;
230
- let custom_lang_timeout = new Promise(async ( resolve, reject ) => {
231
- let page_timeout = setTimeout(function() { reject( `It took to long to try to set the language "${ language }".` ); }, scope.timeout);
232
- // self-invoke an anonymous async function so we don't have to
233
- // abstract the details of resolving and rejecting.
234
- (async function() {
235
- try {
236
- await scope.setLanguage( scope, { language });
237
- resolve();
238
- } catch ( err ) { reject( err ); }
239
- // prevent temporary hang at end of tests
240
- finally { clearTimeout( page_timeout ); }
241
- })();
242
- }); // ends custom_lang_timeout
243
- await custom_lang_timeout;
244
-
245
-
246
227
  let custom_afterStep_timeout = new Promise(async ( resolve, reject ) => {
247
228
  let page_timeout = setTimeout(function() { reject(`\`afterStep()\` took too long.`); }, scope.timeout);
248
229
  // self-invoke an anonymous async function so we don't have to
@@ -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();