codeceptjs 4.0.0-rc.2 → 4.0.0-rc.7

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.
@@ -0,0 +1,610 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
4
+ import Codecept from '../lib/codecept.js'
5
+ import container from '../lib/container.js'
6
+ import { getParamsToString } from '../lib/parser.js'
7
+ import { methodsOfObject } from '../lib/utils.js'
8
+ import event from '../lib/event.js'
9
+ import { fileURLToPath } from 'url'
10
+ import { dirname, resolve as resolvePath } from 'path'
11
+ import path from 'path'
12
+ import crypto from 'crypto'
13
+ import { spawn } from 'child_process'
14
+ import { createRequire } from 'module'
15
+ import { existsSync, readdirSync } from 'fs'
16
+
17
+ const require = createRequire(import.meta.url)
18
+
19
+ const __filename = fileURLToPath(import.meta.url)
20
+ const __dirname = dirname(__filename)
21
+
22
+ let codecept = null
23
+ let containerInitialized = false
24
+ let browserStarted = false
25
+
26
+ let runLock = Promise.resolve()
27
+ async function withLock(fn) {
28
+ const prev = runLock
29
+ let release
30
+ runLock = new Promise(r => (release = r))
31
+ await prev
32
+ try { return await fn() }
33
+ finally { release() }
34
+ }
35
+
36
+ async function withSilencedIO(fn) {
37
+ const origOut = process.stdout.write.bind(process.stdout)
38
+ const origErr = process.stderr.write.bind(process.stderr)
39
+
40
+ process.stdout.write = () => true
41
+ process.stderr.write = () => true
42
+
43
+ try {
44
+ return await fn()
45
+ } finally {
46
+ process.stdout.write = origOut
47
+ process.stderr.write = origErr
48
+ }
49
+ }
50
+
51
+ function runCmd(cmd, args, { cwd = process.cwd(), timeout = 60000 } = {}) {
52
+ return new Promise((resolve, reject) => {
53
+ const child = spawn(cmd, args, {
54
+ cwd,
55
+ env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'test' },
56
+ stdio: ['ignore', 'pipe', 'pipe'],
57
+ })
58
+
59
+ let out = ''
60
+ let err = ''
61
+
62
+ const t = setTimeout(() => {
63
+ child.kill('SIGKILL')
64
+ reject(new Error(`Timeout after ${timeout}ms`))
65
+ }, timeout)
66
+
67
+ child.stdout.on('data', d => (out += d.toString('utf8')))
68
+ child.stderr.on('data', d => (err += d.toString('utf8')))
69
+
70
+ child.on('error', e => {
71
+ clearTimeout(t)
72
+ reject(e)
73
+ })
74
+
75
+ child.on('close', code => {
76
+ clearTimeout(t)
77
+ resolve({ code, out, err })
78
+ })
79
+ })
80
+ }
81
+
82
+ function resolveConfigPath(configPath) {
83
+ const cwd = process.cwd()
84
+ const envRoot = process.env.CODECEPTJS_PROJECT_DIR
85
+
86
+ if (configPath && !path.isAbsolute(configPath)) {
87
+ const base = envRoot || cwd
88
+ configPath = path.resolve(base, configPath)
89
+ }
90
+
91
+ if (!configPath) {
92
+ const base = envRoot || cwd
93
+ configPath = process.env.CODECEPTJS_CONFIG || path.resolve(base, 'codecept.conf.js')
94
+ if (!existsSync(configPath)) configPath = path.resolve(base, 'codecept.conf.cjs')
95
+ }
96
+
97
+ if (!existsSync(configPath)) {
98
+ throw new Error(
99
+ `CodeceptJS config not found: ${configPath}\n` +
100
+ `CODECEPTJS_CONFIG=${process.env.CODECEPTJS_CONFIG || 'not set'}\n` +
101
+ `CODECEPTJS_PROJECT_DIR=${process.env.CODECEPTJS_PROJECT_DIR || 'not set'}\n` +
102
+ `cwd=${cwd}`
103
+ )
104
+ }
105
+
106
+ return { configPath, configDir: path.dirname(configPath) }
107
+ }
108
+
109
+ function findCodeceptCliUpwards(startDir, { maxUp = 8 } = {}) {
110
+ let dir = startDir
111
+
112
+ for (let i = 0; i <= maxUp; i++) {
113
+ const candidates = [
114
+ path.resolve(dir, 'bin', 'codecept.js'),
115
+ path.resolve(dir, 'node_modules', 'codeceptjs', 'bin', 'codecept.js'),
116
+ path.resolve(dir, 'node_modules', '.bin', 'codeceptjs.cmd'),
117
+ path.resolve(dir, 'node_modules', '.bin', 'codeceptjs'),
118
+ ]
119
+
120
+ for (const p of candidates) {
121
+ if (existsSync(p)) return { cli: p, root: dir }
122
+ }
123
+
124
+ try {
125
+ const pkgJson = require.resolve('codeceptjs/package.json', { paths: [dir] })
126
+ const pkgDir = path.dirname(pkgJson)
127
+ const jsCli = path.resolve(pkgDir, 'bin', 'codecept.js')
128
+ if (existsSync(jsCli)) return { cli: jsCli, root: dir }
129
+ } catch {}
130
+
131
+ const parent = path.dirname(dir)
132
+ if (parent === dir) break
133
+ dir = parent
134
+ }
135
+
136
+ throw new Error(`Cannot find CodeceptJS CLI walking up from: ${startDir}`)
137
+ }
138
+
139
+ function looksLikePath(v) {
140
+ return typeof v === 'string' && (
141
+ v.includes('/') || v.includes('\\') ||
142
+ v.endsWith('.js') || v.endsWith('.ts')
143
+ )
144
+ }
145
+
146
+ function normalizePath(p) {
147
+ return String(p).replace(/\\/g, '/')
148
+ }
149
+
150
+ function findFileByBasename(rootDir, baseNames, { maxDepth = 8 } = {}) {
151
+ const targets = new Set(baseNames.map(x => x.toLowerCase()))
152
+
153
+ function walk(dir, depth) {
154
+ if (depth > maxDepth) return null
155
+
156
+ let entries
157
+ try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return null }
158
+
159
+ for (const e of entries) {
160
+ const full = path.join(dir, e.name)
161
+
162
+ if (e.isDirectory()) {
163
+ if (e.name === 'node_modules' || e.name === '.git' || e.name === 'output') continue
164
+ const res = walk(full, depth + 1)
165
+ if (res) return res
166
+ continue
167
+ }
168
+
169
+ if (targets.has(e.name.toLowerCase())) return full
170
+ }
171
+
172
+ return null
173
+ }
174
+
175
+ return walk(rootDir, 0)
176
+ }
177
+
178
+ async function listTestsJson({ cli, root, configPath }) {
179
+ const args = ['list', '--config', configPath, '--json']
180
+ const isNodeScript = cli.endsWith('.js')
181
+
182
+ const res = isNodeScript
183
+ ? await runCmd(process.execPath, [cli, ...args], { cwd: root, timeout: 60000 })
184
+ : await runCmd(cli, args, { cwd: root, timeout: 60000 })
185
+
186
+ const out = (res.out || '').trim()
187
+ try { return JSON.parse(out) } catch { return null }
188
+ }
189
+
190
+ function extractFilesFromListJson(json) {
191
+ if (!json) return []
192
+ if (Array.isArray(json)) return json.map(String)
193
+ if (Array.isArray(json.tests)) return json.tests.map(String)
194
+ if (Array.isArray(json.files)) return json.files.map(String)
195
+ if (Array.isArray(json.testFiles)) return json.testFiles.map(String)
196
+ return []
197
+ }
198
+
199
+ async function resolveTestToFile({ cli, root, configPath, test }) {
200
+ if (looksLikePath(test)) return test
201
+
202
+ const raw = String(test).trim()
203
+ const candidates = [
204
+ raw,
205
+ `${raw}.js`,
206
+ `${raw}.ts`,
207
+ `${raw}_test.js`,
208
+ `${raw}.test.js`,
209
+ ].map(x => x.toLowerCase())
210
+
211
+ const json = await listTestsJson({ cli, root, configPath })
212
+ const files = extractFilesFromListJson(json).map(normalizePath)
213
+
214
+ if (files.length) {
215
+ const byName = files.find(f => candidates.some(c => path.basename(f).toLowerCase() === c))
216
+ if (byName) return byName
217
+
218
+ const byContains = files.find(f => f.toLowerCase().includes(raw.toLowerCase()))
219
+ if (byContains) return byContains
220
+ }
221
+
222
+ const fsFound = findFileByBasename(root, candidates)
223
+ return fsFound ? normalizePath(fsFound) : null
224
+ }
225
+
226
+ function clearString(str) {
227
+ return str.replace(/[^a-zA-Z0-9]/g, '_')
228
+ }
229
+
230
+ function getTraceDir(testTitle, testFile) {
231
+ const hash = crypto.createHash('sha256').update(testFile + testTitle).digest('hex').slice(0, 8)
232
+ const cleanTitle = clearString(testTitle).slice(0, 200)
233
+ const outputDir = global.output_dir || resolvePath(process.cwd(), 'output')
234
+ return resolvePath(outputDir, `trace_${cleanTitle}_${hash}`)
235
+ }
236
+
237
+ async function initCodecept(configPath) {
238
+ if (containerInitialized) return
239
+
240
+ const testRoot = process.env.CODECEPTJS_PROJECT_DIR || process.cwd()
241
+
242
+ if (!configPath) {
243
+ configPath = process.env.CODECEPTJS_CONFIG || resolvePath(testRoot, 'codecept.conf.js')
244
+ if (!existsSync(configPath)) configPath = resolvePath(testRoot, 'codecept.conf.cjs')
245
+ }
246
+
247
+ if (!existsSync(configPath)) {
248
+ throw new Error(
249
+ `CodeceptJS config not found: ${configPath}\n` +
250
+ `CODECEPTJS_CONFIG=${process.env.CODECEPTJS_CONFIG || 'not set'}\n` +
251
+ `CODECEPTJS_PROJECT_DIR=${process.env.CODECEPTJS_PROJECT_DIR || 'not set'}\n` +
252
+ `cwd=${process.cwd()}`
253
+ )
254
+ }
255
+
256
+ console.log = () => {}
257
+ console.error = () => {}
258
+ console.warn = () => {}
259
+
260
+ const { getConfig } = await import('../lib/command/utils.js')
261
+ const config = await getConfig(configPath)
262
+
263
+ codecept = new Codecept(config, {})
264
+ await codecept.init(testRoot)
265
+ await container.create(config, {})
266
+ await container.started()
267
+
268
+ containerInitialized = true
269
+ browserStarted = true
270
+ }
271
+
272
+ const server = new Server(
273
+ { name: 'codeceptjs-mcp-server', version: '1.0.0' },
274
+ { capabilities: { tools: {} } }
275
+ )
276
+
277
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
278
+ tools: [
279
+ {
280
+ name: 'list_tests',
281
+ description: 'List all tests in the CodeceptJS project',
282
+ inputSchema: { type: 'object', properties: { config: { type: 'string' } } },
283
+ },
284
+ {
285
+ name: 'list_actions',
286
+ description: 'List all available CodeceptJS actions (I.* methods)',
287
+ inputSchema: { type: 'object', properties: { config: { type: 'string' } } },
288
+ },
289
+ {
290
+ name: 'run_code',
291
+ description: 'Run arbitrary CodeceptJS code.',
292
+ inputSchema: {
293
+ type: 'object',
294
+ properties: {
295
+ code: { type: 'string' },
296
+ timeout: { type: 'number' },
297
+ config: { type: 'string' },
298
+ saveArtifacts: { type: 'boolean' },
299
+ },
300
+ required: ['code'],
301
+ },
302
+ },
303
+ {
304
+ name: 'run_test',
305
+ description: 'Run a specific test.',
306
+ inputSchema: {
307
+ type: 'object',
308
+ properties: {
309
+ test: { type: 'string' },
310
+ timeout: { type: 'number' },
311
+ config: { type: 'string' },
312
+ },
313
+ required: ['test'],
314
+ },
315
+ },
316
+ {
317
+ name: 'run_step_by_step',
318
+ description: 'Run a test step by step with pauses between steps.',
319
+ inputSchema: {
320
+ type: 'object',
321
+ properties: {
322
+ test: { type: 'string' },
323
+ timeout: { type: 'number' },
324
+ config: { type: 'string' },
325
+ },
326
+ required: ['test'],
327
+ },
328
+ },
329
+ {
330
+ name: 'start_browser',
331
+ description: 'Start the browser session.',
332
+ inputSchema: { type: 'object', properties: { config: { type: 'string' } } },
333
+ },
334
+ {
335
+ name: 'stop_browser',
336
+ description: 'Stop the browser session.',
337
+ inputSchema: { type: 'object', properties: {} },
338
+ },
339
+ ],
340
+ }))
341
+
342
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
343
+ const { name, arguments: args } = request.params
344
+
345
+ try {
346
+ switch (name) {
347
+ case 'list_tests': {
348
+ const configPath = args?.config
349
+ await initCodecept(configPath)
350
+
351
+ codecept.loadTests()
352
+ const tests = codecept.testFiles.map(testFile => {
353
+ const relativePath = testFile.replace(process.cwd(), '').replace(/\\/g, '/')
354
+ return {
355
+ file: testFile,
356
+ relativePath: relativePath.startsWith('/') ? relativePath.slice(1) : relativePath,
357
+ }
358
+ })
359
+
360
+ return { content: [{ type: 'text', text: JSON.stringify({ count: tests.length, tests }, null, 2) }] }
361
+ }
362
+
363
+ case 'list_actions': {
364
+ const configPath = args?.config
365
+ await initCodecept(configPath)
366
+
367
+ const helpers = container.helpers()
368
+ const supportI = container.support('I')
369
+ const actions = []
370
+ const actionDetails = []
371
+
372
+ for (const helperName in helpers) {
373
+ const helper = helpers[helperName]
374
+ methodsOfObject(helper).forEach(action => {
375
+ if (actions.includes(action)) return
376
+ actions.push(action)
377
+ const params = getParamsToString(helper[action])
378
+ actionDetails.push({ helper: helperName, action, signature: `I.${action}(${params})` })
379
+ })
380
+ }
381
+
382
+ for (const n in supportI) {
383
+ if (actions.includes(n)) continue
384
+ const actor = supportI[n]
385
+ const params = getParamsToString(actor)
386
+ actionDetails.push({ helper: 'SupportObject', action: n, signature: `I.${n}(${params})` })
387
+ }
388
+
389
+ return { content: [{ type: 'text', text: JSON.stringify({ count: actionDetails.length, actions: actionDetails }, null, 2) }] }
390
+ }
391
+
392
+ case 'start_browser': {
393
+ const configPath = args?.config
394
+ if (browserStarted) {
395
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'Browser already started' }, null, 2) }] }
396
+ }
397
+ await initCodecept(configPath)
398
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'Browser started successfully' }, null, 2) }] }
399
+ }
400
+
401
+ case 'stop_browser': {
402
+ if (!containerInitialized) {
403
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'Browser not initialized' }, null, 2) }] }
404
+ }
405
+
406
+ const helpers = container.helpers()
407
+ for (const helperName in helpers) {
408
+ const helper = helpers[helperName]
409
+ try { if (helper._finish) await helper._finish() } catch {}
410
+ }
411
+
412
+ browserStarted = false
413
+ containerInitialized = false
414
+
415
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'Browser stopped successfully' }, null, 2) }] }
416
+ }
417
+
418
+ case 'run_code': {
419
+ const { code, timeout = 60000, config: configPath, saveArtifacts = true } = args
420
+ await initCodecept(configPath)
421
+
422
+ const I = container.support('I')
423
+ if (!I) throw new Error('I object not available. Make sure helpers are configured.')
424
+
425
+ const result = { status: 'unknown', output: '', error: null, artifacts: {} }
426
+
427
+ try {
428
+ const asyncFn = new Function('I', `return (async () => { ${code} })()`)
429
+ await Promise.race([
430
+ asyncFn(I),
431
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)),
432
+ ])
433
+
434
+ result.status = 'success'
435
+ result.output = 'Code executed successfully'
436
+
437
+ if (saveArtifacts) {
438
+ const helpers = container.helpers()
439
+ const helper = Object.values(helpers)[0]
440
+ if (helper) {
441
+ try {
442
+ if (helper.grabAriaSnapshot) result.artifacts.aria = await helper.grabAriaSnapshot()
443
+ if (helper.grabCurrentUrl) result.artifacts.url = await helper.grabCurrentUrl()
444
+ if (helper.grabBrowserLogs) result.artifacts.consoleLogs = (await helper.grabBrowserLogs()) || []
445
+ if (helper.grabSource) {
446
+ const html = await helper.grabSource()
447
+ result.artifacts.html = html.substring(0, 10000) + '...'
448
+ }
449
+ } catch (e) {
450
+ result.output += ` (Warning: ${e.message})`
451
+ }
452
+ }
453
+ }
454
+ } catch (error) {
455
+ result.status = 'failed'
456
+ result.error = error.message
457
+ result.output = error.stack || error.message
458
+ }
459
+
460
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
461
+ }
462
+
463
+ case 'run_test': {
464
+ return await withLock(async () => {
465
+ const { test, timeout = 60000, config: configPathArg } = args || {}
466
+ const { configPath, configDir } = resolveConfigPath(configPathArg)
467
+
468
+ const { cli, root } = findCodeceptCliUpwards(configDir)
469
+ const isNodeScript = cli.endsWith('.js')
470
+
471
+ const resolvedFile = await resolveTestToFile({ cli, root, configPath, test })
472
+ const runArgs = ['run', '--config', configPath, '--reporter', 'json']
473
+
474
+ if (resolvedFile) runArgs.push(resolvedFile)
475
+ else if (looksLikePath(test)) runArgs.push(test)
476
+ else runArgs.push('--grep', String(test))
477
+
478
+ const res = isNodeScript
479
+ ? await runCmd(process.execPath, [cli, ...runArgs], { cwd: root, timeout })
480
+ : await runCmd(cli, runArgs, { cwd: root, timeout })
481
+
482
+ const { code, out, err } = res
483
+
484
+ let parsed = null
485
+ const jsonStart = out.indexOf('{')
486
+ const jsonEnd = out.lastIndexOf('}')
487
+ if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) {
488
+ try { parsed = JSON.parse(out.slice(jsonStart, jsonEnd + 1)) } catch {}
489
+ }
490
+
491
+ return {
492
+ content: [{
493
+ type: 'text',
494
+ text: JSON.stringify({
495
+ meta: { exitCode: code, cli, root, configPath, args: runArgs, resolvedFile: resolvedFile || null },
496
+ reporterJson: parsed,
497
+ stderr: err ? err.slice(0, 20000) : '',
498
+ rawStdout: parsed ? '' : out.slice(0, 20000),
499
+ }, null, 2),
500
+ }],
501
+ }
502
+ })
503
+ }
504
+
505
+ case 'run_step_by_step': {
506
+ const { test, timeout = 60000, config: configPath } = args
507
+ await initCodecept(configPath)
508
+
509
+ return await withSilencedIO(async () => {
510
+ codecept.loadTests()
511
+
512
+ let testFiles = codecept.testFiles
513
+ if (test) {
514
+ const testName = normalizePath(test).toLowerCase()
515
+ testFiles = codecept.testFiles.filter(f => {
516
+ const filePath = normalizePath(f).toLowerCase()
517
+ return filePath.includes(testName) || filePath.endsWith(testName)
518
+ })
519
+ }
520
+
521
+ if (!testFiles.length) throw new Error(`No tests found matching: ${test}`)
522
+
523
+ const results = []
524
+ const currentSteps = {}
525
+ let currentTestTitle = null
526
+ const testFile = testFiles[0]
527
+
528
+ const onBefore = (t) => {
529
+ const traceDir = getTraceDir(t.title, t.file)
530
+ currentTestTitle = t.title
531
+ currentSteps[t.title] = []
532
+ results.push({
533
+ test: t.title,
534
+ file: t.file,
535
+ traceFile: `file://${resolvePath(traceDir, 'trace.md')}`,
536
+ status: 'running',
537
+ steps: [],
538
+ })
539
+ }
540
+
541
+ const onAfter = (t) => {
542
+ const r = results.find(x => x.test === t.title)
543
+ if (r) {
544
+ r.status = t.err ? 'failed' : 'completed'
545
+ if (t.err) r.error = t.err.message
546
+ }
547
+ currentTestTitle = null
548
+ }
549
+
550
+ const onStepAfter = (step) => {
551
+ if (!currentTestTitle || !currentSteps[currentTestTitle]) return
552
+ currentSteps[currentTestTitle].push({
553
+ step: step.toString(),
554
+ status: step.status,
555
+ time: step.endTime - step.startTime,
556
+ })
557
+ const r = results.find(x => x.test === currentTestTitle)
558
+ if (r) r.steps = [...currentSteps[currentTestTitle]]
559
+ }
560
+
561
+ event.dispatcher.on(event.test.before, onBefore)
562
+ event.dispatcher.on(event.test.after, onAfter)
563
+ event.dispatcher.on(event.step.after, onStepAfter)
564
+
565
+ try {
566
+ await Promise.race([
567
+ (async () => {
568
+ await codecept.bootstrap()
569
+ await codecept.run(testFile)
570
+ })(),
571
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)),
572
+ ])
573
+ } catch (error) {
574
+ const lastRunning = results.filter(r => r.status === 'running').pop()
575
+ if (lastRunning) {
576
+ lastRunning.status = 'failed'
577
+ lastRunning.error = error.message
578
+ }
579
+ } finally {
580
+ try { event.dispatcher.removeListener(event.test.before, onBefore) } catch {}
581
+ try { event.dispatcher.removeListener(event.test.after, onAfter) } catch {}
582
+ try { event.dispatcher.removeListener(event.step.after, onStepAfter) } catch {}
583
+ }
584
+
585
+ return { content: [{ type: 'text', text: JSON.stringify({ results, stepByStep: true }, null, 2) }] }
586
+ })
587
+ }
588
+
589
+ default:
590
+ throw new Error(`Unknown tool: ${name}`)
591
+ }
592
+ } catch (error) {
593
+ return {
594
+ content: [{ type: 'text', text: JSON.stringify({ error: error.message, stack: error.stack }, null, 2) }],
595
+ isError: true,
596
+ }
597
+ }
598
+ })
599
+
600
+ async function main() {
601
+ const transport = new StdioServerTransport()
602
+ await server.connect(transport)
603
+ }
604
+
605
+ main().catch((error) => {
606
+ import('fs').then(fs => {
607
+ const logFile = path.resolve(process.cwd(), 'mcp-server-error.log')
608
+ fs.appendFileSync(logFile, `${new Date().toISOString()} - ${error.stack}\n`)
609
+ })
610
+ })
@@ -1,11 +1,16 @@
1
1
  Appends text to a input field or textarea.
