@suffolklitlab/alkiln 5.5.0 → 5.7.0-feature-tap1

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/lib/scope.js CHANGED
@@ -174,14 +174,14 @@ module.exports = {
174
174
  if ( showif ) { await scope.page.waitForTimeout( scope.showif_timeout ); }
175
175
  },
176
176
 
177
- waitForTab: async function waitForTab(scope, tab_to_show) {
177
+ waitForTab: async function waitForTab(scope, { tab_id }) {
178
178
  /**
179
- * @param {tab_to_show} the aria-labeledby text of the tab panel, which
179
+ * @param { tab_id } the aria-labeledby text of the tab panel, which
180
180
  * we use to query and wait for that tab panel to be shown.
181
181
  */
182
182
  // Implementation linked to https://github.com/SuffolkLITLab/docassemble-ALToolbox/blob/4aa1069389d2ca9cadb586f309dd73d791f081d1/docassemble/ALToolbox/misc.py#L127
183
183
  // I think `.$()` does not use a timeout
184
- if ( tab_to_show.length == 0) { return }
184
+ if ( tab_id.length == 0) { return }
185
185
  // waiting for the transitionend event: https://stackoverflow.com/a/57955251/11416267
186
186
  await scope.page.evaluate((element_query) => {
187
187
  return new Promise((resolve) => {
@@ -198,13 +198,16 @@ module.exports = {
198
198
  resolve();
199
199
  }
200
200
  });
201
- }, `div[aria-labelledby="${tab_to_show}"]`);
201
+ }, `div[aria-labelledby="${ tab_id }"]`);
202
202
  },
203
203
 
