codeceptjs 3.6.10-beta.1 → 3.7.0-beta.1
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 +81 -110
- package/bin/codecept.js +2 -2
- package/docs/webapi/clearCookie.mustache +1 -1
- package/lib/actor.js +46 -36
- package/lib/assert/empty.js +3 -5
- package/lib/assert/equal.js +4 -7
- package/lib/assert/include.js +4 -6
- package/lib/assert/throws.js +2 -4
- package/lib/assert/truth.js +2 -2
- package/lib/codecept.js +87 -83
- package/lib/command/configMigrate.js +2 -4
- package/lib/command/definitions.js +5 -25
- package/lib/command/generate.js +10 -14
- package/lib/command/gherkin/snippets.js +10 -8
- package/lib/command/gherkin/steps.js +1 -1
- package/lib/command/info.js +1 -3
- package/lib/command/init.js +8 -12
- package/lib/command/interactive.js +1 -1
- package/lib/command/list.js +1 -1
- package/lib/command/run-multiple.js +12 -35
- package/lib/command/run-workers.js +10 -10
- package/lib/command/utils.js +5 -6
- package/lib/command/workers/runTests.js +14 -17
- package/lib/container.js +327 -237
- package/lib/data/context.js +10 -13
- package/lib/data/dataScenarioConfig.js +8 -8
- package/lib/data/dataTableArgument.js +6 -6
- package/lib/data/table.js +5 -11
- package/lib/els.js +177 -0
- package/lib/event.js +1 -0
- package/lib/heal.js +78 -80
- package/lib/helper/ApiDataFactory.js +3 -6
- package/lib/helper/Appium.js +15 -30
- package/lib/helper/FileSystem.js +3 -3
- package/lib/helper/GraphQLDataFactory.js +3 -3
- package/lib/helper/JSONResponse.js +57 -37
- package/lib/helper/Nightmare.js +35 -53
- package/lib/helper/Playwright.js +189 -251
- package/lib/helper/Protractor.js +54 -77
- package/lib/helper/Puppeteer.js +134 -232
- package/lib/helper/REST.js +5 -17
- package/lib/helper/TestCafe.js +21 -44
- package/lib/helper/WebDriver.js +103 -162
- package/lib/helper/testcafe/testcafe-utils.js +26 -27
- package/lib/listener/artifacts.js +2 -2
- package/lib/listener/emptyRun.js +58 -0
- package/lib/listener/exit.js +4 -4
- package/lib/listener/{retry.js → globalRetry.js} +5 -5
- package/lib/listener/{timeout.js → globalTimeout.js} +8 -8
- package/lib/listener/helpers.js +15 -15
- package/lib/listener/mocha.js +1 -1
- package/lib/listener/steps.js +17 -12
- package/lib/listener/store.js +12 -0
- package/lib/mocha/asyncWrapper.js +204 -0
- package/lib/{interfaces → mocha}/bdd.js +3 -3
- package/lib/mocha/cli.js +257 -0
- package/lib/mocha/factory.js +104 -0
- package/lib/{interfaces → mocha}/featureConfig.js +11 -12
- package/lib/{interfaces → mocha}/gherkin.js +26 -28
- package/lib/mocha/hooks.js +83 -0
- package/lib/mocha/index.js +12 -0
- package/lib/mocha/inject.js +24 -0
- package/lib/{interfaces → mocha}/scenarioConfig.js +10 -6
- package/lib/mocha/suite.js +55 -0
- package/lib/mocha/test.js +60 -0
- package/lib/mocha/types.d.ts +31 -0
- package/lib/mocha/ui.js +219 -0
- package/lib/output.js +28 -10
- package/lib/pause.js +159 -135
- package/lib/plugin/autoDelay.js +4 -4
- package/lib/plugin/autoLogin.js +6 -7
- package/lib/plugin/commentStep.js +1 -1
- package/lib/plugin/coverage.js +10 -19
- package/lib/plugin/customLocator.js +3 -3
- package/lib/plugin/debugErrors.js +2 -2
- package/lib/plugin/eachElement.js +1 -1
- package/lib/plugin/fakerTransform.js +1 -1
- package/lib/plugin/heal.js +6 -9
- package/lib/plugin/retryFailedStep.js +4 -4
- package/lib/plugin/retryTo.js +2 -2
- package/lib/plugin/screenshotOnFail.js +9 -36
- package/lib/plugin/selenoid.js +15 -35
- package/lib/plugin/stepByStepReport.js +51 -13
- package/lib/plugin/stepTimeout.js +4 -11
- package/lib/plugin/subtitles.js +4 -4
- package/lib/plugin/tryTo.js +1 -1
- package/lib/plugin/wdio.js +8 -10
- package/lib/recorder.js +142 -121
- package/lib/secret.js +1 -1
- package/lib/step.js +160 -144
- package/lib/store.js +6 -2
- package/lib/template/heal.js +2 -11
- package/lib/utils.js +224 -216
- package/lib/within.js +73 -55
- package/lib/workers.js +265 -261
- package/package.json +45 -46
- package/typings/index.d.ts +172 -184
- package/typings/promiseBasedTypes.d.ts +53 -516
- package/typings/types.d.ts +127 -587
- package/lib/cli.js +0 -256
- package/lib/helper/ExpectHelper.js +0 -391
- package/lib/helper/SoftExpectHelper.js +0 -381
- package/lib/mochaFactory.js +0 -113
- package/lib/scenario.js +0 -224
- package/lib/ui.js +0 -236
package/lib/helper/Playwright.js
CHANGED
|
@@ -7,6 +7,7 @@ const assert = require('assert')
|
|
|
7
7
|
const promiseRetry = require('promise-retry')
|
|
8
8
|
const Locator = require('../locator')
|
|
9
9
|
const recorder = require('../recorder')
|
|
10
|
+
const store = require('../store')
|
|
10
11
|
const stringIncludes = require('../assert/include').includes
|
|
11
12
|
const { urlEquals } = require('../assert/equal')
|
|
12
13
|
const { equals } = require('../assert/equal')
|
|
@@ -24,6 +25,7 @@ const {
|
|
|
24
25
|
clearString,
|
|
25
26
|
requireWithFallback,
|
|
26
27
|
normalizeSpacesInString,
|
|
28
|
+
relativeDir,
|
|
27
29
|
} = require('../utils')
|
|
28
30
|
const { isColorProperty, convertColorToRGBA } = require('../colorUtils')
|
|
29
31
|
const ElementNotFound = require('./errors/ElementNotFound')
|
|
@@ -40,26 +42,10 @@ const popupStore = new Popup()
|
|
|
40
42
|
const consoleLogStore = new Console()
|
|
41
43
|
const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron']
|
|
42
44
|
|
|
43
|
-
const {
|
|
44
|
-
setRestartStrategy,
|
|
45
|
-
restartsSession,
|
|
46
|
-
restartsContext,
|
|
47
|
-
restartsBrowser,
|
|
48
|
-
} = require('./extras/PlaywrightRestartOpts')
|
|
45
|
+
const { setRestartStrategy, restartsSession, restartsContext, restartsBrowser } = require('./extras/PlaywrightRestartOpts')
|
|
49
46
|
const { createValueEngine, createDisabledEngine } = require('./extras/PlaywrightPropEngine')
|
|
50
|
-
const {
|
|
51
|
-
|
|
52
|
-
dontSeeElementError,
|
|
53
|
-
dontSeeElementInDOMError,
|
|
54
|
-
seeElementInDOMError,
|
|
55
|
-
} = require('./errors/ElementAssertion')
|
|
56
|
-
const {
|
|
57
|
-
dontSeeTraffic,
|
|
58
|
-
seeTraffic,
|
|
59
|
-
grabRecordedNetworkTraffics,
|
|
60
|
-
stopRecordingTraffic,
|
|
61
|
-
flushNetworkTraffics,
|
|
62
|
-
} = require('./network/actions')
|
|
47
|
+
const { seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError } = require('./errors/ElementAssertion')
|
|
48
|
+
const { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } = require('./network/actions')
|
|
63
49
|
|
|
64
50
|
const pathSeparator = path.sep
|
|
65
51
|
|
|
@@ -392,9 +378,7 @@ class Playwright extends Helper {
|
|
|
392
378
|
config = Object.assign(defaults, config)
|
|
393
379
|
|
|
394
380
|
if (availableBrowsers.indexOf(config.browser) < 0) {
|
|
395
|
-
throw new Error(
|
|
396
|
-
`Invalid config. Can't use browser "${config.browser}". Accepted values: ${availableBrowsers.join(', ')}`,
|
|
397
|
-
)
|
|
381
|
+
throw new Error(`Invalid config. Can't use browser "${config.browser}". Accepted values: ${availableBrowsers.join(', ')}`)
|
|
398
382
|
}
|
|
399
383
|
|
|
400
384
|
return config
|
|
@@ -440,9 +424,7 @@ class Playwright extends Helper {
|
|
|
440
424
|
}
|
|
441
425
|
this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint
|
|
442
426
|
this.isElectron = this.options.browser === 'electron'
|
|
443
|
-
this.userDataDir = this.playwrightOptions.userDataDir
|
|
444
|
-
? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}`
|
|
445
|
-
: undefined
|
|
427
|
+
this.userDataDir = this.playwrightOptions.userDataDir ? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}` : undefined
|
|
446
428
|
this.isCDPConnection = this.playwrightOptions.cdpConnection
|
|
447
429
|
popupStore.defaultAction = this.options.defaultPopupAction
|
|
448
430
|
}
|
|
@@ -458,14 +440,14 @@ class Playwright extends Helper {
|
|
|
458
440
|
name: 'url',
|
|
459
441
|
message: 'Base url of site to be tested',
|
|
460
442
|
default: 'http://localhost',
|
|
461
|
-
when:
|
|
443
|
+
when: answers => answers.Playwright_browser !== 'electron',
|
|
462
444
|
},
|
|
463
445
|
{
|
|
464
446
|
name: 'show',
|
|
465
447
|
message: 'Show browser window',
|
|
466
448
|
default: true,
|
|
467
449
|
type: 'confirm',
|
|
468
|
-
when:
|
|
450
|
+
when: answers => answers.Playwright_browser !== 'electron',
|
|
469
451
|
},
|
|
470
452
|
]
|
|
471
453
|
}
|
|
@@ -502,7 +484,7 @@ class Playwright extends Helper {
|
|
|
502
484
|
this.currentRunningTest = test
|
|
503
485
|
recorder.retry({
|
|
504
486
|
retries: process.env.FAILED_STEP_RETRIES || 3,
|
|
505
|
-
when:
|
|
487
|
+
when: err => {
|
|
506
488
|
if (!err || typeof err.message !== 'string') {
|
|
507
489
|
return false
|
|
508
490
|
}
|
|
@@ -559,10 +541,7 @@ class Playwright extends Helper {
|
|
|
559
541
|
mainPage = existingPages[0] || (await this.browserContext.newPage())
|
|
560
542
|
} catch (e) {
|
|
561
543
|
if (this.playwrightOptions.userDataDir) {
|
|
562
|
-
this.browser = await playwright[this.options.browser].launchPersistentContext(
|
|
563
|
-
this.userDataDir,
|
|
564
|
-
this.playwrightOptions,
|
|
565
|
-
)
|
|
544
|
+
this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions)
|
|
566
545
|
this.browserContext = this.browser
|
|
567
546
|
const existingPages = await this.browserContext.pages()
|
|
568
547
|
mainPage = existingPages[0]
|
|
@@ -583,7 +562,7 @@ class Playwright extends Helper {
|
|
|
583
562
|
|
|
584
563
|
if (this.isElectron) {
|
|
585
564
|
this.browser.close()
|
|
586
|
-
this.electronSessions.forEach(
|
|
565
|
+
this.electronSessions.forEach(session => session.close())
|
|
587
566
|
return
|
|
588
567
|
}
|
|
589
568
|
|
|
@@ -605,7 +584,7 @@ class Playwright extends Helper {
|
|
|
605
584
|
this.storageState = await currentContext.storageState()
|
|
606
585
|
}
|
|
607
586
|
|
|
608
|
-
await Promise.all(contexts.map(
|
|
587
|
+
await Promise.all(contexts.map(c => c.close()))
|
|
609
588
|
}
|
|
610
589
|
} catch (e) {
|
|
611
590
|
console.log(e)
|
|
@@ -641,10 +620,7 @@ class Playwright extends Helper {
|
|
|
641
620
|
page = await browserContext.newPage()
|
|
642
621
|
} catch (e) {
|
|
643
622
|
if (this.playwrightOptions.userDataDir) {
|
|
644
|
-
browserContext = await playwright[this.options.browser].launchPersistentContext(
|
|
645
|
-
`${this.userDataDir}_${this.activeSessionName}`,
|
|
646
|
-
this.playwrightOptions,
|
|
647
|
-
)
|
|
623
|
+
browserContext = await playwright[this.options.browser].launchPersistentContext(`${this.userDataDir}_${this.activeSessionName}`, this.playwrightOptions)
|
|
648
624
|
this.browser = browserContext
|
|
649
625
|
page = await browserContext.pages()[0]
|
|
650
626
|
}
|
|
@@ -660,7 +636,7 @@ class Playwright extends Helper {
|
|
|
660
636
|
stop: async () => {
|
|
661
637
|
// is closed by _after
|
|
662
638
|
},
|
|
663
|
-
loadVars: async
|
|
639
|
+
loadVars: async context => {
|
|
664
640
|
if (context) {
|
|
665
641
|
this.browserContext = context
|
|
666
642
|
const existingPages = await context.pages()
|
|
@@ -668,7 +644,7 @@ class Playwright extends Helper {
|
|
|
668
644
|
return this._setPage(this.sessionPages[this.activeSessionName])
|
|
669
645
|
}
|
|
670
646
|
},
|
|
671
|
-
restoreVars: async
|
|
647
|
+
restoreVars: async session => {
|
|
672
648
|
this.withinLocator = null
|
|
673
649
|
this.browserContext = defaultContext
|
|
674
650
|
|
|
@@ -793,7 +769,7 @@ class Playwright extends Helper {
|
|
|
793
769
|
return
|
|
794
770
|
}
|
|
795
771
|
page.removeAllListeners('dialog')
|
|
796
|
-
page.on('dialog', async
|
|
772
|
+
page.on('dialog', async dialog => {
|
|
797
773
|
popupStore.popup = dialog
|
|
798
774
|
const action = popupStore.actionType || this.options.defaultPopupAction
|
|
799
775
|
await this._waitForAction()
|
|
@@ -856,16 +832,13 @@ class Playwright extends Helper {
|
|
|
856
832
|
throw err
|
|
857
833
|
}
|
|
858
834
|
} else if (this.playwrightOptions.userDataDir) {
|
|
859
|
-
this.browser = await playwright[this.options.browser].launchPersistentContext(
|
|
860
|
-
this.userDataDir,
|
|
861
|
-
this.playwrightOptions,
|
|
862
|
-
)
|
|
835
|
+
this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions)
|
|
863
836
|
} else {
|
|
864
837
|
this.browser = await playwright[this.options.browser].launch(this.playwrightOptions)
|
|
865
838
|
}
|
|
866
839
|
|
|
867
840
|
// works only for Chromium
|
|
868
|
-
this.browser.on('targetchanged',
|
|
841
|
+
this.browser.on('targetchanged', target => {
|
|
869
842
|
this.debugSection('Url', target.url())
|
|
870
843
|
})
|
|
871
844
|
|
|
@@ -940,7 +913,7 @@ class Playwright extends Helper {
|
|
|
940
913
|
const navigationStart = timing.navigationStart
|
|
941
914
|
|
|
942
915
|
const extractedData = {}
|
|
943
|
-
dataNames.forEach(
|
|
916
|
+
dataNames.forEach(name => {
|
|
944
917
|
extractedData[name] = timing[name] - navigationStart
|
|
945
918
|
})
|
|
946
919
|
|
|
@@ -969,13 +942,7 @@ class Playwright extends Helper {
|
|
|
969
942
|
|
|
970
943
|
const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)))
|
|
971
944
|
|
|
972
|
-
perfTiming = this._extractDataFromPerformanceTiming(
|
|
973
|
-
performanceTiming,
|
|
974
|
-
'responseEnd',
|
|
975
|
-
'domInteractive',
|
|
976
|
-
'domContentLoadedEventEnd',
|
|
977
|
-
'loadEventEnd',
|
|
978
|
-
)
|
|
945
|
+
perfTiming = this._extractDataFromPerformanceTiming(performanceTiming, 'responseEnd', 'domInteractive', 'domContentLoadedEventEnd', 'loadEventEnd')
|
|
979
946
|
|
|
980
947
|
return this._waitForAction()
|
|
981
948
|
}
|
|
@@ -1197,10 +1164,7 @@ class Playwright extends Helper {
|
|
|
1197
1164
|
return this.executeScript(() => {
|
|
1198
1165
|
const body = document.body
|
|
1199
1166
|
const html = document.documentElement
|
|
1200
|
-
window.scrollTo(
|
|
1201
|
-
0,
|
|
1202
|
-
Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight),
|
|
1203
|
-
)
|
|
1167
|
+
window.scrollTo(0, Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight))
|
|
1204
1168
|
})
|
|
1205
1169
|
}
|
|
1206
1170
|
|
|
@@ -1287,7 +1251,22 @@ class Playwright extends Helper {
|
|
|
1287
1251
|
|
|
1288
1252
|
if (this.frame) return findElements(this.frame, locator)
|
|
1289
1253
|
|
|
1290
|
-
|
|
1254
|
+
const els = await findElements(context, locator)
|
|
1255
|
+
|
|
1256
|
+
if (store.debugMode) {
|
|
1257
|
+
const previewElements = els.slice(0, 3)
|
|
1258
|
+
let htmls = await Promise.all(previewElements.map(el => elToString(el, previewElements.length)))
|
|
1259
|
+
if (els.length > 3) htmls.push('...')
|
|
1260
|
+
if (els.length > 1) {
|
|
1261
|
+
this.debugSection(`Elements (${els.length})`, htmls.join('|').trim())
|
|
1262
|
+
} else if (els.length === 1) {
|
|
1263
|
+
this.debugSection('Element', htmls.join('|').trim())
|
|
1264
|
+
} else {
|
|
1265
|
+
this.debug(`No elements found by ${JSON.stringify(locator).slice(0, 50)}....`)
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
return els
|
|
1291
1270
|
}
|
|
1292
1271
|
|
|
1293
1272
|
/**
|
|
@@ -1437,10 +1416,10 @@ class Playwright extends Helper {
|
|
|
1437
1416
|
*/
|
|
1438
1417
|
async closeOtherTabs() {
|
|
1439
1418
|
const pages = await this.browserContext.pages()
|
|
1440
|
-
const otherPages = pages.filter(
|
|
1419
|
+
const otherPages = pages.filter(page => page !== this.page)
|
|
1441
1420
|
if (otherPages.length) {
|
|
1442
1421
|
this.debug(`Closing ${otherPages.length} tabs`)
|
|
1443
|
-
return Promise.all(otherPages.map(
|
|
1422
|
+
return Promise.all(otherPages.map(p => p.close()))
|
|
1444
1423
|
}
|
|
1445
1424
|
return Promise.resolve()
|
|
1446
1425
|
}
|
|
@@ -1483,9 +1462,9 @@ class Playwright extends Helper {
|
|
|
1483
1462
|
*/
|
|
1484
1463
|
async seeElement(locator) {
|
|
1485
1464
|
let els = await this._locate(locator)
|
|
1486
|
-
els = await Promise.all(els.map(
|
|
1465
|
+
els = await Promise.all(els.map(el => el.isVisible()))
|
|
1487
1466
|
try {
|
|
1488
|
-
return empty('visible elements').negate(els.filter(
|
|
1467
|
+
return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
|
|
1489
1468
|
} catch (e) {
|
|
1490
1469
|
dontSeeElementError(locator)
|
|
1491
1470
|
}
|
|
@@ -1497,9 +1476,9 @@ class Playwright extends Helper {
|
|
|
1497
1476
|
*/
|
|
1498
1477
|
async dontSeeElement(locator) {
|
|
1499
1478
|
let els = await this._locate(locator)
|
|
1500
|
-
els = await Promise.all(els.map(
|
|
1479
|
+
els = await Promise.all(els.map(el => el.isVisible()))
|
|
1501
1480
|
try {
|
|
1502
|
-
return empty('visible elements').assert(els.filter(
|
|
1481
|
+
return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
|
|
1503
1482
|
} catch (e) {
|
|
1504
1483
|
seeElementError(locator)
|
|
1505
1484
|
}
|
|
@@ -1511,7 +1490,7 @@ class Playwright extends Helper {
|
|
|
1511
1490
|
async seeElementInDOM(locator) {
|
|
1512
1491
|
const els = await this._locate(locator)
|
|
1513
1492
|
try {
|
|
1514
|
-
return empty('elements on page').negate(els.filter(
|
|
1493
|
+
return empty('elements on page').negate(els.filter(v => v).fill('ELEMENT'))
|
|
1515
1494
|
} catch (e) {
|
|
1516
1495
|
dontSeeElementInDOMError(locator)
|
|
1517
1496
|
}
|
|
@@ -1523,7 +1502,7 @@ class Playwright extends Helper {
|
|
|
1523
1502
|
async dontSeeElementInDOM(locator) {
|
|
1524
1503
|
const els = await this._locate(locator)
|
|
1525
1504
|
try {
|
|
1526
|
-
return empty('elements on a page').assert(els.filter(
|
|
1505
|
+
return empty('elements on a page').assert(els.filter(v => v).fill('ELEMENT'))
|
|
1527
1506
|
} catch (e) {
|
|
1528
1507
|
seeElementInDOMError(locator)
|
|
1529
1508
|
}
|
|
@@ -1547,7 +1526,7 @@ class Playwright extends Helper {
|
|
|
1547
1526
|
* @return {Promise<void>}
|
|
1548
1527
|
*/
|
|
1549
1528
|
async handleDownloads(fileName) {
|
|
1550
|
-
this.page.waitForEvent('download').then(async
|
|
1529
|
+
this.page.waitForEvent('download').then(async download => {
|
|
1551
1530
|
const filePath = await download.path()
|
|
1552
1531
|
fileName = fileName || `downloads/${path.basename(filePath)}`
|
|
1553
1532
|
|
|
@@ -1741,6 +1720,7 @@ class Playwright extends Helper {
|
|
|
1741
1720
|
const el = els[0]
|
|
1742
1721
|
|
|
1743
1722
|
await el.clear()
|
|
1723
|
+
if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
|
|
1744
1724
|
|
|
1745
1725
|
await highlightActiveElement.call(this, el)
|
|
1746
1726
|
|
|
@@ -1852,8 +1832,8 @@ class Playwright extends Helper {
|
|
|
1852
1832
|
*/
|
|
1853
1833
|
async grabNumberOfVisibleElements(locator) {
|
|
1854
1834
|
let els = await this._locate(locator)
|
|
1855
|
-
els = await Promise.all(els.map(
|
|
1856
|
-
return els.filter(
|
|
1835
|
+
els = await Promise.all(els.map(el => el.isVisible()))
|
|
1836
|
+
return els.filter(v => v).length
|
|
1857
1837
|
}
|
|
1858
1838
|
|
|
1859
1839
|
/**
|
|
@@ -1963,9 +1943,7 @@ class Playwright extends Helper {
|
|
|
1963
1943
|
*/
|
|
1964
1944
|
async seeNumberOfElements(locator, num) {
|
|
1965
1945
|
const elements = await this._locate(locator)
|
|
1966
|
-
return equals(
|
|
1967
|
-
`expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`,
|
|
1968
|
-
).assert(elements.length, num)
|
|
1946
|
+
return equals(`expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`).assert(elements.length, num)
|
|
1969
1947
|
}
|
|
1970
1948
|
|
|
1971
1949
|
/**
|
|
@@ -1975,10 +1953,7 @@ class Playwright extends Helper {
|
|
|
1975
1953
|
*/
|
|
1976
1954
|
async seeNumberOfVisibleElements(locator, num) {
|
|
1977
1955
|
const res = await this.grabNumberOfVisibleElements(locator)
|
|
1978
|
-
return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(
|
|
1979
|
-
res,
|
|
1980
|
-
num,
|
|
1981
|
-
)
|
|
1956
|
+
return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(res, num)
|
|
1982
1957
|
}
|
|
1983
1958
|
|
|
1984
1959
|
/**
|
|
@@ -1997,7 +1972,7 @@ class Playwright extends Helper {
|
|
|
1997
1972
|
*/
|
|
1998
1973
|
async seeCookie(name) {
|
|
1999
1974
|
const cookies = await this.browserContext.cookies()
|
|
2000
|
-
empty(`cookie ${name} to be set`).negate(cookies.filter(
|
|
1975
|
+
empty(`cookie ${name} to be set`).negate(cookies.filter(c => c.name === name))
|
|
2001
1976
|
}
|
|
2002
1977
|
|
|
2003
1978
|
/**
|
|
@@ -2005,7 +1980,7 @@ class Playwright extends Helper {
|
|
|
2005
1980
|
*/
|
|
2006
1981
|
async dontSeeCookie(name) {
|
|
2007
1982
|
const cookies = await this.browserContext.cookies()
|
|
2008
|
-
empty(`cookie ${name} not to be set`).assert(cookies.filter(
|
|
1983
|
+
empty(`cookie ${name} not to be set`).assert(cookies.filter(c => c.name === name))
|
|
2009
1984
|
}
|
|
2010
1985
|
|
|
2011
1986
|
/**
|
|
@@ -2016,17 +1991,18 @@ class Playwright extends Helper {
|
|
|
2016
1991
|
async grabCookie(name) {
|
|
2017
1992
|
const cookies = await this.browserContext.cookies()
|
|
2018
1993
|
if (!name) return cookies
|
|
2019
|
-
const cookie = cookies.filter(
|
|
1994
|
+
const cookie = cookies.filter(c => c.name === name)
|
|
2020
1995
|
if (cookie[0]) return cookie[0]
|
|
2021
1996
|
}
|
|
2022
1997
|
|
|
2023
1998
|
/**
|
|
2024
1999
|
* {{> clearCookie }}
|
|
2025
2000
|
*/
|
|
2026
|
-
async clearCookie() {
|
|
2027
|
-
// Playwright currently doesn't support to delete a certain cookie
|
|
2028
|
-
// https://github.com/microsoft/playwright/blob/main/docs/src/api/class-browsercontext.md#async-method-browsercontextclearcookies
|
|
2001
|
+
async clearCookie(cookieName) {
|
|
2029
2002
|
if (!this.browserContext) return
|
|
2003
|
+
if (cookieName) {
|
|
2004
|
+
return this.browserContext.clearCookies({ name: cookieName })
|
|
2005
|
+
}
|
|
2030
2006
|
return this.browserContext.clearCookies()
|
|
2031
2007
|
}
|
|
2032
2008
|
|
|
@@ -2098,7 +2074,7 @@ class Playwright extends Helper {
|
|
|
2098
2074
|
const els = await this._locate(locator)
|
|
2099
2075
|
const texts = []
|
|
2100
2076
|
for (const el of els) {
|
|
2101
|
-
texts.push(await
|
|
2077
|
+
texts.push(await el.innerText())
|
|
2102
2078
|
}
|
|
2103
2079
|
this.debug(`Matched ${els.length} elements`)
|
|
2104
2080
|
return texts
|
|
@@ -2120,7 +2096,7 @@ class Playwright extends Helper {
|
|
|
2120
2096
|
async grabValueFromAll(locator) {
|
|
2121
2097
|
const els = await findFields.call(this, locator)
|
|
2122
2098
|
this.debug(`Matched ${els.length} elements`)
|
|
2123
|
-
return Promise.all(els.map(
|
|
2099
|
+
return Promise.all(els.map(el => el.inputValue()))
|
|
2124
2100
|
}
|
|
2125
2101
|
|
|
2126
2102
|
/**
|
|
@@ -2139,7 +2115,7 @@ class Playwright extends Helper {
|
|
|
2139
2115
|
async grabHTMLFromAll(locator) {
|
|
2140
2116
|
const els = await this._locate(locator)
|
|
2141
2117
|
this.debug(`Matched ${els.length} elements`)
|
|
2142
|
-
return Promise.all(els.map(
|
|
2118
|
+
return Promise.all(els.map(el => el.innerHTML()))
|
|
2143
2119
|
}
|
|
2144
2120
|
|
|
2145
2121
|
/**
|
|
@@ -2160,11 +2136,7 @@ class Playwright extends Helper {
|
|
|
2160
2136
|
async grabCssPropertyFromAll(locator, cssProperty) {
|
|
2161
2137
|
const els = await this._locate(locator)
|
|
2162
2138
|
this.debug(`Matched ${els.length} elements`)
|
|
2163
|
-
const cssValues = await Promise.all(
|
|
2164
|
-
els.map((el) =>
|
|
2165
|
-
el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty),
|
|
2166
|
-
),
|
|
2167
|
-
)
|
|
2139
|
+
const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)))
|
|
2168
2140
|
|
|
2169
2141
|
return cssValues
|
|
2170
2142
|
}
|
|
@@ -2192,19 +2164,16 @@ class Playwright extends Helper {
|
|
|
2192
2164
|
}
|
|
2193
2165
|
}
|
|
2194
2166
|
|
|
2195
|
-
const values = Object.keys(cssPropertiesCamelCase).map(
|
|
2167
|
+
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key])
|
|
2196
2168
|
if (!Array.isArray(props)) props = [props]
|
|
2197
2169
|
let chunked = chunkArray(props, values.length)
|
|
2198
|
-
chunked = chunked.filter(
|
|
2170
|
+
chunked = chunked.filter(val => {
|
|
2199
2171
|
for (let i = 0; i < val.length; ++i) {
|
|
2200
|
-
// eslint-disable-next-line eqeqeq
|
|
2201
2172
|
if (val[i] != values[i]) return false
|
|
2202
2173
|
}
|
|
2203
2174
|
return true
|
|
2204
2175
|
})
|
|
2205
|
-
return equals(
|
|
2206
|
-
`all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`,
|
|
2207
|
-
).assert(chunked.length, elemAmount)
|
|
2176
|
+
return equals(`all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`).assert(chunked.length, elemAmount)
|
|
2208
2177
|
}
|
|
2209
2178
|
|
|
2210
2179
|
/**
|
|
@@ -2217,16 +2186,16 @@ class Playwright extends Helper {
|
|
|
2217
2186
|
|
|
2218
2187
|
const elemAmount = res.length
|
|
2219
2188
|
const commands = []
|
|
2220
|
-
res.forEach(
|
|
2221
|
-
Object.keys(attributes).forEach(
|
|
2189
|
+
res.forEach(el => {
|
|
2190
|
+
Object.keys(attributes).forEach(prop => {
|
|
2222
2191
|
commands.push(el.evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop))
|
|
2223
2192
|
})
|
|
2224
2193
|
})
|
|
2225
2194
|
let attrs = await Promise.all(commands)
|
|
2226
|
-
const values = Object.keys(attributes).map(
|
|
2195
|
+
const values = Object.keys(attributes).map(key => attributes[key])
|
|
2227
2196
|
if (!Array.isArray(attrs)) attrs = [attrs]
|
|
2228
2197
|
let chunked = chunkArray(attrs, values.length)
|
|
2229
|
-
chunked = chunked.filter(
|
|
2198
|
+
chunked = chunked.filter(val => {
|
|
2230
2199
|
for (let i = 0; i < val.length; ++i) {
|
|
2231
2200
|
// the attribute could be a boolean
|
|
2232
2201
|
if (typeof val[i] === 'boolean') return val[i] === values[i]
|
|
@@ -2235,10 +2204,7 @@ class Playwright extends Helper {
|
|
|
2235
2204
|
}
|
|
2236
2205
|
return true
|
|
2237
2206
|
})
|
|
2238
|
-
return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(
|
|
2239
|
-
chunked.length,
|
|
2240
|
-
elemAmount,
|
|
2241
|
-
)
|
|
2207
|
+
return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(chunked.length, elemAmount)
|
|
2242
2208
|
}
|
|
2243
2209
|
|
|
2244
2210
|
/**
|
|
@@ -2311,7 +2277,7 @@ class Playwright extends Helper {
|
|
|
2311
2277
|
const fullPageOption = fullPage || this.options.fullPageScreenshots
|
|
2312
2278
|
let outputFile = screenshotOutputFolder(fileName)
|
|
2313
2279
|
|
|
2314
|
-
this.
|
|
2280
|
+
this.debugSection('Screenshot', relativeDir(outputFile))
|
|
2315
2281
|
|
|
2316
2282
|
await this.page.screenshot({
|
|
2317
2283
|
path: outputFile,
|
|
@@ -2324,7 +2290,7 @@ class Playwright extends Helper {
|
|
|
2324
2290
|
const activeSessionPage = this.sessionPages[sessionName]
|
|
2325
2291
|
outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`)
|
|
2326
2292
|
|
|
2327
|
-
this.
|
|
2293
|
+
this.debugSection('Screenshot', `${sessionName} - ${relativeDir(outputFile)}`)
|
|
2328
2294
|
|
|
2329
2295
|
if (activeSessionPage) {
|
|
2330
2296
|
await activeSessionPage.screenshot({
|
|
@@ -2358,9 +2324,7 @@ class Playwright extends Helper {
|
|
|
2358
2324
|
method = method.toLowerCase()
|
|
2359
2325
|
const allowedMethods = ['get', 'post', 'patch', 'head', 'fetch', 'delete']
|
|
2360
2326
|
if (!allowedMethods.includes(method)) {
|
|
2361
|
-
throw new Error(
|
|
2362
|
-
`Method ${method} is not allowed, use the one from a list ${allowedMethods} or switch to using REST helper`,
|
|
2363
|
-
)
|
|
2327
|
+
throw new Error(`Method ${method} is not allowed, use the one from a list ${allowedMethods} or switch to using REST helper`)
|
|
2364
2328
|
}
|
|
2365
2329
|
|
|
2366
2330
|
if (url.startsWith('/')) {
|
|
@@ -2397,10 +2361,7 @@ class Playwright extends Helper {
|
|
|
2397
2361
|
if (this.options.recordVideo && this.page && this.page.video()) {
|
|
2398
2362
|
test.artifacts.video = saveVideoForPage(this.page, `${test.title}.failed`)
|
|
2399
2363
|
for (const sessionName in this.sessionPages) {
|
|
2400
|
-
test.artifacts[`video_${sessionName}`] = saveVideoForPage(
|
|
2401
|
-
this.sessionPages[sessionName],
|
|
2402
|
-
`${test.title}_${sessionName}.failed`,
|
|
2403
|
-
)
|
|
2364
|
+
test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.failed`)
|
|
2404
2365
|
}
|
|
2405
2366
|
}
|
|
2406
2367
|
|
|
@@ -2408,10 +2369,7 @@ class Playwright extends Helper {
|
|
|
2408
2369
|
test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`)
|
|
2409
2370
|
for (const sessionName in this.sessionPages) {
|
|
2410
2371
|
if (!this.sessionPages[sessionName].context) continue
|
|
2411
|
-
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(
|
|
2412
|
-
this.sessionPages[sessionName].context,
|
|
2413
|
-
`${test.title}_${sessionName}.failed`,
|
|
2414
|
-
)
|
|
2372
|
+
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`)
|
|
2415
2373
|
}
|
|
2416
2374
|
}
|
|
2417
2375
|
|
|
@@ -2425,16 +2383,13 @@ class Playwright extends Helper {
|
|
|
2425
2383
|
if (this.options.keepVideoForPassedTests) {
|
|
2426
2384
|
test.artifacts.video = saveVideoForPage(this.page, `${test.title}.passed`)
|
|
2427
2385
|
for (const sessionName of Object.keys(this.sessionPages)) {
|
|
2428
|
-
test.artifacts[`video_${sessionName}`] = saveVideoForPage(
|
|
2429
|
-
this.sessionPages[sessionName],
|
|
2430
|
-
`${test.title}_${sessionName}.passed`,
|
|
2431
|
-
)
|
|
2386
|
+
test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.passed`)
|
|
2432
2387
|
}
|
|
2433
2388
|
} else {
|
|
2434
2389
|
this.page
|
|
2435
2390
|
.video()
|
|
2436
2391
|
.delete()
|
|
2437
|
-
.catch(
|
|
2392
|
+
.catch(e => {})
|
|
2438
2393
|
}
|
|
2439
2394
|
}
|
|
2440
2395
|
|
|
@@ -2444,10 +2399,7 @@ class Playwright extends Helper {
|
|
|
2444
2399
|
test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`)
|
|
2445
2400
|
for (const sessionName in this.sessionPages) {
|
|
2446
2401
|
if (!this.sessionPages[sessionName].context) continue
|
|
2447
|
-
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(
|
|
2448
|
-
this.sessionPages[sessionName].context,
|
|
2449
|
-
`${test.title}_${sessionName}.passed`,
|
|
2450
|
-
)
|
|
2402
|
+
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.passed`)
|
|
2451
2403
|
}
|
|
2452
2404
|
}
|
|
2453
2405
|
} else {
|
|
@@ -2464,7 +2416,7 @@ class Playwright extends Helper {
|
|
|
2464
2416
|
* {{> wait }}
|
|
2465
2417
|
*/
|
|
2466
2418
|
async wait(sec) {
|
|
2467
|
-
return new Promise(
|
|
2419
|
+
return new Promise(done => {
|
|
2468
2420
|
setTimeout(done, sec * 1000)
|
|
2469
2421
|
})
|
|
2470
2422
|
}
|
|
@@ -2480,20 +2432,18 @@ class Playwright extends Helper {
|
|
|
2480
2432
|
const context = await this._getContext()
|
|
2481
2433
|
if (!locator.isXPath()) {
|
|
2482
2434
|
const valueFn = function ([locator]) {
|
|
2483
|
-
return Array.from(document.querySelectorAll(locator)).filter(
|
|
2435
|
+
return Array.from(document.querySelectorAll(locator)).filter(el => !el.disabled).length > 0
|
|
2484
2436
|
}
|
|
2485
2437
|
waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout })
|
|
2486
2438
|
} else {
|
|
2487
2439
|
const enabledFn = function ([locator, $XPath]) {
|
|
2488
|
-
eval($XPath)
|
|
2489
|
-
return $XPath(null, locator).filter(
|
|
2440
|
+
eval($XPath)
|
|
2441
|
+
return $XPath(null, locator).filter(el => !el.disabled).length > 0
|
|
2490
2442
|
}
|
|
2491
2443
|
waiter = context.waitForFunction(enabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
|
|
2492
2444
|
}
|
|
2493
|
-
return waiter.catch(
|
|
2494
|
-
throw new Error(
|
|
2495
|
-
`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`,
|
|
2496
|
-
)
|
|
2445
|
+
return waiter.catch(err => {
|
|
2446
|
+
throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2497
2447
|
})
|
|
2498
2448
|
}
|
|
2499
2449
|
|
|
@@ -2508,20 +2458,18 @@ class Playwright extends Helper {
|
|
|
2508
2458
|
const context = await this._getContext()
|
|
2509
2459
|
if (!locator.isXPath()) {
|
|
2510
2460
|
const valueFn = function ([locator]) {
|
|
2511
|
-
return Array.from(document.querySelectorAll(locator)).filter(
|
|
2461
|
+
return Array.from(document.querySelectorAll(locator)).filter(el => el.disabled).length > 0
|
|
2512
2462
|
}
|
|
2513
2463
|
waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout })
|
|
2514
2464
|
} else {
|
|
2515
2465
|
const disabledFn = function ([locator, $XPath]) {
|
|
2516
|
-
eval($XPath)
|
|
2517
|
-
return $XPath(null, locator).filter(
|
|
2466
|
+
eval($XPath)
|
|
2467
|
+
return $XPath(null, locator).filter(el => el.disabled).length > 0
|
|
2518
2468
|
}
|
|
2519
2469
|
waiter = context.waitForFunction(disabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
|
|
2520
2470
|
}
|
|
2521
|
-
return waiter.catch(
|
|
2522
|
-
throw new Error(
|
|
2523
|
-
`element (${locator.toString()}) is still enabled after ${waitTimeout / 1000} sec\n${err.message}`,
|
|
2524
|
-
)
|
|
2471
|
+
return waiter.catch(err => {
|
|
2472
|
+
throw new Error(`element (${locator.toString()}) is still enabled after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2525
2473
|
})
|
|
2526
2474
|
}
|
|
2527
2475
|
|
|
@@ -2536,26 +2484,21 @@ class Playwright extends Helper {
|
|
|
2536
2484
|
const context = await this._getContext()
|
|
2537
2485
|
if (!locator.isXPath()) {
|
|
2538
2486
|
const valueFn = function ([locator, value]) {
|
|
2539
|
-
return (
|
|
2540
|
-
Array.from(document.querySelectorAll(locator)).filter((el) => (el.value || '').indexOf(value) !== -1).length >
|
|
2541
|
-
0
|
|
2542
|
-
)
|
|
2487
|
+
return Array.from(document.querySelectorAll(locator)).filter(el => (el.value || '').indexOf(value) !== -1).length > 0
|
|
2543
2488
|
}
|
|
2544
2489
|
waiter = context.waitForFunction(valueFn, [locator.value, value], { timeout: waitTimeout })
|
|
2545
2490
|
} else {
|
|
2546
2491
|
const valueFn = function ([locator, $XPath, value]) {
|
|
2547
|
-
eval($XPath)
|
|
2548
|
-
return $XPath(null, locator).filter(
|
|
2492
|
+
eval($XPath)
|
|
2493
|
+
return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0
|
|
2549
2494
|
}
|
|
2550
2495
|
waiter = context.waitForFunction(valueFn, [locator.value, $XPath.toString(), value], {
|
|
2551
2496
|
timeout: waitTimeout,
|
|
2552
2497
|
})
|
|
2553
2498
|
}
|
|
2554
|
-
return waiter.catch(
|
|
2499
|
+
return waiter.catch(err => {
|
|
2555
2500
|
const loc = locator.toString()
|
|
2556
|
-
throw new Error(
|
|
2557
|
-
`element (${loc}) is not in DOM or there is no element(${loc}) with value "${value}" after ${waitTimeout / 1000} sec\n${err.message}`,
|
|
2558
|
-
)
|
|
2501
|
+
throw new Error(`element (${loc}) is not in DOM or there is no element(${loc}) with value "${value}" after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2559
2502
|
})
|
|
2560
2503
|
}
|
|
2561
2504
|
|
|
@@ -2575,22 +2518,20 @@ class Playwright extends Helper {
|
|
|
2575
2518
|
if (!els || els.length === 0) {
|
|
2576
2519
|
return false
|
|
2577
2520
|
}
|
|
2578
|
-
return Array.prototype.filter.call(els,
|
|
2521
|
+
return Array.prototype.filter.call(els, el => el.offsetParent !== null).length === num
|
|
2579
2522
|
}
|
|
2580
2523
|
waiter = context.waitForFunction(visibleFn, [locator.value, num], { timeout: waitTimeout })
|
|
2581
2524
|
} else {
|
|
2582
2525
|
const visibleFn = function ([locator, $XPath, num]) {
|
|
2583
|
-
eval($XPath)
|
|
2584
|
-
return $XPath(null, locator).filter(
|
|
2526
|
+
eval($XPath)
|
|
2527
|
+
return $XPath(null, locator).filter(el => el.offsetParent !== null).length === num
|
|
2585
2528
|
}
|
|
2586
2529
|
waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString(), num], {
|
|
2587
2530
|
timeout: waitTimeout,
|
|
2588
2531
|
})
|
|
2589
2532
|
}
|
|
2590
|
-
return waiter.catch(
|
|
2591
|
-
throw new Error(
|
|
2592
|
-
`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`,
|
|
2593
|
-
)
|
|
2533
|
+
return waiter.catch(err => {
|
|
2534
|
+
throw new Error(`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2594
2535
|
})
|
|
2595
2536
|
}
|
|
2596
2537
|
|
|
@@ -2598,9 +2539,7 @@ class Playwright extends Helper {
|
|
|
2598
2539
|
* {{> waitForClickable }}
|
|
2599
2540
|
*/
|
|
2600
2541
|
async waitForClickable(locator, waitTimeout) {
|
|
2601
|
-
console.log(
|
|
2602
|
-
'I.waitForClickable is DEPRECATED: This is no longer needed, Playwright automatically waits for element to be clickable',
|
|
2603
|
-
)
|
|
2542
|
+
console.log('I.waitForClickable is DEPRECATED: This is no longer needed, Playwright automatically waits for element to be clickable')
|
|
2604
2543
|
console.log('Remove usage of this function')
|
|
2605
2544
|
}
|
|
2606
2545
|
|
|
@@ -2616,9 +2555,7 @@ class Playwright extends Helper {
|
|
|
2616
2555
|
try {
|
|
2617
2556
|
await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
|
|
2618
2557
|
} catch (e) {
|
|
2619
|
-
throw new Error(
|
|
2620
|
-
`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`,
|
|
2621
|
-
)
|
|
2558
|
+
throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`)
|
|
2622
2559
|
}
|
|
2623
2560
|
}
|
|
2624
2561
|
|
|
@@ -2710,10 +2647,8 @@ class Playwright extends Helper {
|
|
|
2710
2647
|
.locator(buildLocatorString(locator))
|
|
2711
2648
|
.first()
|
|
2712
2649
|
.waitFor({ timeout: waitTimeout, state: 'hidden' })
|
|
2713
|
-
.catch(
|
|
2714
|
-
throw new Error(
|
|
2715
|
-
`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`,
|
|
2716
|
-
)
|
|
2650
|
+
.catch(err => {
|
|
2651
|
+
throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2717
2652
|
})
|
|
2718
2653
|
}
|
|
2719
2654
|
|
|
@@ -2739,6 +2674,9 @@ class Playwright extends Helper {
|
|
|
2739
2674
|
if ((this.context && this.context.constructor.name === 'FrameLocator') || this.context) {
|
|
2740
2675
|
return this.context
|
|
2741
2676
|
}
|
|
2677
|
+
if (this.frame) {
|
|
2678
|
+
return this.frame
|
|
2679
|
+
}
|
|
2742
2680
|
return this.page
|
|
2743
2681
|
}
|
|
2744
2682
|
|
|
@@ -2750,14 +2688,14 @@ class Playwright extends Helper {
|
|
|
2750
2688
|
|
|
2751
2689
|
return this.page
|
|
2752
2690
|
.waitForFunction(
|
|
2753
|
-
|
|
2691
|
+
urlPart => {
|
|
2754
2692
|
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
|
|
2755
2693
|
return currUrl.indexOf(urlPart) > -1
|
|
2756
2694
|
},
|
|
2757
2695
|
urlPart,
|
|
2758
2696
|
{ timeout: waitTimeout },
|
|
2759
2697
|
)
|
|
2760
|
-
.catch(async
|
|
2698
|
+
.catch(async e => {
|
|
2761
2699
|
const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
|
|
2762
2700
|
if (/Timeout/i.test(e.message)) {
|
|
2763
2701
|
throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
|
|
@@ -2780,14 +2718,14 @@ class Playwright extends Helper {
|
|
|
2780
2718
|
|
|
2781
2719
|
return this.page
|
|
2782
2720
|
.waitForFunction(
|
|
2783
|
-
|
|
2721
|
+
urlPart => {
|
|
2784
2722
|
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
|
|
2785
2723
|
return currUrl.indexOf(urlPart) > -1
|
|
2786
2724
|
},
|
|
2787
2725
|
urlPart,
|
|
2788
2726
|
{ timeout: waitTimeout },
|
|
2789
2727
|
)
|
|
2790
|
-
.catch(async
|
|
2728
|
+
.catch(async e => {
|
|
2791
2729
|
const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
|
|
2792
2730
|
if (/Timeout/i.test(e.message)) {
|
|
2793
2731
|
throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`)
|
|
@@ -2803,28 +2741,23 @@ class Playwright extends Helper {
|
|
|
2803
2741
|
async waitForText(text, sec = null, context = null) {
|
|
2804
2742
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2805
2743
|
const errorMessage = `Text "${text}" was not found on page after ${waitTimeout / 1000} sec.`
|
|
2806
|
-
let waiter
|
|
2807
2744
|
|
|
2808
2745
|
const contextObject = await this._getContext()
|
|
2809
2746
|
|
|
2810
2747
|
if (context) {
|
|
2811
2748
|
const locator = new Locator(context, 'css')
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2749
|
+
try {
|
|
2750
|
+
if (!locator.isXPath()) {
|
|
2751
|
+
return contextObject
|
|
2815
2752
|
.locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`)
|
|
2816
2753
|
.first()
|
|
2817
2754
|
.waitFor({ timeout: waitTimeout, state: 'visible' })
|
|
2818
|
-
} catch (e) {
|
|
2819
|
-
throw new Error(`${errorMessage}\n${e.message}`)
|
|
2820
2755
|
}
|
|
2821
|
-
}
|
|
2822
2756
|
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
await contextObject.waitForFunction(
|
|
2757
|
+
if (locator.isXPath()) {
|
|
2758
|
+
return contextObject.waitForFunction(
|
|
2826
2759
|
([locator, text, $XPath]) => {
|
|
2827
|
-
eval($XPath)
|
|
2760
|
+
eval($XPath)
|
|
2828
2761
|
const el = $XPath(null, locator)
|
|
2829
2762
|
if (!el.length) return false
|
|
2830
2763
|
return el[0].innerText.indexOf(text) > -1
|
|
@@ -2832,27 +2765,34 @@ class Playwright extends Helper {
|
|
|
2832
2765
|
[locator.value, text, $XPath.toString()],
|
|
2833
2766
|
{ timeout: waitTimeout },
|
|
2834
2767
|
)
|
|
2835
|
-
} catch (e) {
|
|
2836
|
-
throw new Error(`${errorMessage}\n${e.message}`)
|
|
2837
2768
|
}
|
|
2769
|
+
} catch (e) {
|
|
2770
|
+
throw new Error(`${errorMessage}\n${e.message}`)
|
|
2838
2771
|
}
|
|
2839
|
-
} else {
|
|
2840
|
-
// we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
|
|
2841
|
-
|
|
2842
|
-
const _contextObject = this.frame ? this.frame : contextObject
|
|
2843
|
-
let count = 0
|
|
2844
|
-
do {
|
|
2845
|
-
waiter = await _contextObject
|
|
2846
|
-
.locator(`:has-text(${JSON.stringify(text)})`)
|
|
2847
|
-
.first()
|
|
2848
|
-
.isVisible()
|
|
2849
|
-
if (waiter) break
|
|
2850
|
-
await this.wait(1)
|
|
2851
|
-
count += 1000
|
|
2852
|
-
} while (count <= waitTimeout)
|
|
2853
|
-
|
|
2854
|
-
if (!waiter) throw new Error(`${errorMessage}`)
|
|
2855
2772
|
}
|
|
2773
|
+
|
|
2774
|
+
const timeoutGap = waitTimeout + 1000
|
|
2775
|
+
|
|
2776
|
+
// We add basic timeout to make sure we don't wait forever
|
|
2777
|
+
// We apply 2 strategies here: wait for text as innert text on page (wide strategy) - older
|
|
2778
|
+
// or we use native Playwright matcher to wait for text in element (narrow strategy) - newer
|
|
2779
|
+
// If a user waits for text on a page they are mostly expect it to be there, so wide strategy can be helpful even PW strategy is available
|
|
2780
|
+
return Promise.race([
|
|
2781
|
+
new Promise((_, reject) => {
|
|
2782
|
+
setTimeout(() => reject(errorMessage), waitTimeout)
|
|
2783
|
+
}),
|
|
2784
|
+
this.page.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, text, { timeout: timeoutGap }),
|
|
2785
|
+
promiseRetry(
|
|
2786
|
+
async retry => {
|
|
2787
|
+
const textPresent = await contextObject
|
|
2788
|
+
.locator(`:has-text(${JSON.stringify(text)})`)
|
|
2789
|
+
.first()
|
|
2790
|
+
.isVisible()
|
|
2791
|
+
if (!textPresent) retry(errorMessage)
|
|
2792
|
+
},
|
|
2793
|
+
{ retries: 1000, minTimeout: 500, maxTimeout: 500, factor: 1 },
|
|
2794
|
+
),
|
|
2795
|
+
])
|
|
2856
2796
|
}
|
|
2857
2797
|
|
|
2858
2798
|
/**
|
|
@@ -3021,11 +2961,11 @@ class Playwright extends Helper {
|
|
|
3021
2961
|
}
|
|
3022
2962
|
} else {
|
|
3023
2963
|
const visibleFn = function ([locator, $XPath]) {
|
|
3024
|
-
eval($XPath)
|
|
2964
|
+
eval($XPath)
|
|
3025
2965
|
return $XPath(null, locator).length === 0
|
|
3026
2966
|
}
|
|
3027
2967
|
waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
|
|
3028
|
-
return waiter.catch(
|
|
2968
|
+
return waiter.catch(err => {
|
|
3029
2969
|
throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
3030
2970
|
})
|
|
3031
2971
|
}
|
|
@@ -3047,9 +2987,9 @@ class Playwright extends Helper {
|
|
|
3047
2987
|
|
|
3048
2988
|
return promiseRetry(
|
|
3049
2989
|
async (retry, number) => {
|
|
3050
|
-
const _grabCookie = async
|
|
2990
|
+
const _grabCookie = async name => {
|
|
3051
2991
|
const cookies = await this.browserContext.cookies()
|
|
3052
|
-
const cookie = cookies.filter(
|
|
2992
|
+
const cookie = cookies.filter(c => c.name === name)
|
|
3053
2993
|
if (cookie.length === 0) throw Error(`Cookie ${name} is not found after ${retries}s`)
|
|
3054
2994
|
}
|
|
3055
2995
|
|
|
@@ -3128,7 +3068,7 @@ class Playwright extends Helper {
|
|
|
3128
3068
|
this.recording = true
|
|
3129
3069
|
this.recordedAtLeastOnce = true
|
|
3130
3070
|
|
|
3131
|
-
this.page.on('requestfinished', async
|
|
3071
|
+
this.page.on('requestfinished', async request => {
|
|
3132
3072
|
const information = {
|
|
3133
3073
|
url: request.url(),
|
|
3134
3074
|
method: request.method(),
|
|
@@ -3167,20 +3107,20 @@ class Playwright extends Helper {
|
|
|
3167
3107
|
*/
|
|
3168
3108
|
blockTraffic(urls) {
|
|
3169
3109
|
if (Array.isArray(urls)) {
|
|
3170
|
-
urls.forEach(
|
|
3171
|
-
this.page.route(url,
|
|
3110
|
+
urls.forEach(url => {
|
|
3111
|
+
this.page.route(url, route => {
|
|
3172
3112
|
route
|
|
3173
3113
|
.abort()
|
|
3174
3114
|
// Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
|
|
3175
|
-
.catch(
|
|
3115
|
+
.catch(e => {})
|
|
3176
3116
|
})
|
|
3177
3117
|
})
|
|
3178
3118
|
} else {
|
|
3179
|
-
this.page.route(urls,
|
|
3119
|
+
this.page.route(urls, route => {
|
|
3180
3120
|
route
|
|
3181
3121
|
.abort()
|
|
3182
3122
|
// Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
|
|
3183
|
-
.catch(
|
|
3123
|
+
.catch(e => {})
|
|
3184
3124
|
})
|
|
3185
3125
|
}
|
|
3186
3126
|
}
|
|
@@ -3209,8 +3149,8 @@ class Playwright extends Helper {
|
|
|
3209
3149
|
urls = [urls]
|
|
3210
3150
|
}
|
|
3211
3151
|
|
|
3212
|
-
urls.forEach(
|
|
3213
|
-
this.page.route(url,
|
|
3152
|
+
urls.forEach(url => {
|
|
3153
|
+
this.page.route(url, route => {
|
|
3214
3154
|
if (this.page.isClosed()) {
|
|
3215
3155
|
// Sometimes it happens that browser has been closed in the meantime.
|
|
3216
3156
|
// In this case we just don't fulfill to prevent error in test scenario.
|
|
@@ -3256,13 +3196,10 @@ class Playwright extends Helper {
|
|
|
3256
3196
|
*/
|
|
3257
3197
|
grabTrafficUrl(urlMatch) {
|
|
3258
3198
|
if (!this.recordedAtLeastOnce) {
|
|
3259
|
-
throw new Error(
|
|
3260
|
-
'Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.',
|
|
3261
|
-
)
|
|
3199
|
+
throw new Error('Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.')
|
|
3262
3200
|
}
|
|
3263
3201
|
|
|
3264
3202
|
for (const i in this.requests) {
|
|
3265
|
-
// eslint-disable-next-line no-prototype-builtins
|
|
3266
3203
|
if (this.requests.hasOwnProperty(i)) {
|
|
3267
3204
|
const request = this.requests[i]
|
|
3268
3205
|
|
|
@@ -3312,15 +3249,15 @@ class Playwright extends Helper {
|
|
|
3312
3249
|
await this.cdpSession.send('Network.enable')
|
|
3313
3250
|
await this.cdpSession.send('Page.enable')
|
|
3314
3251
|
|
|
3315
|
-
this.cdpSession.on('Network.webSocketFrameReceived',
|
|
3252
|
+
this.cdpSession.on('Network.webSocketFrameReceived', payload => {
|
|
3316
3253
|
this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload))
|
|
3317
3254
|
})
|
|
3318
3255
|
|
|
3319
|
-
this.cdpSession.on('Network.webSocketFrameSent',
|
|
3256
|
+
this.cdpSession.on('Network.webSocketFrameSent', payload => {
|
|
3320
3257
|
this._logWebsocketMessages(this._getWebSocketLog('SENT', payload))
|
|
3321
3258
|
})
|
|
3322
3259
|
|
|
3323
|
-
this.cdpSession.on('Network.webSocketFrameError',
|
|
3260
|
+
this.cdpSession.on('Network.webSocketFrameError', payload => {
|
|
3324
3261
|
this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload))
|
|
3325
3262
|
})
|
|
3326
3263
|
}
|
|
@@ -3344,9 +3281,7 @@ class Playwright extends Helper {
|
|
|
3344
3281
|
grabWebSocketMessages() {
|
|
3345
3282
|
if (!this.recordingWebSocketMessages) {
|
|
3346
3283
|
if (!this.recordedWebSocketMessagesAtLeastOnce) {
|
|
3347
|
-
throw new Error(
|
|
3348
|
-
'Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.',
|
|
3349
|
-
)
|
|
3284
|
+
throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.')
|
|
3350
3285
|
}
|
|
3351
3286
|
}
|
|
3352
3287
|
return this.webSocketMessages
|
|
@@ -3493,17 +3428,13 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
3493
3428
|
}
|
|
3494
3429
|
const els = await findClickable.call(this, matcher, locator)
|
|
3495
3430
|
if (context) {
|
|
3496
|
-
assertElementExists(
|
|
3497
|
-
els,
|
|
3498
|
-
locator,
|
|
3499
|
-
'Clickable element',
|
|
3500
|
-
`was not found inside element ${new Locator(context).toString()}`,
|
|
3501
|
-
)
|
|
3431
|
+
assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`)
|
|
3502
3432
|
} else {
|
|
3503
3433
|
assertElementExists(els, locator, 'Clickable element')
|
|
3504
3434
|
}
|
|
3505
3435
|
|
|
3506
3436
|
await highlightActiveElement.call(this, els[0])
|
|
3437
|
+
if (store.debugMode) this.debugSection('Clicked', await elToString(els[0], 1))
|
|
3507
3438
|
|
|
3508
3439
|
/*
|
|
3509
3440
|
using the force true options itself but instead dispatching a click
|
|
@@ -3565,16 +3496,13 @@ async function proceedSee(assertType, text, context, strict = false) {
|
|
|
3565
3496
|
description = `element ${locator.toString()}`
|
|
3566
3497
|
const els = await this._locate(locator)
|
|
3567
3498
|
assertElementExists(els, locator.toString())
|
|
3568
|
-
allText = await Promise.all(els.map(
|
|
3499
|
+
allText = await Promise.all(els.map(el => el.innerText()))
|
|
3569
3500
|
}
|
|
3570
3501
|
|
|
3571
3502
|
if (strict) {
|
|
3572
|
-
return allText.map(
|
|
3503
|
+
return allText.map(elText => equals(description)[assertType](text, elText))
|
|
3573
3504
|
}
|
|
3574
|
-
return stringIncludes(description)[assertType](
|
|
3575
|
-
normalizeSpacesInString(text),
|
|
3576
|
-
normalizeSpacesInString(allText.join(' | ')),
|
|
3577
|
-
)
|
|
3505
|
+
return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')))
|
|
3578
3506
|
}
|
|
3579
3507
|
|
|
3580
3508
|
async function findCheckable(locator, context) {
|
|
@@ -3604,7 +3532,7 @@ async function findCheckable(locator, context) {
|
|
|
3604
3532
|
async function proceedIsChecked(assertType, option) {
|
|
3605
3533
|
let els = await findCheckable.call(this, option)
|
|
3606
3534
|
assertElementExists(els, option, 'Checkable')
|
|
3607
|
-
els = await Promise.all(els.map(
|
|
3535
|
+
els = await Promise.all(els.map(el => el.isChecked()))
|
|
3608
3536
|
const selected = els.reduce((prev, cur) => prev || cur)
|
|
3609
3537
|
return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
|
|
3610
3538
|
}
|
|
@@ -3636,10 +3564,10 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
3636
3564
|
const els = await findFields.call(this, field)
|
|
3637
3565
|
assertElementExists(els, field, 'Field')
|
|
3638
3566
|
const el = els[0]
|
|
3639
|
-
const tag = await el.evaluate(
|
|
3567
|
+
const tag = await el.evaluate(e => e.tagName)
|
|
3640
3568
|
const fieldType = await el.getAttribute('type')
|
|
3641
3569
|
|
|
3642
|
-
const proceedMultiple = async
|
|
3570
|
+
const proceedMultiple = async elements => {
|
|
3643
3571
|
const fields = Array.isArray(elements) ? elements : [elements]
|
|
3644
3572
|
|
|
3645
3573
|
const elementValues = []
|
|
@@ -3653,7 +3581,7 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
3653
3581
|
if (assertType === 'assert') {
|
|
3654
3582
|
equals(`select option by ${field}`)[assertType](true, elementValues.length > 0)
|
|
3655
3583
|
}
|
|
3656
|
-
elementValues.forEach(
|
|
3584
|
+
elementValues.forEach(val => stringIncludes(`fields by ${field}`)[assertType](value, val))
|
|
3657
3585
|
}
|
|
3658
3586
|
}
|
|
3659
3587
|
|
|
@@ -3740,6 +3668,8 @@ function isFrameLocator(locator) {
|
|
|
3740
3668
|
}
|
|
3741
3669
|
|
|
3742
3670
|
function assertElementExists(res, locator, prefix, suffix) {
|
|
3671
|
+
// if element text is an empty string, just exit this check
|
|
3672
|
+
if (typeof res === 'string' && res === '') return
|
|
3743
3673
|
if (!res || res.length === 0) {
|
|
3744
3674
|
throw new ElementNotFound(locator, prefix, suffix)
|
|
3745
3675
|
}
|
|
@@ -3776,12 +3706,9 @@ async function targetCreatedHandler(page) {
|
|
|
3776
3706
|
this.contextLocator = null
|
|
3777
3707
|
})
|
|
3778
3708
|
})
|
|
3779
|
-
page.on('console',
|
|
3709
|
+
page.on('console', msg => {
|
|
3780
3710
|
if (!consoleLogStore.includes(msg) && this.options.ignoreLog && !this.options.ignoreLog.includes(msg.type())) {
|
|
3781
|
-
this.debugSection(
|
|
3782
|
-
`Browser:${ucfirst(msg.type())}`,
|
|
3783
|
-
((msg.text && msg.text()) || msg._text || '') + msg.args().join(' '),
|
|
3784
|
-
)
|
|
3711
|
+
this.debugSection(`Browser:${ucfirst(msg.type())}`, ((msg.text && msg.text()) || msg._text || '') + msg.args().join(' '))
|
|
3785
3712
|
}
|
|
3786
3713
|
consoleLogStore.add(msg)
|
|
3787
3714
|
})
|
|
@@ -3889,7 +3816,7 @@ async function refreshContextSession() {
|
|
|
3889
3816
|
const contexts = await this.browser.contexts()
|
|
3890
3817
|
contexts.shift()
|
|
3891
3818
|
|
|
3892
|
-
await Promise.all(contexts.map(
|
|
3819
|
+
await Promise.all(contexts.map(c => c.close()))
|
|
3893
3820
|
} catch (e) {
|
|
3894
3821
|
console.log(e)
|
|
3895
3822
|
}
|
|
@@ -3908,10 +3835,10 @@ async function refreshContextSession() {
|
|
|
3908
3835
|
const currentUrl = await this.grabCurrentUrl()
|
|
3909
3836
|
|
|
3910
3837
|
if (currentUrl.startsWith('http')) {
|
|
3911
|
-
await this.executeScript('localStorage.clear();').catch(
|
|
3838
|
+
await this.executeScript('localStorage.clear();').catch(err => {
|
|
3912
3839
|
if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
|
|
3913
3840
|
})
|
|
3914
|
-
await this.executeScript('sessionStorage.clear();').catch(
|
|
3841
|
+
await this.executeScript('sessionStorage.clear();').catch(err => {
|
|
3915
3842
|
if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
|
|
3916
3843
|
})
|
|
3917
3844
|
}
|
|
@@ -3941,11 +3868,22 @@ async function saveTraceForContext(context, name) {
|
|
|
3941
3868
|
}
|
|
3942
3869
|
|
|
3943
3870
|
async function highlightActiveElement(element) {
|
|
3944
|
-
if (this.options.highlightElement &&
|
|
3945
|
-
await element.evaluate(
|
|
3871
|
+
if ((this.options.highlightElement || store.onPause) && store.debugMode) {
|
|
3872
|
+
await element.evaluate(el => {
|
|
3946
3873
|
const prevStyle = el.style.boxShadow
|
|
3947
|
-
el.style.boxShadow = '0px 0px 4px 3px rgba(
|
|
3874
|
+
el.style.boxShadow = '0px 0px 4px 3px rgba(147, 51, 234, 0.8)' // Bright purple that works on both dark/light modes
|
|
3948
3875
|
setTimeout(() => (el.style.boxShadow = prevStyle), 2000)
|
|
3949
3876
|
})
|
|
3950
3877
|
}
|
|
3951
3878
|
}
|
|
3879
|
+
|
|
3880
|
+
async function elToString(el, numberOfElements) {
|
|
3881
|
+
const html = await el.evaluate(node => node.outerHTML)
|
|
3882
|
+
return (
|
|
3883
|
+
html
|
|
3884
|
+
.replace(/\n/g, '')
|
|
3885
|
+
.replace(/\s+/g, ' ')
|
|
3886
|
+
.substring(0, 100 / numberOfElements)
|
|
3887
|
+
.trim() + '...'
|
|
3888
|
+
)
|
|
3889
|
+
}
|