@appliqation/automation-sdk 2.2.0 → 2.3.0
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/README.md +269 -7
- package/package.json +2 -2
- package/src/AppliqationClient.js +47 -2
- package/src/core/HttpClient.js +52 -0
- package/src/reporters/playwright/AppliqationReporter.js +372 -66
- package/src/reporters/playwright/helpers/UuidExtractor.js +8 -3
- package/src/services/ResultService.js +36 -1
- package/src/services/RunMatrixService.js +28 -0
- package/src/services/TaggingService.js +211 -14
package/README.md
CHANGED
|
@@ -459,15 +459,277 @@ DETAILED ERRORS & WARNINGS:
|
|
|
459
459
|
|
|
460
460
|
---
|
|
461
461
|
|
|
462
|
-
## Auto-
|
|
462
|
+
## Auto-Tagging Test Cases
|
|
463
463
|
|
|
464
|
-
|
|
464
|
+
The SDK automatically tags test cases with "Appq_automated" (configurable) after their **first successful run**. This helps you track which test cases have been automated and are actively running in your test suite.
|
|
465
465
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
466
|
+
### How It Works
|
|
467
|
+
|
|
468
|
+
1. **Test runs and passes** → SDK submits result to Appliqation
|
|
469
|
+
2. **Backend accepts result** → SDK triggers auto-tagging (fire-and-forget)
|
|
470
|
+
3. **Check if already tagged** → Skip if test case already has the tag
|
|
471
|
+
4. **Add tag** → Test case gets tagged in Appliqation UI
|
|
472
|
+
|
|
473
|
+
**Key Features:**
|
|
474
|
+
- ✅ **Enabled by default** when Appliqation reporting is enabled
|
|
475
|
+
- ✅ **Fire-and-forget** - tagging failures never block your test runs
|
|
476
|
+
- ✅ **Smart deduplication** - checks before tagging, won't create duplicate tags
|
|
477
|
+
- ✅ **Only accepted results** - backend-rejected results are NOT tagged
|
|
478
|
+
- ✅ **Works for both** single and batch result submissions
|
|
479
|
+
- ✅ **Async execution** - zero impact on test execution performance
|
|
480
|
+
|
|
481
|
+
### Configuration
|
|
482
|
+
|
|
483
|
+
#### Environment Variables
|
|
484
|
+
|
|
485
|
+
Add to your `.env` file:
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
# Auto-Tagging Configuration (optional - all have sensible defaults)
|
|
489
|
+
APPLIQATION_AUTO_TAG_ENABLED=true # Enable/disable (default: true)
|
|
490
|
+
APPLIQATION_AUTO_TAG_NAME=Appq_automated # Custom tag name (default: Appq_automated)
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
#### Playwright Reporter Config
|
|
494
|
+
|
|
495
|
+
Configure in `playwright.config.js`:
|
|
496
|
+
|
|
497
|
+
```javascript
|
|
498
|
+
reporter: [
|
|
499
|
+
['@appliqation/automation-sdk/playwright/reporter', {
|
|
500
|
+
apiKey: process.env.APPLIQATION_API_KEY,
|
|
501
|
+
projectKey: process.env.APPLIQATION_PROJECT_KEY,
|
|
502
|
+
|
|
503
|
+
// Auto-tagging options (optional)
|
|
504
|
+
autoTag: true, // Enable auto-tagging (default: true)
|
|
505
|
+
autoTagName: 'My_Custom_Tag' // Custom tag name (default: 'Appq_automated')
|
|
506
|
+
}]
|
|
507
|
+
]
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
#### Programmatic Configuration
|
|
511
|
+
|
|
512
|
+
When using the SDK directly:
|
|
513
|
+
|
|
514
|
+
```javascript
|
|
515
|
+
const { AppliqationClient } = require('@appliqation/automation-sdk');
|
|
516
|
+
|
|
517
|
+
const client = new AppliqationClient({
|
|
518
|
+
apiKey: 'your_api_key',
|
|
519
|
+
projectKey: 'your_project_key',
|
|
520
|
+
|
|
521
|
+
// Auto-tagging options
|
|
522
|
+
options: {
|
|
523
|
+
autoTag: true, // Enable auto-tagging (default: true)
|
|
524
|
+
autoTagName: 'Automated_Test' // Custom tag name (default: 'Appq_automated')
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Disabling Auto-Tagging
|
|
530
|
+
|
|
531
|
+
If you want to disable auto-tagging:
|
|
532
|
+
|
|
533
|
+
**Option 1: Environment Variable**
|
|
534
|
+
```bash
|
|
535
|
+
APPLIQATION_AUTO_TAG_ENABLED=false
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
**Option 2: Config**
|
|
539
|
+
```javascript
|
|
540
|
+
{
|
|
541
|
+
options: {
|
|
542
|
+
autoTag: false
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### What You'll See
|
|
548
|
+
|
|
549
|
+
When auto-tagging is working:
|
|
550
|
+
|
|
551
|
+
```
|
|
552
|
+
✅ Auto-tagged 3 test case(s) with "Appq_automated"
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
When test cases are already tagged (second run):
|
|
556
|
+
|
|
557
|
+
```
|
|
558
|
+
DEBUG: Skipped 3 already-tagged test case(s)
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
If tagging fails (non-blocking):
|
|
562
|
+
|
|
563
|
+
```
|
|
564
|
+
⚠️ Auto-tagging failed (non-blocking): Connection timeout
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### Troubleshooting
|
|
568
|
+
|
|
569
|
+
**Q: I don't see the tag in Appliqation UI**
|
|
570
|
+
|
|
571
|
+
Check:
|
|
572
|
+
1. Is `APPQ_ENABLE=1` set? (Auto-tagging only works when reporting is enabled)
|
|
573
|
+
2. Did the test result get accepted by backend? (Check for backend validation errors)
|
|
574
|
+
3. Check SDK logs for "Auto-tagged X test case(s)" message
|
|
575
|
+
|
|
576
|
+
**Q: Can I use a custom tag name?**
|
|
577
|
+
|
|
578
|
+
Yes! Set `APPLIQATION_AUTO_TAG_NAME=Your_Tag_Name` in your `.env` file or use the config options shown above.
|
|
579
|
+
|
|
580
|
+
**Q: Does tagging failure affect my test results?**
|
|
581
|
+
|
|
582
|
+
No! Auto-tagging is fire-and-forget. If tagging fails, it's logged as a warning but your test run continues normally and results are still submitted successfully.
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## Handling Orphan Tests
|
|
587
|
+
|
|
588
|
+
### What are Orphan Tests?
|
|
589
|
+
|
|
590
|
+
**Orphan tests** are tests that execute successfully but **cannot be mapped to Appliqation test cases** because they're missing UUID annotations. When a test runs without a UUID, the SDK cannot link it to a specific test case in your Appliqation project, making the result "orphaned."
|
|
591
|
+
|
|
592
|
+
```javascript
|
|
593
|
+
// ❌ This test will be orphaned (no UUID annotation)
|
|
594
|
+
test('Login with valid credentials', async ({ page }) => {
|
|
595
|
+
await page.goto('/login');
|
|
596
|
+
await page.fill('#username', 'user@example.com');
|
|
597
|
+
// ... test code
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// ✅ This test will be properly mapped (has UUID annotation)
|
|
601
|
+
test('Login with valid credentials', { tag: '@uuid:1154-abc-def' }, async ({ page }) => {
|
|
602
|
+
await page.goto('/login');
|
|
603
|
+
await page.fill('#username', 'user@example.com');
|
|
604
|
+
// ... test code
|
|
605
|
+
});
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### Automatic Orphan Run Cleanup
|
|
609
|
+
|
|
610
|
+
By default, the SDK **automatically prevents corrupted runs** from being created when ALL tests in a run are orphaned:
|
|
611
|
+
|
|
612
|
+
**Default Behavior:**
|
|
613
|
+
- ✅ If ALL tests lack UUIDs → Run is **deleted** and a clear error message is shown
|
|
614
|
+
- ✅ If SOME tests have UUIDs → Run is **kept**, valid results are submitted, orphans are logged as warnings
|
|
615
|
+
- ✅ CI/CD pipeline **fails with exit code 1** when orphan-only runs are detected
|
|
616
|
+
- ✅ Clear, actionable error message guides users on how to fix the issue
|
|
617
|
+
|
|
618
|
+
**Why?** Orphan-only runs create empty entries in Appliqation with "N/A" pass rates, which corrupts your analytics and dashboards.
|
|
619
|
+
|
|
620
|
+
### Error Message Example
|
|
621
|
+
|
|
622
|
+
When all tests are orphaned, you'll see:
|
|
623
|
+
|
|
624
|
+
```
|
|
625
|
+
╔════════════════════════════════════════════════════════════════════╗
|
|
626
|
+
║ ❌ RUN CREATION FAILED - ALL TESTS MISSING UUID ANNOTATIONS ║
|
|
627
|
+
╠════════════════════════════════════════════════════════════════════╣
|
|
628
|
+
║ Project: 1162-MyProject ║
|
|
629
|
+
║ Orphan Tests: 6 ║
|
|
630
|
+
║ ║
|
|
631
|
+
║ ⚠️ NO RESULTS WERE SUBMITTED TO APPLIQATION ║
|
|
632
|
+
║ The test run was automatically deleted to prevent analytics ║
|
|
633
|
+
║ corruption. All tests are missing UUID annotations. ║
|
|
634
|
+
╠════════════════════════════════════════════════════════════════════╣
|
|
635
|
+
║ ✅ ACTION REQUIRED: Add UUID Annotations ║
|
|
636
|
+
╠════════════════════════════════════════════════════════════════════╣
|
|
637
|
+
║ Option 1: Using test tags (Recommended) ║
|
|
638
|
+
║ test('My Test', { tag: '@uuid:123-xxx' }, async ({ page }) => { ║
|
|
639
|
+
║ // your test code ║
|
|
640
|
+
║ }); ║
|
|
641
|
+
╚════════════════════════════════════════════════════════════════════╝
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### Configuration Options
|
|
645
|
+
|
|
646
|
+
You can customize orphan handling behavior via environment variables:
|
|
647
|
+
|
|
648
|
+
```env
|
|
649
|
+
# .env file
|
|
650
|
+
|
|
651
|
+
# Delete runs with only orphan tests (default: true)
|
|
652
|
+
APPLIQATION_DELETE_ORPHAN_RUNS=true
|
|
653
|
+
|
|
654
|
+
# Exit with error code 1 for orphan-only runs (default: true)
|
|
655
|
+
APPLIQATION_FAIL_ON_ORPHAN_RUNS=true
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
**Configuration via playwright.config.js:**
|
|
659
|
+
|
|
660
|
+
```javascript
|
|
661
|
+
reporter: [
|
|
662
|
+
[
|
|
663
|
+
'@appliqation/automation-sdk-js/playwright',
|
|
664
|
+
{
|
|
665
|
+
deleteOrphanOnlyRuns: true, // Delete orphan-only runs (default: true)
|
|
666
|
+
failOnOrphanOnlyRuns: true, // Fail CI/CD for orphan-only runs (default: true)
|
|
667
|
+
}
|
|
668
|
+
]
|
|
669
|
+
]
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### Mixed Scenarios (Some Tests Have UUIDs)
|
|
673
|
+
|
|
674
|
+
When your test suite has **both valid and orphan tests**, the SDK handles it gracefully:
|
|
675
|
+
|
|
676
|
+
```javascript
|
|
677
|
+
// Project 1162: 3 tests total
|
|
678
|
+
test('Valid Test 1', { tag: '@uuid:1154-abc' }, async ({ page }) => {
|
|
679
|
+
// ✅ Will be submitted to Appliqation
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
test('Orphan Test 1', async ({ page }) => {
|
|
683
|
+
// ⚠️ Logged as warning, not submitted
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test('Valid Test 2', { tag: '@uuid:1155-def' }, async ({ page }) => {
|
|
687
|
+
// ✅ Will be submitted to Appliqation
|
|
688
|
+
});
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
**Result:**
|
|
692
|
+
- ✅ Run is kept (because 2 tests have UUIDs)
|
|
693
|
+
- ✅ 2 valid results submitted to Appliqation
|
|
694
|
+
- ⚠️ 1 orphan logged in console and summary file
|
|
695
|
+
- ✅ CI/CD passes (because at least some tests were valid)
|
|
696
|
+
- ✅ Analytics remain accurate (only valid tests counted)
|
|
697
|
+
|
|
698
|
+
### Troubleshooting FAQ
|
|
699
|
+
|
|
700
|
+
**Q: Why does my run get deleted?**
|
|
701
|
+
|
|
702
|
+
Your run is deleted only when **100% of your tests lack UUID annotations**. This prevents corrupted analytics. Add UUIDs to at least one test to keep the run.
|
|
703
|
+
|
|
704
|
+
**Q: How do I disable automatic deletion?**
|
|
705
|
+
|
|
706
|
+
Set `APPLIQATION_DELETE_ORPHAN_RUNS=false` in your `.env` file. However, this is not recommended as it will corrupt your analytics with N/A pass rates.
|
|
707
|
+
|
|
708
|
+
**Q: Can I keep orphan-only runs but still fail CI/CD?**
|
|
709
|
+
|
|
710
|
+
Yes! Set:
|
|
711
|
+
```env
|
|
712
|
+
APPLIQATION_DELETE_ORPHAN_RUNS=false
|
|
713
|
+
APPLIQATION_FAIL_ON_ORPHAN_RUNS=true
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
This will create the run in Appliqation but still fail your pipeline, forcing developers to fix UUIDs.
|
|
717
|
+
|
|
718
|
+
**Q: Where do I find UUIDs for my tests?**
|
|
719
|
+
|
|
720
|
+
1. Log into Appliqation portal
|
|
721
|
+
2. Navigate to your project
|
|
722
|
+
3. Go to "Test Cases" tab
|
|
723
|
+
4. Find your test case
|
|
724
|
+
5. The UUID is in the format: `{test_nid}-{uuid}` (e.g., `1154-c1f9559c-b978-43cc-9c76-fd539c717cb4`)
|
|
725
|
+
|
|
726
|
+
**Q: Does orphan cleanup affect test execution?**
|
|
727
|
+
|
|
728
|
+
No! Cleanup happens **after** all tests complete in the `onEnd()` hook. Test execution is never blocked or interrupted.
|
|
729
|
+
|
|
730
|
+
**Q: What if deletion fails?**
|
|
731
|
+
|
|
732
|
+
Deletion is fire-and-forget with error handling. If deletion fails (network issue, permission, etc.), it's logged as an error but doesn't crash your test run. The corrupted run may remain in Appliqation in this rare case.
|
|
471
733
|
|
|
472
734
|
---
|
|
473
735
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appliqation/automation-sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Appliqation Automation SDK with API key authentication, custom run titles, and framework-specific reporters",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
},
|
|
115
115
|
"repository": {
|
|
116
116
|
"type": "git",
|
|
117
|
-
"url": "https://github.com/appliqation/automation-sdk-js"
|
|
117
|
+
"url": "git+https://github.com/appliqation/automation-sdk-js.git"
|
|
118
118
|
},
|
|
119
119
|
"bugs": {
|
|
120
120
|
"url": "https://github.com/appliqation/automation-sdk-js/issues"
|
package/src/AppliqationClient.js
CHANGED
|
@@ -2,6 +2,7 @@ const HttpClient = require('./core/HttpClient');
|
|
|
2
2
|
const AuthManager = require('./core/AuthManager');
|
|
3
3
|
const RunMatrixService = require('./services/RunMatrixService');
|
|
4
4
|
const ResultService = require('./services/ResultService');
|
|
5
|
+
const TaggingService = require('./services/TaggingService');
|
|
5
6
|
const OrphanTestService = require('./services/OrphanTestService');
|
|
6
7
|
const UuidValidator = require('./utils/UuidValidator');
|
|
7
8
|
const PayloadBuilder = require('./utils/PayloadBuilder');
|
|
@@ -99,7 +100,15 @@ class AppliqationClient {
|
|
|
99
100
|
timeout: normalizedConfig.options?.timeout || 30000,
|
|
100
101
|
retries: normalizedConfig.options?.retries || 3,
|
|
101
102
|
logOrphans: normalizedConfig.options?.logOrphans !== false,
|
|
102
|
-
logLevel: normalizedConfig.options?.logLevel || 'info'
|
|
103
|
+
logLevel: normalizedConfig.options?.logLevel || 'info',
|
|
104
|
+
// Auto-tagging configuration
|
|
105
|
+
autoTag: normalizedConfig.options?.autoTag !== false,
|
|
106
|
+
autoTagName: normalizedConfig.options?.autoTagName ||
|
|
107
|
+
normalizedConfig.autoTagName ||
|
|
108
|
+
process.env.APPLIQATION_AUTO_TAG_NAME ||
|
|
109
|
+
'Appq_automated',
|
|
110
|
+
autoTagBatchSize: normalizedConfig.options?.autoTagBatchSize || 50,
|
|
111
|
+
autoTagRetries: normalizedConfig.options?.autoTagRetries || 2
|
|
103
112
|
}
|
|
104
113
|
};
|
|
105
114
|
|
|
@@ -112,9 +121,16 @@ class AppliqationClient {
|
|
|
112
121
|
|
|
113
122
|
// Initialize services
|
|
114
123
|
this.runMatrix = new RunMatrixService(this.http, this.config);
|
|
115
|
-
this.
|
|
124
|
+
this.tagging = new TaggingService(this.http, this.config);
|
|
125
|
+
this.results = new ResultService(this.http, this.tagging, this.config);
|
|
116
126
|
this.orphans = new OrphanTestService(this.http);
|
|
117
127
|
|
|
128
|
+
// Log tagging configuration
|
|
129
|
+
logger.debug('TaggingService initialized', {
|
|
130
|
+
enabled: this.tagging.isEnabled(),
|
|
131
|
+
tagName: this.tagging.tagName
|
|
132
|
+
});
|
|
133
|
+
|
|
118
134
|
// Track current run context
|
|
119
135
|
this.currentRun = null;
|
|
120
136
|
|
|
@@ -405,6 +421,35 @@ class AppliqationClient {
|
|
|
405
421
|
this.currentRun = run;
|
|
406
422
|
}
|
|
407
423
|
|
|
424
|
+
/**
|
|
425
|
+
* Delete a test run
|
|
426
|
+
* @param {string} runId - Run ID to delete
|
|
427
|
+
* @param {string} reason - Deletion reason (for audit logging)
|
|
428
|
+
* @returns {Promise<Object>} Deletion result
|
|
429
|
+
*/
|
|
430
|
+
async deleteRun(runId, reason = 'orphan_cleanup') {
|
|
431
|
+
try {
|
|
432
|
+
logger.info('Deleting run...', { runId, reason });
|
|
433
|
+
|
|
434
|
+
const result = await this.runMatrix.delete(runId, reason);
|
|
435
|
+
|
|
436
|
+
// Clear from current run if it matches
|
|
437
|
+
if (this.currentRun && this.currentRun.runId === runId) {
|
|
438
|
+
this.currentRun = null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
logger.info('Run deleted successfully', { runId });
|
|
442
|
+
|
|
443
|
+
return result;
|
|
444
|
+
} catch (error) {
|
|
445
|
+
logger.error('Failed to delete run', {
|
|
446
|
+
error: error.message,
|
|
447
|
+
runId
|
|
448
|
+
});
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
408
453
|
/**
|
|
409
454
|
* Validate UUID format
|
|
410
455
|
* @param {string} uuid - UUID to validate
|
package/src/core/HttpClient.js
CHANGED
|
@@ -254,6 +254,58 @@ class HttpClient {
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Delete a test run
|
|
259
|
+
* @param {string} runId - Run ID to delete
|
|
260
|
+
* @param {string} reason - Reason for deletion (for audit logging)
|
|
261
|
+
* @returns {Promise<Object>} Deletion result
|
|
262
|
+
*/
|
|
263
|
+
async deleteRun(runId, reason = 'sdk_cleanup') {
|
|
264
|
+
const maxRetries = 3;
|
|
265
|
+
const retryDelays = [500, 1000, 2000]; // ms - exponential backoff
|
|
266
|
+
|
|
267
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
268
|
+
try {
|
|
269
|
+
logger.debug('Deleting run', { runId, reason, attempt: attempt + 1 });
|
|
270
|
+
|
|
271
|
+
const response = await this.delete(`/api/automation/run/${runId}/delete`, {
|
|
272
|
+
data: { reason }
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
if (attempt > 0) {
|
|
276
|
+
logger.info('Run deletion succeeded after retry', { runId, attempt: attempt + 1 });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return response;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
const is404 = error.response?.status === 404;
|
|
282
|
+
const isLastAttempt = attempt === maxRetries;
|
|
283
|
+
|
|
284
|
+
// Only retry on 404 (run not found yet - possible race condition)
|
|
285
|
+
if (is404 && !isLastAttempt) {
|
|
286
|
+
const delay = retryDelays[attempt];
|
|
287
|
+
logger.warn('Run not found, retrying deletion...', {
|
|
288
|
+
runId,
|
|
289
|
+
attempt: attempt + 1,
|
|
290
|
+
maxRetries: maxRetries + 1,
|
|
291
|
+
retryAfterMs: delay
|
|
292
|
+
});
|
|
293
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Final attempt failed or non-404 error - throw
|
|
298
|
+
logger.error('Failed to delete run', {
|
|
299
|
+
error: error.message,
|
|
300
|
+
runId,
|
|
301
|
+
status: error.response?.status,
|
|
302
|
+
attempts: attempt + 1
|
|
303
|
+
});
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
257
309
|
/**
|
|
258
310
|
* Set authorization header
|
|
259
311
|
* @param {string} token - Authorization token
|