@suffolklitlab/alkiln 5.9.0 → 5.10.0-fix-typing1

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.
@@ -91,5 +91,5 @@ jobs:
91
91
  #### to debug the docker install step
92
92
  #### Use a "tag expression" to specify which tagged tests to run (https://cucumber.io/docs/cucumber/api/#tag-expressions)
93
93
  # ALKILN_TAG_EXPRESSION: @a_tag_expression
94
- # ALKILN_VERSION: 2column
94
+ ALKILN_VERSION: url
95
95
 
@@ -44,6 +44,11 @@ jobs:
44
44
  # relative paths to action files
45
45
  - uses: actions/checkout@v3
46
46
 
47
+ - name: Pause between triggers
48
+ if: (github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork)
49
+ shell: bash
50
+ run: sleep 30
51
+
47
52
  #### Developer note: you'll need to replace `.` with the path to
48
53
  #### the repo and branch and leave off the trailing `/`
49
54
  #### For example suffolkLITLab/ALKiln@v5
@@ -55,4 +60,4 @@ jobs:
55
60
  #### to debug the docker install step
56
61
  # ALKILN_TAG_EXPRESSION: @a_tag_expression
57
62
  #### Developer note: you probalby don't need this
58
- # ALKILN_VERSION: 2column
63
+ ALKILN_VERSION: url
package/CHANGELOG.md CHANGED
@@ -43,7 +43,30 @@ Format:
43
43
  -
44
44
  -->
45
45
 