2
2
  Field is located by name, label, CSS or XPath
3
3
 
4
+ The third parameter is an optional context (CSS or XPath locator) to narrow the search.
5
+
4
6
  ```js
5
7
  I.appendField('#myTextField', 'appended');
6
8
  // typing secret
7
9
  I.appendField('password', secret('123456'));
10
+ // within a context
11
+ I.appendField('name', 'John', '.form-container');
8
12
  ```
9
13
  @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator
10
14
  @param {string} value text value to append.
15
+ @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
11
16
  @returns {void} automatically synchronized promise through #recorder
@@ -2,11 +2,23 @@ Attaches a file to element located by label, name, CSS or XPath
2
2
  Path to file is relative current codecept directory (where codecept.conf.ts or codecept.conf.js is located).
3
3
  File will be uploaded to remote system (if tests are running remotely).
4
4
 
5
+ The third parameter is an optional context (CSS or XPath locator) to narrow the search.
6
+
5
7
  ```js
6
8
  I.attachFile('Avatar', 'data/avatar.jpg');
7
9
  I.attachFile('form input[name=avatar]', 'data/avatar.jpg');
10
+ // within a context
11
+ I.attachFile('Avatar', 'data/avatar.jpg', '.form-container');
12
+ ```
13
+
14
+ If the locator points to a non-file-input element (e.g., a dropzone area),
15
+ the file will be dropped onto that element using drag-and-drop events.
16
+
17
+ ```js
18
+ I.attachFile('#dropzone', 'data/avatar.jpg');
8
19
  ```