204
- afterStep: async function afterStep(scope, options = {waitForShowIf: false, tabToWaitFor: '', navigated: false, waitForTimeout: 0}) {
205
- /* Common things to do after a step, including take care of errors.
206
- * TODO: discuss splitting into `afterField` and `afterStep` or something. Or just change name
207
- * TODO: Update to latest cucumberjs, which has `AfterStep` */
204
+ afterStep: async function afterStep(scope, options = {waitForShowIf: false, navigated: false, waitForTimeout: 0}) {
205
+ /** Common things to do after a step, including take care of errors.
206
+ * `waitForShowIf` is automatically `false` for observation/non-interactive steps, etc.
207
+ *
208
+ * TODO: discuss splitting into `afterField` and `afterStep` or something. Or just change name
209
+ * TODO: Update to latest cucumberjs, which has `AfterStep`
210
+ */
208
211
 
209
212
  if ( scope.page ) {
210
213
  // Use a while loop until the below does not error with `Error: Execution context was destroyed`
@@ -231,7 +234,6 @@ module.exports = {
231
234
 
232
235
  // Wait for elements that might get revealed
233
236
  if ( options.waitForShowIf ) { await scope.waitForShowIf( scope ); }
234
- if ( options.tabToWaitFor ) { await scope.waitForTab( scope, options.tabToWaitFor); }
235
237
  } // ends if scope.page
236
238
 
237
239
  if ( options.navigated ) {
@@ -248,39 +250,56 @@ module.exports = {
248
250
  if ( options.waitForTimeout ) { await time.waitForTimeout( options.waitForTimeout ); }
249
251
  }, // Ends afterStep
250
252
 
251
- tapElement: async function tapElement(scope, elem, tabToWaitFor='', waitForTimeout=0) {
252
- /* Tap a given element, detect navigation, and do appropriate related things. */
253
+ tapElementAndNavigate: async function(scope, { elem, waitForTimeout=0 }) {
254
+ /** Tap a given element, detect navigation, and do appropriate related things.
255
+ * TODO: not quite sure if we'll be using `waitForTimeout`. */
253
256
 
254
- // This check might never be needed now as we try to match fields on the page
255
- // first in user-triggered cases - users used to be able to access this almost
256
- // directly, but they can't anymore. Our functions should know to only pass good things.
257
- let msg = `Cannot find the element to tap.`;
258
- if ( !elem ) { reports.addToReport(scope, { type: `error`, value: msg }); }
257
+ let start_url = await scope.page.url(); // so we can later check for navigation
259
258
 
260
- expect( elem, msg ).to.exist;
259
+ await scope.tapElementAndStayOnPage(scope, {
260
+ elem,
261
+ click_promise: Promise.all([
262
+ scope.page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
263
+ elem.evaluate( (el) => { return el.click(); })
264
+ ])
265
+ });
261
266
 
262
- // Get the text on the button or link for a potential error message later
263
- let tapperText = await (await elem.getProperty('textContent')).jsonValue();
267
+ // TODO: Not sure how to detect navigation landing at same page, such as
268
+ // with a user input validation error. Does that count as navigating?
269
+ // Maybe url change + trigger var change? Maybe just trigger var?
270
+ let end_url = await scope.page.url();
271
+ let navigated = start_url !== end_url;
264
272
 
265
- let start_url = await scope.page.url(); // so we can later check for navigation
273
+ // Might be able to handle this in `scope.afterStep`
274
+ if ( navigated ) {
266
275
 
267
- // We'll wait for the click itself at the very least
268
- let click_promises = [ elem.evaluate( (el) => { return el.click(); }) ];
269
-
270
- // If we're possibly navigating, wait for navigation as well
271
- if ( !tabToWaitFor ) {
272
- click_promises.push( scope.page.waitForNavigation({ waitUntil: 'domcontentloaded' }) );
276
+ // TODO: Can we remove this?
277
+ // Save the id that comes next
278
+ let { question_has_id, id, id_matches } = await scope.examinePageID( scope, 'none to match' );
279
+
280
+ // If navigation, wait for 200ms to let things finish loading and moving
281
+ await scope.afterStep(scope, { waitForTimeout: 200, navigated: true });
282
+ } else {
283
+ await scope.afterStep(scope, { waitForTimeout, navigated: false });
273
284
  }
274
- let clickWait = Promise.all( click_promises );
285
+ }, // Ends scope.tapElementAndNavigate()
275
286
 
276
- let winner = null;
287
+ tapElementAndStayOnPage: async function(scope, { elem, click_promise }) {
288
+ /** Tap a given element, detect navigation, and do appropriate related things.
289
+ * `click_promise can be any kind of Promise, including a Promise.all()
290
+ */
291
+ let msg = `Cannot find the element to tap.`;
292
+ if ( !elem ) { reports.addToReport(scope, { type: `error`, value: msg }); }
293
+ expect( elem, msg ).to.exist;
277
294
 
295
+ click_promise = click_promise || elem.click();
296
+
278
297
  // TODO: implement and use scope.handle_possible_timeout()
279
298
  try {
280
- // Error loads page, so no need to detect to keep things short
281
- // Also, puppeteer will ensure proper timeout.
282
- winner = await Promise.race([
283
- clickWait,
299
+ // Serever error loads page, so keeping things shorter by not looking
300
+ // for that text too. Also, puppeteer will ensure proper timeout.
301
+ await Promise.race([
302
+ click_promise,
284
303
  // This is meant to detect user-visible alerts/errors,
285
304
  // not breaking errors in the testing code.
286
305
  scope.page.waitForSelector('.alert-danger', { visible: true }),
@@ -292,6 +311,9 @@ module.exports = {
292
311
  let non_reload_report_msg = `ALKiln tapped "${ tapperText }", but the interview took to long to finish.`;
293
312
  await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
294
313
  } else {
314
+
315
+ // Get the text on the button or link for a potential error message later
316
+ let tapperText = await (await elem.getProperty('textContent')).jsonValue();
295
317
  // Throw any non-timeout error
296
318
  let err_msg = `Error occurred when ALKiln tried to tap "${ tapperText }".`
297
319
  reports.addToReport( scope, { type: `error`, value: err_msg });
@@ -299,30 +321,7 @@ module.exports = {
299
321
  } // ends if error is timeout error
300
322
 
301
323
  } // ends try/catch
302
-
303
- // TODO: Not sure how to detect navigation landing at same page, such as
304
- // with a user input validation error. Does that count as navigating?
305
- // Maybe url change + trigger var change? Maybe just trigger var?
306
- let end_url = await scope.page.url();
307
- let navigated = start_url !== end_url;
308
-
309
- // Might be able to handle this in `scope.afterStep`
310
- if ( navigated ) {
311
- // Save the id that comes next
312
- let { question_has_id, id, id_matches } = await scope.examinePageID( scope, 'none to match' );
313
- // If navigation, wait for 200ms to let things settle down
314
- await scope.afterStep(scope, {waitForTimeout: 200, navigated: navigated});
315
- } else {
316
- // If stay on page, wait for `show if`, the new tab to be loaded, or feedback elements to be move around
317
- options = {waitForShowIf: !tabToWaitFor, tabToWaitFor: tabToWaitFor, navigated: navigated};
318
- if (waitForTimeout > 0) {
319
- options = {waitForTimeout: waitForTimeout, navigated: navigated};
320
- }
321
- await scope.afterStep(scope, options);
322
- }
323
-
324
- return winner;
325
- }, // Ends scope.tapElement()
324
+ }, // Ends scope.tapElementAndStayOnPage()
326
325
 
327
326
  // Handle server reload errors
328
327
  server_statuses: [],
@@ -516,7 +515,7 @@ module.exports = {
516
515
  ]);
517
516
  await elem.evaluate( el => { return el.className });
518
517
  // Waits for navigation or user error
519
- await scope.tapElement( scope, elem );
518
+ await scope.tapElementAndNavigate( scope, { elem });
520
519
  }, // Ends scope.continue()
521
520
 
522
521
  continue_exists: async function ( scope ) {
@@ -545,7 +544,8 @@ module.exports = {
545
544
  reports.addToReport(scope, { type: `error`, value: error_msg });
546
545
  }
547
546
  expect( elem, error_msg ).to.exist;
548
- await scope.tapElement( scope, elem, tab_id );
547
+ await scope.tapElementAndStayOnPage(scope, { elem });
548
+ await scope.waitForTab( scope, { tab_id });
549
549
  },
550
550
 
551
551
  checkForA11y: async function (scope ) {
@@ -586,14 +586,25 @@ module.exports = {
586
586
  };
587
587
  },
588
588
 
589
- tapElementBySelector: async function ( scope, selector, waitForTimeout ) {
590
- let elem = await scope.page.$(`${selector}`);
591
- let error_msg = `Couldn't find "${selector}" on the page, is there a typo?`;
589
+ tapSelectorToNavigate: async function ( scope, selector, waitForTimeout ) {
590
+ let elem = await scope.page.$(`${ selector }`);
591
+ let error_msg = `Couldn't find "${ selector }" on the page, is there a typo?`;
592
+ if ( !elem ) {
593
+ reports.addToReport(scope, { type: `error`, value: error_msg });
594
+ }
595
+ expect( elem, error_msg ).to.exist;
596
+ await scope.tapElementAndNavigate( scope, { elem, waitForTimeout });
597
+ },
598
+
599
+ tapSelectorAndStay: async function ( scope, { selector }) {
600
+ let elem = await scope.page.$(`${ selector }`);
601
+ let error_msg = `Couldn't find "${ selector }" on the page, is there a typo?`;
592
602
  if ( !elem ) {
593
603
  reports.addToReport(scope, { type: `error`, value: error_msg });
594
604
  }
595
605
  expect( elem, error_msg ).to.exist;
596
- await scope.tapElement( scope, elem, '', waitForTimeout );
606
+ await scope.tapElementAndStayOnPage(scope, { elem });
607
+ await scope.afterStep(scope, { waitForShowIf: false, navigated: false, waitForTimeout: 0 });
597
608
  },
598
609
 
599
610
  load: async function ( scope, file_name ) {
@@ -1539,7 +1550,7 @@ module.exports = {
1539
1550
 
1540
1551
  enter_answer: {
1541
1552
  button: async function ( scope, { handle, answer }) {
1542
- await scope.tapElement( scope, handle );
1553
+ await scope.tapElementAndNavigate( scope, { elem: handle });
1543
1554
  return true;
1544
1555
  },
1545
1556
  textarea: async function ( scope, { handle, answer }) {
@@ -2466,16 +2477,16 @@ module.exports = {
2466
2477
  }, // Ends scope.steps.set_secret_var
2467
2478
 
2468
2479
  tap_term: async ( scope, phrase ) => {
2469
- /** Tap a link that doesn't navigate. Depends on the language of the text. */
2480
+ /** Tap a link that doesn't navigate. Depends on the language of the text.
2481
+ * TODO: This may be broken at this point.
2482
+ * */
2470
2483
  const [link] = await scope.page.$x(`//a[contains(text(), "${phrase}")]`);
2471
2484
 
2472
2485
  let msg = `The term "${ phrase }" seems to be missing.`;
2473
2486
  if ( !link ) { reports.addToReport(scope, { type: `error`, value: msg }); }
2474
2487
  expect( link, msg ).to.exist;
2475
-
2476
- await scope.tapElement( scope, link );
2477
-
2478
- await scope.afterStep(scope, {waitForShowIf: true});
2488
+ await scope.tapElementAndStayOnPage(scope, { elem: link });
2489
+ await scope.page.waitForTimeout(10000);
2479
2490
  }, // Ends scope.steps.tap_term()
2480
2491
 
2481
2492
  sign: async ( scope, name ) => {
@@ -2529,7 +2540,7 @@ module.exports = {
2529
2540
  if (failed_to_download) {
2530
2541
  reports.addToReport(scope, { type: `warning`, value: `Could not download file using fetch (${ err_msg }). ALKiln will now fallback to the click download method` });
2531
2542
  scope.toDownload = filename;
2532
- // Should this be using `scope.tapElement()`?
2543
+ // Should this be using `scope.tapElement`?
2533
2544
  await elem.evaluate( elem => { return elem.click(); });
2534
2545
  await scope.detectDownloadComplete( scope );
2535
2546
  }
@@ -2702,7 +2713,7 @@ module.exports = {
2702
2713
  await scope.page.type( `#email`, email );
2703
2714
  await scope.page.type( `#password`, password );
2704
2715
  let elem = await scope.page.$( `button[name="submit"]` );
2705
- await scope.tapElement( scope, elem );
2716
+ await scope.tapElementAndNavigate( scope, { elem });
2706
2717
 
2707
2718
  // Wait to see the success message.
2708
2719
  // This is a bit of an assumption and needs success and failure testing
package/lib/steps.js CHANGED
@@ -67,6 +67,10 @@ to detect that.
67
67
  regex thoughts: https://stackoverflow.com/questions/171480/regex-grabbing-values-between-quotation-marks
68
68
  */
69
69
 
70
+ // Make sure all the required env vars exist. Required to be
71
+ // here as well as `setup`` specifically for local development.
72
+ session_vars.validateEnvironment();
73
+
70
74
  // Hoping that most load and download times will be under 30 seconds.
71
75
  // TODO: Check what average load and download times are in our current interviews.
72
76
  let default_timeout = 30 * 1000
@@ -250,7 +254,7 @@ Given(/I (?:sign|log) ?(?:in)?(?:on)?(?:to the server)? with(?: the email)? "([^
250
254
  * I sign on with the email "USER1_EMAIL" and the password "USER1_PASSWORD" SECRETs
251
255
  * I log in with "USER1_EMAIL" and "USER1_PASSWORD"
252
256
  */
253
- // Exiting the custom timeout happens in scope.tapElement() if needed
257
+ // `page` timeout will deal with custom timeout
254
258
  // Couldn't test the timeout for tapping the button because there's not
255
259
  // enough of a pause between initial navigation and pressing button.
256
260
  await scope.steps.sign_in( scope, {
@@ -741,24 +745,32 @@ When(/I tap to continue/i, { timeout: -1 }, async () => {
741
745
  // Any selectors for this seem somewhat precarious
742
746
  // No data will cause an automatic continue when `ensure_navigation` is true
743
747
 
744
- // Exiting the custom timeout happens in scope.tapElement() where we can
748
+ // `page` timeout will deal with custom timeout
745
749
  // check if the server is reloading
746
750
  await scope.setFields( scope, { var_data: [], ensure_navigation: true });
747
751
  });
748
752
 
749
753
  When(/I tap the "([^"]+)" tab/i, { timeout: -1 }, async ( tabId ) => {
750
- // Exiting the custom timeout happens in scope.tapElement() if needed
754
+ // `page` timeout will deal with custom timeout
751
755
  await scope.tapTab( scope, tabId );
752
756
  });
753
757
 
754
- When(/I tap the "([^"]+)" element$/i, { timeout: -1 }, async ( elemSelector ) => {
755
- // Exiting the custom timeout happens in scope.tapElement() if needed
756
- await scope.tapElementBySelector( scope, elemSelector, 2000 );
758
+ When(/I tap the "([^"]+)" element(?: and go to (?:a new|the next) page)?$/i, { timeout: -1 }, async ( elemSelector ) => {
759
+ // `page` timeout will deal with custom timeout
760
+ await scope.tapSelectorToNavigate( scope, elemSelector, 2000 );
761
+ });
762
+
763
+ When(/I tap the "([^"]+)" element and stay(?: on the same page)?$/i, { timeout: -1 }, async ( selector ) => {
764
+ // `page` timeout will deal with custom timeout
765
+ await scope.tapSelectorAndStay( scope, { selector });
757
766
  });
758
767
 
759
- When(/I tap the "([^"]+)" element and wait ([0-9]+) second(?:s)?/i, { timeout: -1 }, async ( elemSelector, waitTime ) => {
760
- // Exiting the custom timeout happens in scope.tapElement() if needed
761
- await scope.tapElementBySelector( scope, elemSelector, waitTime * 1000 )
768
+ When(/I tap the "([^"]+)" element and wait (?:for )?([0-9]+) second(?:s)?/i, { timeout: -1 }, async ( elemSelector, waitTime ) => {
769
+ // TODO: Why is it the developer can't just wait after the step?
770
+ // Also, this is always supposed to navigate, and that handles its own waiting.
771
+
772
+ // `page` timeout will deal with custom timeout
773
+ await scope.tapSelectorToNavigate( scope, elemSelector, waitTime * 1000 )
762
774
  });
763
775
 
764
776
  When(/I check the page for (?:accessibility|a11y) issues/i, {timeout: -1}, async () => {
@@ -963,7 +975,7 @@ When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async (
963
975
  * 1. I set the var "benefits['SSI']" to "false"
964
976
  * 1. I set the variable "rent_interval" to "weekly"
965
977
  */
966
- // Exiting the custom timeout happens in scope.tapElement() if needed
978
+ // `page` timeout will deal with custom timeout
967
979
  await scope.steps.set_regular_var( scope, var_name, answer )
968
980
 
969
981
  });
@@ -971,7 +983,7 @@ When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async (
971
983
  When(/I set the var(?:iable)? "([^"]+)" to ?(?:the)? ?(?:GitHub)? secret "([^"]+)"/i, { timeout: -1 }, async ( var_name, set_to_env_name ) => {
972
984
  /* Set a non-story table variable to a secrete value. Same as the above, but reads the value from `process.env` */
973
985
  // This could theoretically tap an element, so
974
- // exiting the custom timeout happens in scope.tapElement() if needed
986
+ // `page` timeout will deal with custom timeout
975
987
  await scope.steps.set_secret_var( scope, var_name, set_to_env_name )
976
988
  });
977
989
 
@@ -991,7 +1003,7 @@ Then(/I upload "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async ( filenames, var_
991
1003
 
992
1004
  When('I tap the defined text link {string}', { timeout: -1 }, async ( phrase ) => {
993
1005
  /** Tap a link that doesn't navigate. Depends on the language of the text. */
994
- // exiting the custom timeout happens in scope.tapElement() if needed
1006
+ // `page` timeout will deal with custom timeout
995
1007
  await scope.steps.tap_term( scope, phrase );
996
1008
  });
997
1009
 
@@ -1018,7 +1030,7 @@ When(/I download "([^"]+)"$/, { timeout: -1 }, async ( filename ) => {
1018
1030
  * page load. Just stick to the name of the file.
1019
1031
  */
1020
1032
  // TODO:
1021
- // This should be using `scope.tapElement()`? When someone clicks on a link to
1033
+ // This should be using `scope.tapElementAndStayOnPage()`? When someone clicks on a link to
1022
1034
  // download a document, it sends a request to the server, which could be reloading
1023
1035
  return wrapPromiseWithTimeout(
1024
1036
  scope.steps.download( scope, filename ),
@@ -371,8 +371,6 @@ session_vars.validateEnvironment = function() {
371
371
  errorMessage = errorMessage.trim();
372
372
 
373
373
  if (errorMessage !== '') {
374
- throw new ReferenceError(`ALKiln error: ${errorMessage}`);
374
+ throw new ReferenceError(`ALKiln error:\n${errorMessage}`);
375
375
  }
376
376
  };
377
-
378
- session_vars.validateEnvironment();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suffolklitlab/alkiln",
3
- "version": "5.5.0",
3
+ "version": "5.7.0-feature-tap1",
4
4
  "description": "Integrated automated end-to-end testing with docassemble, puppeteer, and cucumber.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -1,73 +0,0 @@
1
- name: Test the tests
2
-
3
- on:
4
- push:
5
- workflow_dispatch:
6
- inputs:
7
- tags:
8
- required: False
9
- description: 'Optional. Use a "tag expression" specify which tagged tests to run (https://cucumber.io/docs/cucumber/api/#tag-expressions)'
10
- default: ''
11
-
12
- jobs:
13
-
14
- puppeteer-tests:
15
- runs-on: ubuntu-latest
16
-
17
- strategy:
18
- matrix:
19
- node-version: [18.x]
20
-
21
- env:
22
- BASE_URL: ${{ secrets.SERVER_URL }}
23
- PLAYGROUND_EMAIL: ${{ secrets.PLAYGROUND_EMAIL }}
24
- PLAYGROUND_PASSWORD: ${{ secrets.PLAYGROUND_PASSWORD }}
25
- PLAYGROUND_ID: ${{ secrets.PLAYGROUND_ID }}
26
- DOCASSEMBLE_DEVELOPER_API_KEY: ${{ secrets.DOCASSEMBLE_DEVELOPER_API_KEY }}
27
- USER1_EMAIL: ${{ secrets.USER1_EMAIL }}
28
- USER1_PASSWORD: ${{ secrets.USER1_PASSWORD }}
29
- SECRET_VAR1: secret-var1-value
30
- SECRET_VAR2: secret-var2-value
31
- SECRET_FOR_MISSING_FIELD: secret for missing field
32
- _ORIGIN: github
33
-
34
- name: Run Puppeteer tests
35
- steps:
36
- - name: Set Github-related environment variables
37
- run: |
38
- echo "REPO_URL=${{ github.server_url }}/${{ github.repository }}" >> $GITHUB_ENV
39
- echo "BRANCH_PATH=${{ github.ref }}" >> $GITHUB_ENV
40
- - name: Set name of artifact folder with date and UTC time
41
- # https://www.shell-tips.com/linux/how-to-format-date-and-time-in-linux-macos-and-bash/#how-to-format-a-date-in-bash
42
- run: |
43
- echo "ARTIFACT_NAME=alkiln-$(date +'%Y-%m-%d at %Hh%Mm%Ss' -u)UTC" >> $GITHUB_ENV
44
- echo "UNIT_TESTS_ARTIFACT_NAME=_alkiln-misc-$(date +'%Y-%m-%d at %Hh%Mm%Ss' -u)UTC" >> $GITHUB_ENV
45
- # For the testing repo, the `REPO_URL` is that of a different repo and is created in `env:`
46
- - run: echo "Repo address is $REPO_URL"
47
- - name: Checkout
48
- uses: actions/checkout@v3
49
- - name: Setup environment
50
- uses: actions/setup-node@v3
51
- with:
52
- node-version: ${{ matrix.node-version }}
53
- - run: npm install
54
- - run: npm run setup
55
- - run: npm run test ${{ github.event.inputs.tags && format('{0}', github.event.inputs.tags) }}
56
- # Artifacts
57
- - name: upload unit tests artifacts folder
58
- uses: actions/upload-artifact@v3
59
- # This will upload even if a previous step has failed
60
- if: ${{ always() }}
61
- with:
62
- name: ${{ env.UNIT_TESTS_ARTIFACT_NAME }}
63
- path: ./_alkiln-*
64
- - name: upload integration tests artifacts folder
65
- uses: actions/upload-artifact@v3
66
- if: ${{ always() }}
67
- with:
68
- name: ${{ env.ARTIFACT_NAME }}
69
- path: ./alkiln-*
70
- # Delete Playground Project and project name file
71
- - name: Cleanup
72
- if: ${{ always() }}
73
- run: npm run takedown