46
- <!-- ## [Unreleased] -->
46
+ ## [Unreleased]
47
+
48
+ ### Fixed
49
+
50
+ - Fixes the issue that typing is too fast for some fields, like Ajax comboboxes. See [#857](https://github.com/SuffolkLITLab/ALKiln/issues/857).
51
+
52
+ ## [5.10.0] - 2024-02-22
53
+
54
+ ### Added
55
+
56
+ - New Step: 'I go to ""' to go to any arbitrary url, not just arbitrary interview urls.
57
+ - New navigation report messages.
58
+ - Allow non-local environments to get debug logs. For example, the GitHub console during a workflow run.
59
+
60
+ ### Changed
61
+
62
+ - Avoid waiting for "success" on sign in. Not everyone has it.
63
+ - Detect signature page variable names with the trigger variable name as a fallback to the `_save_as` input. If a server's config has its `restrict input variables` value set to `True`, the #trigger value is the only way for ALKiln to know which variable the signature is setting. Add an error to the report if ALKiln is unable to find either of those.
64
+
65
+ ### Internal
66
+
67
+ - Handle non-existence of `elem` more robustly when checking for `elem.disabled`
68
+ - Add a pause between pull request tests and push tests when the PR is coming from our own repo
69
+ - Make getting page field values more robust by doing more to ensure that undefined option `value`s don't cause errors. For example, ajax combobox `select` options don't always have `value`s
47
70
 
48
71
  ## [5.9.0] - 2024-02-11
49
72
 
@@ -1,3 +1,4 @@
1
+ // TODO: Add the error message to the report. Would have add a `scope` parameter
1
2
  module.exports = async function wrapPromiseWithTimeout (
2
3
  promise,
3
4
  timeoutInMilliseconds,
package/lib/scope.js CHANGED
@@ -626,7 +626,7 @@ module.exports = {
626
626
 
627
627
  reports.addToReport(scope,{
628
628
  type: `row`,
629
- value: `Trying to load the interview at "${interview_url}"`
629
+ value: `Trying to load the interview at "${ interview_url }"`
630
630
  });
631
631
 
632
632
  if ( !scope.page ) { scope.page = await scope.browser.newPage(); }
@@ -641,8 +641,9 @@ module.exports = {
641
641
  // If the page didn't load because it took to long to load
642
642
  if ( error.name === `TimeoutError` ) {
643
643
 
644
- let non_reload_report_msg = `Timeout error occurred when ALKiln tried to go to the interview url`;
644
+ let non_reload_report_msg = `Timeout error occurred when ALKiln tried to go to the interview at ${ interview_url }`;
645
645
  await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
646
+ // TODO: Should this throw to avoid multiple error messages?
646
647
 
647
648
  // If the page didn't load because of any other reason
648
649
  } else {
@@ -650,7 +651,7 @@ module.exports = {
650
651
  // Throw any non-timeout error
651
652
  reports.addToReport( scope, {
652
653
  type: `error`,
653
- value: `Error occurred when ALKiln tried to go to the interview url`
654
+ value: `Error occurred when ALKiln tried to go to the interview at ${ interview_url }`
654
655
  });
655
656
  throw( error );
656
657
  } // ends if error is timeout error
@@ -658,10 +659,12 @@ module.exports = {
658
659
  } // ends try/catch
659
660
 
660
661
  let load_result = await scope.getLoadData( scope );
661
- // Errors, or reaching timeout, causes an exception and is shown to the developer at the end
662
+ // da page invalidation errors or timeout will show an error to the developer at the end
662
663
  if ( load_result.error ) {
663
- reports.addToReport(scope, { type: `error`, value: `On final attempt to load interview, got "${ load_result.error }"` });
664
+ reports.addToReport(scope, { type: `error`, value: `On final attempt to load interview at "${ interview_url }", got "${ load_result.error }"` });
664
665
  expect( load_result.error ).to.equal( '' );
666
+ } else {
667
+ reports.addToReport(scope, { type: `info`, value: `Successfully found the interview at "${ interview_url }"` });
665
668
  }
666
669
 
667
670
  return scope.page;
@@ -693,6 +696,11 @@ module.exports = {
693
696
 
694
697
  // or if they cause some other kind of error
695
698
  } else {
699
+ reports.addToReport(scope, {
700
+ type: `error`,
701
+ value: `ALKiln got an error when it tried to load the interview page`,
702
+ data: error
703
+ });
696
704
  throw error;
697
705
  } // ends if error.name.includes(`TimeoutError`)
698
706
  }); // ends Promise.race()
@@ -869,7 +877,29 @@ module.exports = {
869
877
  if ( $(`#dasigpage`).length > 0 && $(`#dasigpage`)[0].name ) {
870
878
  field_data.selector = `#daquestion canvas`;
871
879
  field_data.tag = `canvas`;
872
- not_decoded_name = $( `input[name="_save_as"]` ).attr( `value` );
880
+
881
+ // If da server's config value of `restrict input variables`
882
+ // set to True, `_save_as` should work to get the var name
883
+ if ( $( `input[name="_save_as"]` ).length > 0 ) {
884
+ not_decoded_name = $( `input[name="_save_as"]` ).attr( `value` );
885
+
886
+ // If not, the author will have to use the trigger var HTML element
887
+ // or we can't get the var name being set for the signature
888
+ } else if ( trigger !== `` ) {
889
+ not_decoded_name = trigger;
890
+
891
+ // If we have neither of those, we need to tell the user we can't
892
+ // set this variable this way. We can't test this without a
893
+ // server that has that special config value set to `True`
894
+ } else {
895
+ reports.addToReport(scope, {
896
+ type: `error`,
897
+ value: `You cannot sign a signature page this way because the `
898
+ + `interview's HTML is missing data. Use the "I sign" Step `
899
+ + `instead. You can also read documentation about `
900
+ + `how to create the "trigger" HTML.`
901
+ });
902
+ }
873
903
  }
874
904
 
875
905
  // The selector we'll use to set the DOM node's value later
@@ -983,6 +1013,7 @@ module.exports = {
983
1013
  /* Return variable names and values combined in ways that can
984
1014
  * try to match table rows. Tables can have two properties:
985
1015
  * variable name and value. */
1016
+ if ( session_vars.get_debug() ) { console.log(`scope.getPossibleTableRowMatchers() var_names:`, var_names, `values:`, values); }
986
1017
  let possible_table_rows = [];
987
1018
  for ( let var_name of var_names ) {
988
1019
  // Inputs like textareas don't have values
@@ -993,7 +1024,8 @@ module.exports = {
993
1024
  // Modified from https://stackoverflow.com/a/8571649/11416267
994
1025
  // DA doesn't use `==` to pad base 64
995
1026
  // TODO: How are 'None', 'True', and 'False' being handled?
996
- if (value !== 'None' && value !== 'True' && value !== 'False'
1027
+ if (value !== undefined && value !== null
1028
+ && value !== 'None' && value !== 'True' && value !== 'False'
997
1029
  && value.match(da_base64_regex)) {
998
1030
  let value_decoded = fromBase64(value);
999
1031
  let values_decoded = scope.decodeFully( scope, { expr_str: value_decoded });
@@ -1246,7 +1278,10 @@ module.exports = {
1246
1278
  let options = [];
1247
1279
  let option_nodes = $select.find( `option` );
1248
1280
  for ( let opt of option_nodes ) {
1249
- options.push( opt.attribs.value );
1281
+ // Some options don't have a value, like for ajax comboboxes
1282
+ if ( opt.attribs.value !== undefined ) {
1283
+ options.push( opt.attribs.value );
1284
+ }
1250
1285
  // Dropdowns created w/ objects are base64 encoded; but it's handled in getPossibleTableRowMatchers
1251
1286
  }
1252
1287
  return options;
@@ -1490,6 +1525,7 @@ module.exports = {
1490
1525
  }
1491
1526
 
1492
1527
  let handle = await scope.get_handle_from_field( scope, { field: field });
1528
+ // TODO: expect elem to exist
1493
1529
 
1494
1530
  // After each previous input, figure out if this input is (now/still) disabled
1495
1531
  // TODO: Consider testing for disabled this before we get the matching row
@@ -1498,8 +1534,12 @@ module.exports = {
1498
1534
  // sometimes it blocks nodes from clicks. Scroll item to the bottom instead.
1499
1535
  // Note: I'm not sure how we'd get in here if `elem` didn't exist, but
1500
1536
  // I once got an error here that I couldn't read `.disabled` of `null`.
1501
- if ( elem && !elem.disabled ) { elem.scrollIntoView(false); }
1502
- return elem.disabled;
1537
+ if ( elem && !elem.disabled ) {
1538
+ elem.scrollIntoView(false);
1539
+ return elem.disabled;
1540
+ } else {
1541
+ return false;
1542
+ }
1503
1543
  });
1504
1544
 
1505
1545
  // If field is disabled, don't set it yet
@@ -1531,7 +1571,9 @@ module.exports = {
1531
1571
 
1532
1572
  get_handle_from_field: async function ( scope, { field }) {
1533
1573
  /** Get the element's puppeteer handle using the field's selector. */
1534
-
1574
+ if ( session_vars.get_debug() ) {
1575
+ console.log(`scope.get_handle_from_field() field.selector:`, field.selector );
1576
+ }
1535
1577
  let handle = await scope.page.evaluateHandle(( selector ) => {
1536
1578
  let elem = document.querySelector( selector );
1537
1579
  // If the element has a sibling that's a label, we need to
@@ -1758,7 +1800,9 @@ module.exports = {
1758
1800
  // Authors should enter the time like 12:34 PM, but we shouldn't type ":", space, or M.
1759
1801
  let answer_time = answer.replace(":", "").replace(" ", "").replace("M", "");
1760
1802
  await handle.focus();
1761
- await handle.type( answer );
1803
+ let delay = 0;
1804
+ if ( session_vars.get_debug() ) { delay = 25; }
1805
+ await handle.type( answer, { delay });
1762
1806
  await handle.press(`Enter`);
1763
1807
  await scope.afterStep(scope, { waitForShowIf: true });
1764
1808
  } else {
@@ -1932,9 +1976,16 @@ module.exports = {
1932
1976
 
1933
1977
  setText: async function ( scope, { handle, answer }) {
1934
1978
  // Set text in some kind of field (input text, input date, textarea, etc.)
1935
- await handle.evaluate( el => { el.value = '' });
1979
+ // First set the value to part of the string
1980
+ const to_set = answer.slice(0, -3);
1981
+ const to_type = answer.slice(-3);
1982
+ await handle.evaluate(( elem, text ) => {
1983
+ elem.value = text;
1984
+ }, to_set );
1985
+ // Then type the last three characters of the string with a
1986
+ // good pause to let the field register the interaction
1936
1987
  await handle.focus();
1937
- await handle.type( answer );
1988
+ await handle.type( to_type, { delay: 100 });
1938
1989
  },
1939
1990
 
1940
1991
  draw_signature: async function ( scope, name ) {
@@ -2060,6 +2111,8 @@ module.exports = {
2060
2111
  } // ends for every non-submit-button field on the page
2061
2112
  } // ends while fields are still being set
2062
2113
 
2114
+ if ( session_vars.get_debug() ) { await scope.page.waitForTimeout(2000); }
2115
+
2063
2116
  // Press a continue button with a story table variable name if it exists
2064
2117
  let did_navigate = false;
2065
2118
  let error = {};
@@ -2838,11 +2891,6 @@ module.exports = {
2838
2891
  let elem = await scope.page.$( `button[name="submit"]` );
2839
2892
  await scope.tapElementAndNavigate( scope, { elem });
2840
2893
 
2841
- // Wait to see the success message.
2842
- // This is a bit of an assumption and needs success and failure testing
2843
- // TODO: use a Promise.race to fail faster.
2844
- await scope.page.waitForSelector( `.datopcenter .alert-success` );
2845
-
2846
2894
  reports.addToReport( scope, {
2847
2895
  type: `row`,
2848
2896
  value: `Signed into ${ session_vars.get_da_server_url() }/user/sign-in successfully.`
package/lib/steps.js CHANGED
@@ -114,7 +114,7 @@ Before(async (scenario) => {
114
114
 
115
115
  // Create browser, which can't be created in BeforeAll() where .driver doesn't exist for some reason
116
116
  if (!scope.browser) {
117
- if (session_vars.get_origin() === 'playground') {
117
+ if ( session_vars.get_origin() !== `local` ) {
118
118
  // Will only run in the Playground outside of a sandbox. TODO: There's a
119
119
  // better way to do this, though it's more complicated. See comments in
120
120
  // https://github.com/SuffolkLITLab/ALKiln/issues/661
@@ -178,11 +178,12 @@ Given(/I start the interview at "([^"]+)"/, {timeout: -1}, async (file_name) =>
178
178
  await scope.load( scope, file_name );
179
179
 
180
180
  // The page timeout is set in here too in case someone set a custom timeout
181
- // before a page was loaded.
181
+ // just before a page was loaded for the Scenario. Move this to just after page creation?
182
182
  // The function itself won't really timeout here, so we don't need to wrap it in
183
183
  // a custom timeout
184
184
  await scope.page.setDefaultTimeout( scope.timeout ); // overrides outer default timeout
185
185
 
186
+ // TODO: Move this to the `download` Step and only do it once per scenario
186
187
  // Allow developer to save download files
187
188
  let custom_download_behavior_timeout = new Promise(async ( resolve, reject ) => {
188
189
  let page_timeout = setTimeout(function() {reject( `Took too long to set download behavior.` ); }, scope.timeout);
@@ -236,6 +237,8 @@ Given(/I start the interview at "([^"]+)"/, {timeout: -1}, async (file_name) =>
236
237
  } // ends if this is the file the developer expected
237
238
  }); // ends page.on 'response'
238
239
 
240
+ // TODO: Wrap everything in this step in this timeout? Or is this Step's timeout different?
241
+ // Wait for navigation
239
242
  let custom_afterStep_timeout = new Promise(async ( resolve, reject ) => {
240
243
  let page_timeout = setTimeout(function() { reject(`\`afterStep()\` took too long.`); }, scope.timeout);
241
244
  // self-invoke an anonymous async function so we don't have to
@@ -251,6 +254,22 @@ Given(/I start the interview at "([^"]+)"/, {timeout: -1}, async (file_name) =>
251
254
  await custom_afterStep_timeout;
252
255
  });
253
256
 
257
+ Given(/I go to "([^"]+)"/i, {timeout: -1}, async ( url ) => {
258
+ /** Navigate to an arbitrary url. What you do after that depends on you. There's
259
+ * a limited amount ALKiln can do on a non-docassemble page or a very customized
260
+ * docassemble page.
261
+ *
262
+ * @params {string} A valid url */
263
+ let result = await wrapPromiseWithTimeout(
264
+ scope.page.goto( url, { waitUntil: 'domcontentloaded', timeout: scope.timeout }),
265
+ scope.timeout,
266
+ `It took to long to get to "${ url }"`
267
+ );
268
+
269
+ reports.addToReport(scope, { type: `info`, value: `Successfully went to "${ url }"` });
270
+ return result;
271
+ });
272
+
254
273
  Given(/I (?:sign|log) ?(?:in)?(?:on)?(?:to the server)? with(?: the email)? "([^"]+)" and(?: the password)? "([^"]+)"(?: SECRETs)?/i, {timeout: -1}, async ( email, password ) => {
255
274
  /** Uses the names of environment variables (most often GitHub SECRETs) to
256
275
  * log into an account on the user's server. Must be SECRETs to stay secure.
@@ -294,7 +294,7 @@ session_vars.set_sources_paths = function ( paths ) {
294
294
  }
295
295
 
296
296
  console.info(
297
- `ALKiln info: Did find "sources" folder(s) at "${ existing_paths.join('", "') }".`
297
+ `ALKiln SUCCESS: Did find "sources" file(s) at "${ existing_paths.join('", "') }".`
298
298
  )
299
299
 
300
300
  // Otherwise, store valid paths
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suffolklitlab/alkiln",
3
- "version": "5.9.0",
3
+ "version": "5.10.0fix-typing1",
4
4
  "description": "Integrated automated end-to-end testing with docassemble, puppeteer, and cucumber.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -1108,6 +1108,76 @@ fields.object_multiselect = [
1108
1108
  },
1109
1109
  ];
1110
1110
 
1111
+ // ============================
1112
+ // Ajax combobox
1113
+ // ============================
1114
+ fields.ajax = [
1115
+ {
1116
+ "selector": "#daquestion input[name=\"ZmF2b3JpdGVfd29yZA\"]",
1117
+ "tag": "input",
1118
+ "guesses": [
1119
+ {
1120
+ "var": "ZmF2b3JpdGVfd29yZA",
1121
+ "value": ""
1122
+ },
1123
+ {
1124
+ "var": "favorite_word",
1125
+ "value": ""
1126
+ }
1127
+ ],
1128
+ "type": "hidden",
1129
+ "trigger": ""
1130
+ },
1131
+ {
1132
+ "selector": "#daquestion input[id=\"ZmF2b3JpdGVfd29yZA\"][class=\"form-control da-ajax-combobox\"]",
1133
+ "tag": "input",
1134
+ "guesses": [
1135
+ {
1136
+ "var": "ZmF2b3JpdGVfd29yZA",
1137
+ "value": ""
1138
+ },
1139
+ {
1140
+ "var": "favorite_word",
1141
+ "value": ""
1142
+ }
1143
+ ],
1144
+ "type": "text",
1145
+ "trigger": ""
1146
+ },
1147
+ {
1148
+ "selector": "#daquestion select[id=\"ZmF2b3JpdGVfd29yZAcombobox\"][class=\"form-control da-ajax-combobox\"]",
1149
+ "tag": "select",
1150
+ "guesses": [
1151
+ {
1152
+ "var": "ZmF2b3JpdGVfd29yZAcombobox",
1153
+ "value": ""
1154
+ },
1155
+ {
1156
+ "var": "favorite_word\u0007(��\u001b�",
1157
+ "value": ""
1158
+ }
1159
+ ],
1160
+ "type": "",
1161
+ "trigger": ""
1162
+ },
1163
+ {
1164
+ "selector": "#daquestion button[id=\"da-continue-button\"][class=\"btn btn-da btn-primary\"]",
1165
+ "tag": "button",
1166
+ "guesses": [
1167
+ {
1168
+ "var": "da-continue-button",
1169
+ "value": ""
1170
+ },
1171
+ {
1172
+ "var": "u���{b��n�m�",
1173
+ "value": ""
1174
+ }
1175
+ ],
1176
+ "type": "submit",
1177
+ "trigger": ""
1178
+ }
1179
+ ];
1180
+
1111
1181
 
1112
1182
  // ============================
1113
1183
  // mixed quotes
@@ -33,8 +33,8 @@ chai.config.truncateThreshold = 0
33
33
  it(`creates the right data for standard fields`, async function() {
34
34
  // 18 fields (03/15/21)
35
35
  let result = await getAllFields( scope, { html: html.standard });
36
- // console.log(`=================\n${ JSON.stringify(fields.standard, null, 2) }`);
37
- // console.log(`-----------------\n${ JSON.stringify(result, null, 2) }`);
36
+ // console.log(`=================\n${ JSON.stringify(fields.standard, null, 2) }`);
37
+ // console.log(`-----------------\n${ JSON.stringify(result, null, 2) }`);
38
38
  expect( result ).to.deep.equalInAnyOrder( fields.standard );
39
39
  });
40
40
 
@@ -148,6 +148,12 @@ it(`creates the right data for a dropdown created with an object`, async functio
148
148
  expect( result ).to.deep.equalInAnyOrder(fields.object_dropdown );
149
149
  });
150
150
 
151
+ // Ajax
152
+ it('creates the right data for an `ajax` combobox', async function() {
153
+ let result = await getAllFields( scope, { html: html.ajax });
154
+ expect( result ).to.deep.equalInAnyOrder(fields.ajax );
155
+ });
156
+
151
157
  // ============================
152
158
  // DOM for mixed quotes test in getMatchingRows.test.js
153
159
  // ============================
@@ -789,6 +789,62 @@ html.object_dropdown = `
789
789
  </form>
790
790
  </div>`;
791
791
 
792
+ html.ajax = `
793
+ <div id="daquestion" aria-labelledby="dapagetitle" role="main" class="tab-pane fade show active offset-lg-3 col-lg-6 offset-md-2 col-md-8">
794
+ <form aria-labelledby="daMainQuestion" action="/interview?i=docassemble.base:data/questions/examples/fields-ajax.yml" id="daform" class="form-horizontal daformfields" method="POST" novalidate="novalidate">
795
+ <div class="da-page-header">
796
+ <h1 class="h3" id="daMainQuestion">What is your favorite word?</h1>
797
+ <div class="daclear"></div>
798
+ </div>
799
+ <div
800
+ class="da-container da-form-group row darequired da-field-container da-field-container-datatype-text da-field-container-inputtype-ajax">
801
+ <label for="ZmF2b3JpdGVfd29yZA" class="col-md-4 col-form-label da-form-label datext-right" >Word</label>
802
+ <div class="col-md-8 dafieldpart">
803
+ <div class="combobox-container">
804
+ <input type="hidden" name="ZmF2b3JpdGVfd29yZA" value="" />
805
+ <div class="input-group">
806
+ <input type="text" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-activedescendant="" autocomplete="off" id="ZmF2b3JpdGVfd29yZA" aria-controls="ZmF2b3JpdGVfd29yZAmenu" aria-owns="ZmF2b3JpdGVfd29yZAmenu" placeholder="" required="required" class="form-control da-ajax-combobox"/>
807
+ <ul role="listbox" class="typeahead typeahead-long dropdown-menu" id="ZmF2b3JpdGVfd29yZAmenu"></ul>
808
+ <div class="input-group-append">
809
+ <button class="btn btn-outline-secondary dacomboboxtoggle" type="button" tabindex="-1" aria-label="Dropdown" aria-expanded="false" aria-controls="ZmF2b3JpdGVfd29yZAmenu">
810
+ <svg class="svg-inline--fa fa-caret-down"></svg
811
+ ><!-- <span class="fas fa-xmark"></span> Font Awesome fontawesome.com -->
812
+ </button>
813
+ </div>
814
+ </div>
815
+ </div>
816
+ <select data-action="wordlist" data-trig="4" alt="Input box" class="form-control da-ajax-combobox" id="ZmF2b3JpdGVfd29yZAcombobox" required="" style="display: none;">
817
+ <option selected="selected"></option>
818
+ </select>
819
+ </div>
820
+ </div>
821
+ <fieldset class="da-button-set da-field-buttons">
822
+ <legend class="visually-hidden">
823
+ Press one of the following buttons:
824
+ </legend>
825
+ <button class="btn btn-da btn-primary" id="da-continue-button" type="submit">
826
+ Continue
827
+ </button>
828
+ </fieldset>
829
+ <input
830
+ type="hidden"
831
+ name="csrf_token"
832
+ value="ImI4NDAyMmI3Mjc4NGY3MDlkMTRiNGE5OTQzZDhmZTVhYWY5NzQwNzUi.ZdVoHQ.GLQUgOWjNag9f7jGSiO9ZvHDoEc"
833
+ />
834
+ <input type="hidden" name="_question_name" value="Question_0" />
835
+ <input type="hidden" name="_tracker" value="2" />
836
+ <input type="hidden" name="_visible" value="" />
837
+ <input
838
+ type="hidden"
839
+ name="_varnames"
840
+ value="eyJYMlpwWld4a1h6QSI6ICJabUYyYjNKcGRHVmZkMjl5WkEifQ"
841
+ />
842
+ </form>
843
+ </div>`;
844
+
845
+ // ============================
846
+ // quotes
847
+ // ============================
792
848
  html.mixed_quotes = `
793
849
  <div id="daquestion" aria-labelledby="dapagetitle" role="main" class="tab-pane fade show active offset-xl-3 offset-lg-3 col-xl-6 col-lg-6 offset-md-2 col-md-8">
794
850
  <div data-variable="ZG91YmxlX3F1b3RlX2RpY3RbJ2RvdWJsZV9xdW90ZV9rZXknXQ" id="trigger" aria-hidden="true" style="display: none;"></div>
@@ -892,5 +948,4 @@ html.mixed_quotes = `
892
948
  </form>
893
949
  </div>`;
894
950
 
895
-
896
951
  module.exports = html;