9
20
 
10
21
  @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
11
22
  @param {string} pathToFile local file path relative to codecept.conf.ts or codecept.conf.js config file.
23
+ @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
12
24
  @returns {void} automatically synchronized promise through #recorder
@@ -1,7 +1,7 @@
1
1
  Selects a checkbox or radio button.
2
2
  Element is located by label or name or CSS or XPath.
3
3
 
4
- The second parameter is a context (CSS or XPath locator) to narrow the search.
4
+ The second parameter is an optional context (CSS or XPath locator) to narrow the search.
5
5
 
6
6
  ```js
7
7
  I.checkOption('#agree');
@@ -1,9 +1,14 @@
1
1
  Clears a `<textarea>` or text `<input>` element's value.
2
2
 
3
+ The second parameter is an optional context (CSS or XPath locator) to narrow the search.
4
+
3
5
  ```js
4
6
  I.clearField('Email');
5
7
  I.clearField('user[email]');
6
8
  I.clearField('#email');
9
+ // within a context
10
+ I.clearField('Email', '.form-container');
7
11
  ```
8
12
  @param {LocatorOrString} editable field located by label|name|CSS|XPath|strict locator.
13
+ @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
9
14
  @returns {void} automatically synchronized promise through #recorder.
@@ -1,8 +1,12 @@
1
1
  Opposite to `seeElement`. Checks that element is not visible (or in DOM)
2
2
 
3
+ The second parameter is a context (CSS or XPath locator) to narrow the search.
4
+
3
5
  ```js
4
6
  I.dontSeeElement('.modal'); // modal is not shown
7
+ I.dontSeeElement('.modal', '#container');
5
8
  ```
6
9
 
7
10
  @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|Strict locator.
11
+ @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
8
12
  @returns {void} automatically synchronized promise through #recorder