diff --git a/packages/app/src/components/sidebar/explorer.ts b/packages/app/src/components/sidebar/explorer.ts index a345ceb..4733e33 100644 --- a/packages/app/src/components/sidebar/explorer.ts +++ b/packages/app/src/components/sidebar/explorer.ts @@ -23,6 +23,7 @@ interface TestEntry { uid: string state?: string label: string + callSource?: string children: TestEntry[] } @@ -66,7 +67,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry { #renderEntry (entry: TestEntry): TemplateResult { return html` - + ${entry.children && entry.children.length ? html` @@ -116,6 +117,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry { : entry.tests.find((t) => t.state === 'failed') ? TestState.FAILED : TestState.PASSED, + callSource: (entry as any).callSource, children: Object.values(entries) .map(this.#getTestEntry.bind(this)) .filter(this.#filterEntry.bind(this)) @@ -129,6 +131,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry { : entry.state === 'failed' ? TestState.FAILED : TestState.PASSED, + callSource: (entry as any).callSource, children: [] } } diff --git a/packages/app/src/components/sidebar/test-suite.ts b/packages/app/src/components/sidebar/test-suite.ts index 34ac2a7..972f4ae 100644 --- a/packages/app/src/components/sidebar/test-suite.ts +++ b/packages/app/src/components/sidebar/test-suite.ts @@ -48,6 +48,9 @@ export class ExplorerTestEntry extends CollapseableEntry { @property({ type: String }) state?: TestState + @property({ type: String, attribute: 'call-source' }) + callSource?: string + static styles = [...Element.styles, css` :host { display: block; @@ -71,6 +74,13 @@ export class ExplorerTestEntry extends CollapseableEntry { this.requestUpdate() } + #viewSource() { + if (!this.callSource) return + window.dispatchEvent(new CustomEvent('app-source-highlight', { + detail: this.callSource + })) + } + get hasPassed () { return this.state === TestState.PASSED } @@ -126,7 +136,7 @@ export class ExplorerTestEntry extends CollapseableEntry { ` } - ${!hasNoChildren ? html` diff --git a/packages/app/src/components/workbench/source.ts b/packages/app/src/components/workbench/source.ts index 9d92ef0..457cdcf 100644 --- a/packages/app/src/components/workbench/source.ts +++ b/packages/app/src/components/workbench/source.ts @@ -24,6 +24,7 @@ export class DevtoolsSource extends Element { .cm-editor { width: 100%; + height: 100%; padding: 10px 0px; } .cm-content { @@ -63,20 +64,24 @@ export class DevtoolsSource extends Element { const editorView = new EditorView(opts) container.replaceWith(editorView.dom) - /** - * highlight line of call source - */ - const lines = [...(this.shadowRoot?.querySelectorAll('.cm-line') || [])] - if (highlightLine && lines.length && highlightLine < lines.length) { - setTimeout(() => { - lines[highlightLine].classList.add('cm-activeLine') - }, 100) + // Use CodeMirror API to select and scroll to the target line (1-based index) + if (highlightLine && highlightLine > 0) { + try { + const lineInfo = editorView.state.doc.line(highlightLine) // 1-based + requestAnimationFrame(() => { + editorView.dispatch({ + selection: { anchor: lineInfo.from }, + effects: EditorView.scrollIntoView(lineInfo.from, { y: 'center' }) // center the line + }) + }) + } catch { /* ignore */ } } } #highlightCallSource (ev: CustomEvent) { const [filePath, line] = ev.detail.split(':') - this.#renderEditor(filePath, parseInt(line, 10) + 1) + this.#renderEditor(filePath, parseInt(line, 10)) + this.closest('wdio-devtools-tabs')?.activateTab('Source') } render() { diff --git a/packages/service/package.json b/packages/service/package.json index 9d99b0b..6939adf 100644 --- a/packages/service/package.json +++ b/packages/service/package.json @@ -25,21 +25,26 @@ "test": "eslint ." }, "dependencies": { + "@babel/types": "^7.28.4", "@wdio/devtools-backend": "workspace:^", "@wdio/devtools-script": "workspace:^", "@wdio/logger": "9.18.0", "@wdio/reporter": "9.18.0", "@wdio/types": "9.16.2", "import-meta-resolve": "^4.1.0", - "stack-trace": "1.0.0-pre2", "ws": "^8.18.3" }, "license": "MIT", "devDependencies": { + "@babel/parser": "^7.28.4", + "@babel/traverse": "^7.28.4", + "@types/babel__core": "^7.20.5", + "@types/babel__traverse": "^7.28.0", "@types/stack-trace": "^0.0.33", "@types/ws": "^8.18.1", "@wdio/globals": "9.17.0", "@wdio/protocols": "9.16.2", + "stack-trace": "1.0.0-pre2", "vite-plugin-dts": "^4.5.4" }, "peerDependencies": { diff --git a/packages/service/src/constants.ts b/packages/service/src/constants.ts index c8bf0c0..dde972d 100644 --- a/packages/service/src/constants.ts +++ b/packages/service/src/constants.ts @@ -8,9 +8,9 @@ export const DEFAULT_LAUNCH_CAPS: WebdriverIO.Capabilities = { browserName: 'chrome', 'goog:chromeOptions': { // production: - // args: ['--window-size=1200,800'] - // development: args: ['--window-size=1600,1200'] + // development: + // args: ['--window-size=1600,1200', '--auto-open-devtools-for-tabs'] } } @@ -25,4 +25,52 @@ export const CONTEXT_CHANGE_COMMANDS = [ 'url', 'back', 'forward', 'refresh', 'switchFrame', 'newWindow', 'createWindow', 'closeWindow' ] +/** + * Existing pattern (kept for any external consumers) + */ export const SPEC_FILE_PATTERN = /(test|spec|features)[\\/].*\.(js|ts)$/i + +/** + * Parser options + */ +export const PARSE_PLUGINS = [ + 'typescript', + 'jsx', + 'decorators-legacy', + 'classProperties', + 'dynamicImport' +] as const + +/** + * Test framework identifiers + */ +export const TEST_FN_NAMES = ['it', 'test', 'specify', 'fit', 'xit'] as const +export const SUITE_FN_NAMES = ['describe', 'context', 'suite'] as const +export const STEP_FN_NAMES = ['Given', 'When', 'Then', 'And', 'But', 'defineStep'] as const + +/** + * File/type recognizers + */ +export const STEP_FILE_RE = /\.(?:steps?)\.[cm]?[jt]sx?$/i +export const STEP_DIR_RE = /(?:^|\/)(?:step[-_]?definitions|steps)\/.+\.[cm]?[jt]sx?$/i +export const SPEC_FILE_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/i +export const FEATURE_FILE_RE = /\.feature$/i +export const SOURCE_FILE_EXT_RE = /\.(?:[cm]?js|[cm]?ts)x?$/ + +/** + * Gherkin Feature/Scenario line + */ +export const FEATURE_OR_SCENARIO_LINE_RE = /^\s*(Feature|Scenario(?: Outline)?):\s*(.+)\s*$/i + +/** + * Step definition textual scan regexes + */ +export const STEP_DEF_REGEX_LITERAL_RE = /\b(Given|When|Then|And|But)\s*\(\s*(\/(?:\\.|[^/\\])+\/[gimsuy]*)/ +export const STEP_DEF_STRING_RE = /\b(Given|When|Then|And|But)\s*\(\s*(['`])([^'`\\]*(?:\\.[^'`\\]*)*)\2/ + +/** + * Step directories discovery + */ +export const STEPS_DIR_CANDIDATES = ['step-definitions', 'step_definitions', 'steps'] as const +export const STEPS_DIR_ASCENT_MAX = 6 +export const STEPS_GLOBAL_SEARCH_MAX_DEPTH = 5 diff --git a/packages/service/src/reporter.ts b/packages/service/src/reporter.ts index 779de41..10e5ca1 100644 --- a/packages/service/src/reporter.ts +++ b/packages/service/src/reporter.ts @@ -1,7 +1,10 @@ import WebdriverIOReporter, { type SuiteStats, type TestStats } from '@wdio/reporter' +import { mapTestToSource, setCurrentSpecFile, mapSuiteToSource } from './utils.js' export class TestReporter extends WebdriverIOReporter { #report: (data: any) => void + #currentSpecFile?: string + #suitePath: string[] = [] constructor (options: any, report: (data: any) => void) { super(options) @@ -10,10 +13,27 @@ export class TestReporter extends WebdriverIOReporter { onSuiteStart(suiteStats: SuiteStats): void { super.onSuiteStart(suiteStats) + this.#currentSpecFile = suiteStats.file + setCurrentSpecFile(suiteStats.file) + + // Push title if non-empty + if (suiteStats.title) this.#suitePath.push(suiteStats.title) + + // Enrich and set callSource for suites + mapSuiteToSource(suiteStats as any, this.#currentSpecFile, this.#suitePath) + if ((suiteStats as any).file && (suiteStats as any).line != null) { + ;(suiteStats as any).callSource = `${(suiteStats as any).file}:${(suiteStats as any).line}` + } + this.#sendUpstream() } onTestStart(testStats: TestStats): void { + // Enrich testStats with callSource info + mapTestToSource(testStats, this.#currentSpecFile) + if ((testStats as any).file && (testStats as any).line != null) { + ;(testStats as any).callSource = `${(testStats as any).file}:${(testStats as any).line}` + } super.onTestStart(testStats) this.#sendUpstream() } @@ -25,6 +45,15 @@ export class TestReporter extends WebdriverIOReporter { onSuiteEnd(suiteStats: SuiteStats): void { super.onSuiteEnd(suiteStats) + // Pop the suite we pushed on start + if (suiteStats.title && this.#suitePath[this.#suitePath.length - 1] === suiteStats.title) { + this.#suitePath.pop() + } + // Only clear when the last suite ends + if (this.#suitePath.length === 0) { + this.#currentSpecFile = undefined + setCurrentSpecFile(undefined) + } this.#sendUpstream() } diff --git a/packages/service/src/types.ts b/packages/service/src/types.ts index 1eac4a9..7f5ff3e 100644 --- a/packages/service/src/types.ts +++ b/packages/service/src/types.ts @@ -66,3 +66,15 @@ declare module WebdriverIO { interface ServiceOption extends ServiceOptions {} interface Capabilities {} } + +declare module '@wdio/reporter' { + interface TestStats { + file?: string + line?: number + column?: number + } + + interface SuiteStats { + line?: string + } +} diff --git a/packages/service/src/utils.ts b/packages/service/src/utils.ts index 5f4ea57..36c881c 100644 --- a/packages/service/src/utils.ts +++ b/packages/service/src/utils.ts @@ -1,4 +1,604 @@ +import fs from 'fs' +import path from 'node:path' +import { createRequire } from 'node:module' +import { parse } from '@babel/parser' +import * as babelTraverse from '@babel/traverse' +import type { NodePath } from '@babel/traverse' +import type { CallExpression } from '@babel/types' + +import { + PARSE_PLUGINS, + TEST_FN_NAMES, + SUITE_FN_NAMES, + STEP_FN_NAMES, + STEP_FILE_RE, + STEP_DIR_RE, + SPEC_FILE_RE, + FEATURE_FILE_RE, + FEATURE_OR_SCENARIO_LINE_RE, + STEP_DEF_REGEX_LITERAL_RE, + STEP_DEF_STRING_RE, + SOURCE_FILE_EXT_RE, + STEPS_DIR_CANDIDATES, + STEPS_DIR_ASCENT_MAX, + STEPS_GLOBAL_SEARCH_MAX_DEPTH +} from './constants.js' + +const require = createRequire(import.meta.url) +const stackTrace = require('stack-trace') as typeof import('stack-trace') +const _astCache = new Map() + +let CE: { CucumberExpression: any, ParameterTypeRegistry: any } | undefined +try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const ce = require('@cucumber/cucumber-expressions') + CE = { CucumberExpression: ce.CucumberExpression, ParameterTypeRegistry: ce.ParameterTypeRegistry } +} catch { /* optional */ } + +const traverse: (typeof import('@babel/traverse'))['default'] = + (babelTraverse as any).default ?? (babelTraverse as any) + +/** + * Track current spec file (set by reporter) + */ +let CURRENT_SPEC_FILE: string | undefined +export function setCurrentSpecFile(file?: string) { + CURRENT_SPEC_FILE = file +} + +/** + * Get the top-level browser object from an element/browser + */ export function getBrowserObject (elem: WebdriverIO.Element | WebdriverIO.Browser): WebdriverIO.Browser { const elemObject = elem as WebdriverIO.Element - return (elemObject as WebdriverIO.Element).parent ? getBrowserObject(elemObject.parent) : elem as WebdriverIO.Browser + return (elemObject as WebdriverIO.Element).parent + ? getBrowserObject(elemObject.parent) + : (elem as WebdriverIO.Browser) +} + +/** + * Get root callee name (handles Identifier and MemberExpression like it.only) + */ +function rootCalleeName(callee: any): string | undefined { + if (!callee) return + if (callee.type === 'Identifier') return callee.name + if (callee.type === 'MemberExpression') { + const obj: any = callee.object + return obj && obj.type === 'Identifier' ? obj.name : undefined + } + return +} + +/** + * Parse a JS/TS test/spec file to collect suite/test calls (Mocha/Jasmine) with full title path + */ +export function findTestLocations(filePath: string) { + if (!fs.existsSync(filePath)) return [] + + const src = fs.readFileSync(filePath, 'utf-8') + const ast = parse(src, { + sourceType: 'module', + plugins: PARSE_PLUGINS as any, + errorRecovery: true, + allowReturnOutsideFunction: true, + }) + + type Loc = { + type: 'test' | 'suite' + name: string + titlePath: string[] + line?: number + column?: number + } + + const out: Loc[] = [] + const suiteStack: string[] = [] + + const isSuite = (n?: string) => !!n && (SUITE_FN_NAMES as readonly string[]).includes(n) || n === 'Feature' + const isTest = (n?: string) => !!n && (TEST_FN_NAMES as readonly string[]).includes(n) + + const staticTitle = (node: any): string | undefined => { + if (!node) return + if (node.type === 'StringLiteral') return node.value + if (node.type === 'TemplateLiteral' && node.expressions.length === 0) { + return node.quasis.map((q: any) => q.value.cooked).join('') + } + return + } + + traverse(ast, { + enter(p) { + if (!p.isCallExpression()) return + const callee: any = p.node.callee + const root = rootCalleeName(callee) + if (!root) return + + if (isSuite(root)) { + const ttl = staticTitle(p.node.arguments?.[0] as any) + if (ttl) { + out.push({ + type: 'suite', + name: ttl, + titlePath: [...suiteStack, ttl], + line: p.node.loc?.start.line, + column: p.node.loc?.start.column + }) + suiteStack.push(ttl) + } + } else if (isTest(root)) { + const ttl = staticTitle(p.node.arguments?.[0] as any) + if (ttl) { + out.push({ + type: 'test', + name: ttl, + titlePath: [...suiteStack, ttl], + line: p.node.loc?.start.line, + column: p.node.loc?.start.column + }) + } + } + }, + exit(p) { + if (!p.isCallExpression()) return + const callee: any = p.node.callee + const root = rootCalleeName(callee) + if (!root || !isSuite(root)) return + const ttl = ((): string | undefined => { + const a0: any = p.node.arguments?.[0] + if (a0?.type === 'StringLiteral') return a0.value + if (a0?.type === 'TemplateLiteral' && a0.expressions.length === 0) { + return a0.quasis.map((q: any) => q.value.cooked).join('') + } + return + })() + if (ttl && suiteStack[suiteStack.length - 1] === ttl) { + suiteStack.pop() + } + } + }) + + return out +} + +/** + * Capture stack trace and try to find a user frame. + * Prefer step-definition files, then spec/tests, then feature files. + */ +export function getCurrentTestLocation() { + const frames = stackTrace.parse(new Error()) + + const pick = (predicate: (f: any) => boolean) => { + const f = frames.find((fr) => { + const fn = fr.getFileName() + return !!fn && !fn.includes('node_modules') && predicate(fr) + }) + return f + ? { + file: f.getFileName() as string, + line: f.getLineNumber() as number, + column: f.getColumnNumber() as number + } + : null + } + + const step = pick((fr) => { + const fn = fr.getFileName() as string + return STEP_FILE_RE.test(fn) || STEP_DIR_RE.test(fn) + }) + if (step) return step + + const spec = pick((fr) => SPEC_FILE_RE.test(fr.getFileName() as string)) + if (spec) return spec + + const feature = pick((fr) => FEATURE_FILE_RE.test(fr.getFileName() as string)) + if (feature) return feature + + return null +} + +/** + * Step-definition discovery and matching (Cucumber) + */ +type StepDef = { + kind: 'regex' | 'string' | 'expression' + keyword?: string + text?: string + regex?: RegExp + expr?: any + file: string + line: number + column: number +} + +// Look for step-definitions directory by ascending from a base directory +function _findStepsDir(startDir: string): string | undefined { + let dir = startDir + for (let i = 0; i < STEPS_DIR_ASCENT_MAX; i++) { + for (const c of STEPS_DIR_CANDIDATES) { + const p = path.join(dir, c) + if (fs.existsSync(p) && fs.statSync(p).isDirectory()) return p + } + const up = path.dirname(dir) + if (up === dir) break + dir = up + } + return undefined +} + +// Global fallback (find a features/*/(step-definitions|steps) directory under cwd) +let _globalStepsDir: string | undefined +function _findStepsDirGlobal(): string | undefined { + if (_globalStepsDir && fs.existsSync(_globalStepsDir)) return _globalStepsDir + + const root = process.cwd() + const queue: { dir: string; depth: number }[] = [{ dir: root, depth: 0 }] + const maxDepth = STEPS_GLOBAL_SEARCH_MAX_DEPTH + while (queue.length) { + const { dir, depth } = queue.shift()! + if (depth > maxDepth) continue + + // Look for a features folder here + const featuresDir = path.join(dir, 'features') + if (fs.existsSync(featuresDir) && fs.statSync(featuresDir).isDirectory()) { + for (const c of STEPS_DIR_CANDIDATES) { + const p = path.join(featuresDir, c) + if (fs.existsSync(p) && fs.statSync(p).isDirectory()) { + _globalStepsDir = p + return p + } + } + } + + // BFS into subdirs + for (const entry of fs.readdirSync(dir)) { + if (entry.startsWith('.')) continue + const full = path.join(dir, entry) + let st: fs.Stats + try { st = fs.statSync(full) } catch { continue } + if (st.isDirectory() && !full.includes('node_modules')) { + queue.push({ dir: full, depth: depth + 1 }) + } + } + } + return undefined +} + +// Recursively list all source files in a directory +function _listFiles(dir: string): string[] { + const out: string[] = [] + for (const entry of fs.readdirSync(dir)) { + const full = path.join(dir, entry) + const st = fs.statSync(full) + if (st.isDirectory()) out.push(..._listFiles(full)) + else if (SOURCE_FILE_EXT_RE.test(entry)) out.push(full) + } + return out +} + +// Text fallback: scan a file for step definitions on a single line +function _collectStepDefsFromText(file: string): StepDef[] { + const out: StepDef[] = [] + const src = fs.readFileSync(file, 'utf-8') + const lines = src.split(/\r?\n/) + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + // Regex step: Given(/^...$/i, ...) + const mRe = line.match(STEP_DEF_REGEX_LITERAL_RE) + if (mRe) { + const lit = mRe[2] // like /pattern/flags + const lastSlash = lit.lastIndexOf('/') + const pattern = lit.slice(1, lastSlash) + const flags = lit.slice(lastSlash + 1) + try { + out.push({ + kind: 'regex', + regex: new RegExp(pattern, flags), + file, + line: i + 1, + column: mRe.index ?? 0 + }) + continue + } catch { + // ignore malformed regex + } + } + // String step: Given('I do X', ...) + const mStr = line.match(STEP_DEF_STRING_RE) + if (mStr) { + const keyword = mStr[1] + const text = mStr[3] + out.push({ + kind: 'string', + keyword, + text, + file, + line: i + 1, + column: mStr.index ?? 0 + }) + } + } + return out +} + +const _stepsCache = new Map() +function _collectStepDefs(stepsDir: string): StepDef[] { + const cached = _stepsCache.get(stepsDir) + if (cached) return cached + + const files = _listFiles(stepsDir) + const defs: StepDef[] = [] + + for (const file of files) { + let pushed = 0 + try { + const src = fs.readFileSync(file, 'utf-8') + const ast = parse(src, { sourceType: 'module', plugins: PARSE_PLUGINS as any, errorRecovery: true }) + + traverse(ast, { + CallExpression(p: NodePath) { + const callee: any = p.node.callee + // Support Identifier (Given(...)) and MemberExpression (cucumber.Given(...)) + let name: string | undefined + if (callee?.type === 'Identifier') { + name = callee.name + } else if (callee?.type === 'MemberExpression') { + const prop = (callee as any).property + if (prop?.type === 'Identifier') name = prop.name + } + if (!name || !(STEP_FN_NAMES as readonly string[]).includes(name)) return + + const arg = p.node.arguments?.[0] as any + const loc = { file, line: p.node.loc?.start.line ?? 1, column: p.node.loc?.start.column ?? 0 } + + if (arg?.type === 'RegExpLiteral') { + defs.push({ kind: 'regex', regex: new RegExp(arg.pattern, arg.flags ?? ''), ...loc }) + pushed++ + } else if (arg?.type === 'StringLiteral') { + // If Cucumber Expressions is available and pattern contains {...}, treat as expression + if (CE && arg.value.includes('{')) { + const expr = new CE!.CucumberExpression(arg.value, new CE!.ParameterTypeRegistry()) + defs.push({ kind: 'expression', expr, ...loc }) + } else { + defs.push({ kind: 'string', keyword: name, text: arg.value, ...loc }) + } + pushed++ + } + } + }) + } catch { + // ignore AST parse errors; fallback below + } + // If AST found nothing, fallback to text scan for this file + if (pushed === 0) { + const fromText = _collectStepDefsFromText(file) + if (fromText.length) { + defs.push(...fromText) + } + } + } + + _stepsCache.set(stepsDir, defs) + return defs +} + +function findStepDefinitionLocation(stepTitle: string, hintPath?: string) { + const baseDir = hintPath + ? (path.extname(hintPath) ? path.dirname(hintPath) : hintPath) + : undefined + + let stepsDir = baseDir ? _findStepsDir(baseDir) : undefined + if (!stepsDir) stepsDir = _findStepsDirGlobal() + if (!stepsDir) return + + const defs = _collectStepDefs(stepsDir) + + const title = String(stepTitle ?? '').trim() + const titleNoKw = title.replace(/^(Given|When|Then|And|But)\s+/i, '').trim() + + // String match + const s = defs.find(d => + d.kind === 'string' && + (titleNoKw.localeCompare(d.text!, 'en', { sensitivity: 'base' }) === 0 || + title.localeCompare(`${d.keyword} ${d.text}`, 'en', { sensitivity: 'base' }) === 0) + ) + if (s) return { file: s.file, line: s.line, column: s.column } + + // Cucumber expression match + const e = defs.find(d => d.kind === 'expression' && (() => { + try { return !!d.expr!.match(titleNoKw) || !!d.expr!.match(title) } catch { return false } + })()) + if (e) return { file: e.file, line: e.line, column: e.column } + + // Regex match + const r = defs.find(d => + d.kind === 'regex' && (d.regex!.test(titleNoKw) || d.regex!.test(title)) + ) + if (r) return { file: r.file, line: r.line, column: r.column } + + return +} + +/** + * Helpers for Mocha/Jasmine mapping + */ +function normalizeFullTitle(full?: string) { + return String(full || '') + .replace(/^\d+:\s*/, '') // drop worker prefix like "0: " + .replace(/\s+/g, ' ') + .trim() +} + +function escapeRegExp(s: string) { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +function offsetToLineCol(src: string, offset: number) { + let line = 1, col = 1 + for (let i = 0; i < offset && i < src.length; i++) { + if (src.charCodeAt(i) === 10) { line++; col = 1 } else { col++ } + } + return { line, column: col } +} + +/** + * Textual fallback: find the test by scanning for it/test/specify(...) with the exact title. + * Works even if Babel AST couldn’t be built or callee is wrapped. + */ +function findTestLocationByText(file: string, title: string) { + try { + const src = fs.readFileSync(file, 'utf-8') + const q = `(['"\`])${escapeRegExp(title)}\\1` + const call = String.raw`\b(?:${(TEST_FN_NAMES as readonly string[]).join('|')})\s*\(\s*${q}` + const re = new RegExp(call) + const m = re.exec(src) + if (m && typeof m.index === 'number') { + const { line, column } = offsetToLineCol(src, m.index) + return { file, line, column } + } + } catch {} + return undefined +} + +// Find describe/context/suite("", ...) by text as a fallback +function findSuiteLocationByText(file: string, title: string) { + try { + const src = fs.readFileSync(file, 'utf-8') + const q = `(['"\`])${escapeRegExp(title)}\\1` + const call = String.raw`\b(?:${(SUITE_FN_NAMES as readonly string[]).join('|')})\s*\(\s*${q}` + const re = new RegExp(call) + const m = re.exec(src) + if (m && typeof m.index === 'number') { + const { line, column } = offsetToLineCol(src, m.index) + return { file, line, column } + } + } catch {} + return undefined +} + +/** + * Enrich stats: + * - Cucumber: prefer step-definition file/line + * - Mocha/Jasmine: AST with suite path; fallback to runtime stack + */ +export function mapTestToSource(testStats: any, hintFile?: string) { + const title = String(testStats?.title ?? '').trim() + const fullTitle = normalizeFullTitle(testStats?.fullTitle) + + // Hint for locating related files + const hint = + (Array.isArray((testStats as any).specs) ? (testStats as any).specs[0] : undefined) || + (testStats as any).file || + (testStats as any).specFile || + hintFile || + CURRENT_SPEC_FILE + + // Cucumber-like step: resolve step-definition location + if (/^(Given|When|Then|And|But)\b/i.test(title)) { + const stepLoc = findStepDefinitionLocation(title, FEATURE_FILE_RE.test(String(hint)) ? hint : undefined) + if (stepLoc) { + Object.assign(testStats, stepLoc) + return + } + } + + // Mocha/Jasmine static mapping via AST + const file = + (testStats as any).file || + (Array.isArray((testStats as any).specs) ? (testStats as any).specs[0] : undefined) || + (testStats as any).specFile || + hintFile || + CURRENT_SPEC_FILE + + if (file && !FEATURE_FILE_RE.test(file)) { + if (!_astCache.has(file)) { + try { + _astCache.set(file, findTestLocations(file)) + } catch { + // ignore parse errors + } + } + const locs = _astCache.get(file) as any[] | undefined + if (locs?.length) { + let match = + locs.find(l => l.type === 'test' && l.name === title && fullTitle.includes(l.titlePath.join(' '))) || + locs.find(l => l.type === 'test' && l.name === title) + + if (match) { + Object.assign(testStats, { file, line: match.line, column: match.column }) + return + } + } + + // Fallback: plain text search for it/test/specify("<title>") + const textLoc = findTestLocationByText(file, title) + if (textLoc) { + Object.assign(testStats, textLoc) + return + } + } + + // Runtime stack fallback + const runtimeLoc = getCurrentTestLocation() + if (runtimeLoc) { + Object.assign(testStats, runtimeLoc) + } +} + +/** + * Enrich a suite with file + line + * - Mocha/Jasmine: map "describe/context" by title path using AST + * - Cucumber: find Feature/Scenario line in .feature file + */ +export function mapSuiteToSource( + suiteStats: any, + hintFile?: string, + suitePath: string[] = [] +) { + const title = String(suiteStats?.title ?? '').trim() + const file = (suiteStats as any).file || hintFile || CURRENT_SPEC_FILE + if (!title || !file) return + + // Cucumber: feature/scenario line + if (FEATURE_FILE_RE.test(file)) { + try { + const src = fs.readFileSync(file, 'utf-8').split(/\r?\n/) + const norm = (s: string) => s.trim().replace(/\s+/g, ' ') + const want = norm(title) + for (let i = 0; i < src.length; i++) { + const m = src[i].match(FEATURE_OR_SCENARIO_LINE_RE) + if (m && norm(m[2]) === want) { + Object.assign(suiteStats, { file, line: i + 1, column: 1 }) + return + } + } + } catch {} + return + } + + // Mocha/Jasmine: AST first + try { + if (!_astCache.has(file)) _astCache.set(file, findTestLocations(file)) + const locs = _astCache.get(file) as any[] | undefined + if (locs?.length) { + const match = + locs.find(l => l.type === 'suite' + && Array.isArray(l.titlePath) + && l.titlePath.length === suitePath.length + && l.titlePath.every((t: string, i: number) => t === suitePath[i])) || + locs.find(l => l.type === 'suite' && l.titlePath.at(-1) === title) + + if (match?.line) { + Object.assign(suiteStats, { file, line: match.line, column: match.column }) + return + } + } + } catch { + // ignore + } + + // Fallback: text search + const textLoc = findSuiteLocationByText(file, title) + if (textLoc) { + Object.assign(suiteStats, textLoc) + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f607aef..3ba32c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,9 +27,6 @@ importers: '@vitest/browser': specifier: ^3.2.4 version: 3.2.4(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.0))(vitest@3.2.4)(webdriverio@9.19.1(puppeteer-core@21.11.0)) - '@wdio/mocha-framework': - specifier: ^9.19.2 - version: 9.19.2 autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) @@ -93,9 +90,6 @@ importers: '@wdio/local-runner': specifier: 9.18.0 version: 9.18.0(@wdio/globals@9.17.0)(webdriverio@9.19.1(puppeteer-core@21.11.0)) - '@wdio/logger': - specifier: 9.18.0 - version: 9.18.0 '@wdio/spec-reporter': specifier: 9.18.0 version: 9.18.0 @@ -158,7 +152,7 @@ importers: specifier: ^4.1.12 version: 4.1.12 '@wdio/devtools-service': - specifier: file:/Users/vishnu.p@browserstack.com/Documents/devtools/packages/service + specifier: workspace:* version: link:../service '@wdio/reporter': specifier: 9.18.0 @@ -194,7 +188,7 @@ importers: specifier: ^11.2.0 version: 11.2.0 '@wdio/devtools-app': - specifier: file:/Users/vishnu.p@browserstack.com/Documents/devtools/packages/app + specifier: workspace:^ version: link:../app '@wdio/logger': specifier: 9.18.0 @@ -236,11 +230,14 @@ importers: packages/service: dependencies: + '@babel/types': + specifier: ^7.28.4 + version: 7.28.4 '@wdio/devtools-backend': - specifier: file:/Users/vishnu.p@browserstack.com/Documents/devtools/packages/backend + specifier: workspace:^ version: link:../backend '@wdio/devtools-script': - specifier: file:/Users/vishnu.p@browserstack.com/Documents/devtools/packages/script + specifier: workspace:^ version: link:../script '@wdio/logger': specifier: 9.18.0 @@ -257,9 +254,6 @@ importers: import-meta-resolve: specifier: ^4.1.0 version: 4.1.0 - stack-trace: - specifier: 1.0.0-pre2 - version: 1.0.0-pre2 webdriverio: specifier: ^9.19.1 version: 9.19.1(puppeteer-core@21.11.0) @@ -267,6 +261,18 @@ importers: specifier: ^8.18.3 version: 8.18.3 devDependencies: + '@babel/parser': + specifier: ^7.28.4 + version: 7.28.4 + '@babel/traverse': + specifier: ^7.28.4 + version: 7.28.4 + '@types/babel__core': + specifier: ^7.20.5 + version: 7.20.5 + '@types/babel__traverse': + specifier: ^7.28.0 + version: 7.28.0 '@types/stack-trace': specifier: ^0.0.33 version: 0.0.33 @@ -279,6 +285,9 @@ importers: '@wdio/protocols': specifier: 9.16.2 version: 9.16.2 + stack-trace: + specifier: 1.0.0-pre2 + version: 1.0.0-pre2 vite-plugin-dts: specifier: ^4.5.4 version: 4.5.4(@types/node@24.3.0)(rollup@4.47.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.0)) @@ -320,6 +329,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/runtime@7.28.3': resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} engines: {node: '>=6.9.0'} @@ -332,8 +346,12 @@ packages: resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.2': - resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} '@codemirror/autocomplete@6.18.6': @@ -1397,6 +1415,18 @@ packages: '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -1421,9 +1451,6 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/mocha@10.0.10': - resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} - '@types/node@20.19.11': resolution: {integrity: sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==} @@ -1651,10 +1678,6 @@ packages: resolution: {integrity: sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==} engines: {node: '>=18.20.0'} - '@wdio/mocha-framework@9.19.2': - resolution: {integrity: sha512-aW08G8OjQfKNCiaLw74Qzl7/UiJBEcd9osxB8ZoMlsL9Aem2FqN523X+b92kk6hQQSfZporstulP3HPBRatVqw==} - engines: {node: '>=18.20.0'} - '@wdio/protocols@8.40.3': resolution: {integrity: sha512-wK7+eyrB3TAei8RwbdkcyoNk2dPu+mduMBOdPJjp8jf/mavd15nIUXLID1zA+w5m1Qt1DsT1NbvaeO9+aJQ33A==} @@ -1692,10 +1715,6 @@ packages: resolution: {integrity: sha512-Q1HVcXiWMHp3ze2NN1BvpsfEh/j6GtAeMHhHW4p2IWUfRZlZqTfiJ+95LmkwXOG2gw9yndT8NkJigAz8v7WVYQ==} engines: {node: '>=18.20.0'} - '@wdio/types@9.19.2': - resolution: {integrity: sha512-fBI7ljL+YcPXSXUhdk2+zVuz7IYP1aDMTq1eVmMme9GY0y67t0dCNPOt6xkCAEdL5dOcV6D2L1r6Cf/M2ifTvQ==} - engines: {node: '>=18.20.0'} - '@wdio/utils@8.41.0': resolution: {integrity: sha512-0TcTjBiax1VxtJQ/iQA0ZyYOSHjjX2ARVmEI0AMo9+AuIq+xBfnY561+v8k9GqOMPKsiH/HrK3xwjx8xCVS03g==} engines: {node: ^16.13 || >=18} @@ -1708,10 +1727,6 @@ packages: resolution: {integrity: sha512-wWx5uPCgdZQxFIemAFVk/aa3JLwqrTsvEJsPlV3lCRpLeQ67V8aUPvvNAzE+RhX67qvelwwsvX8RrPdLDfnnYw==} engines: {node: '>=18.20.0'} - '@wdio/utils@9.19.2': - resolution: {integrity: sha512-caimJiTsxDUfXn/gRAzcYTO3RydSl7XzD+QpjfWZYJjzr8a2XfNnj+Vdmr8gG4BSkiVHirW9mFCZeQp2eTD7rA==} - engines: {node: '>=18.20.0'} - '@zip.js/zip.js@2.7.72': resolution: {integrity: sha512-3/A4JwrgkvGBlCxtItjxs8HrNbuTAAl/zlGkV6tC5Fb5k5nk4x2Dqxwl/YnUys5Ch+QB01eJ8Q5K/J2uXfy9Vw==} engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=16.5.0'} @@ -1772,10 +1787,6 @@ packages: alien-signals@0.4.14: resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} - ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -1987,9 +1998,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browser-stdout@1.3.1: - resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} - browserslist@4.25.3: resolution: {integrity: sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -2050,10 +2058,6 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - caniuse-lite@1.0.30001736: resolution: {integrity: sha512-ImpN5gLEY8gWeqfLUyEF4b7mYWcYoR2Si1VhnrbM4JizRFmfGaAQ12PhNykq6nvI4XvKLrsp8Xde74D5phJOSw==} @@ -2138,9 +2142,6 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} - cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -2337,10 +2338,6 @@ packages: supports-color: optional: true - decamelize@4.0.0: - resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} - engines: {node: '>=10'} - decamelize@6.0.1: resolution: {integrity: sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2401,10 +2398,6 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} - diff@5.2.0: - resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} - engines: {node: '>=0.3.1'} - diff@8.0.2: resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} engines: {node: '>=0.3.1'} @@ -2830,10 +2823,6 @@ packages: flat-cache@6.1.13: resolution: {integrity: sha512-gmtS2PaUjSPa4zjObEIn4WWliKyZzYljgxODBfxugpK6q6HU9ClXzgCJ+nlcPKY9Bt090ypTOLIFWkV0jbKFjw==} - flat@5.0.2: - resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} - hasBin: true - flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -2945,11 +2934,6 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported - glob@8.1.0: - resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} - engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported - global-dirs@3.0.1: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} @@ -3251,10 +3235,6 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} - is-plain-obj@2.1.0: - resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} - engines: {node: '>=8'} - is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -3295,10 +3275,6 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} - is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - is-unicode-supported@2.1.0: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} @@ -3620,10 +3596,6 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} - loglevel-plugin-prefix@0.8.4: resolution: {integrity: sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==} @@ -3753,11 +3725,6 @@ packages: mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} - mocha@10.8.2: - resolution: {integrity: sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==} - engines: {node: '>= 14.0.0'} - hasBin: true - mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -4233,9 +4200,6 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -4458,9 +4422,6 @@ packages: resolution: {integrity: sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==} engines: {node: '>=18'} - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - set-cookie-parser@2.7.1: resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} @@ -5267,9 +5228,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - workerpool@6.5.1: - resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} - wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -5333,22 +5291,10 @@ packages: engines: {node: '>= 14.6'} hasBin: true - yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} - yargs-unparser@2.0.0: - resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} - engines: {node: '>=10'} - - yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} - yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -5403,7 +5349,7 @@ snapshots: '@babel/generator@7.28.3': dependencies: '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.30 jsesc: 3.1.0 @@ -5416,15 +5362,19 @@ snapshots: '@babel/parser@7.28.3': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 '@babel/runtime@7.28.3': {} '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 '@babel/traverse@7.28.3': dependencies: @@ -5433,12 +5383,24 @@ snapshots: '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.3 '@babel/template': 7.27.2 - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 debug: 4.4.1 transitivePeerDependencies: - supports-color - '@babel/types@7.28.2': + '@babel/types@7.28.4': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 @@ -6447,6 +6409,27 @@ snapshots: '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.4 + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 @@ -6469,8 +6452,6 @@ snapshots: '@types/json5@0.0.29': {} - '@types/mocha@10.0.10': {} - '@types/node@20.19.11': dependencies: undici-types: 6.21.0 @@ -6692,7 +6673,7 @@ snapshots: '@vue/compiler-core@3.5.19': dependencies: - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.4 '@vue/shared': 3.5.19 entities: 4.5.0 estree-walker: 2.0.2 @@ -6857,18 +6838,6 @@ snapshots: safe-regex2: 5.0.0 strip-ansi: 7.1.0 - '@wdio/mocha-framework@9.19.2': - dependencies: - '@types/mocha': 10.0.10 - '@types/node': 20.19.11 - '@wdio/logger': 9.18.0 - '@wdio/types': 9.19.2 - '@wdio/utils': 9.19.2 - mocha: 10.8.2 - transitivePeerDependencies: - - bare-buffer - - supports-color - '@wdio/protocols@8.40.3': {} '@wdio/protocols@9.16.2': {} @@ -6924,10 +6893,6 @@ snapshots: dependencies: '@types/node': 20.19.11 - '@wdio/types@9.19.2': - dependencies: - '@types/node': 20.19.11 - '@wdio/utils@8.41.0': dependencies: '@puppeteer/browsers': 1.9.1 @@ -6987,26 +6952,6 @@ snapshots: - bare-buffer - supports-color - '@wdio/utils@9.19.2': - dependencies: - '@puppeteer/browsers': 2.10.7 - '@wdio/logger': 9.18.0 - '@wdio/types': 9.19.2 - decamelize: 6.0.1 - deepmerge-ts: 7.1.5 - edgedriver: 6.1.2 - geckodriver: 5.0.0 - get-port: 7.1.0 - import-meta-resolve: 4.1.0 - locate-app: 2.5.0 - mitt: 3.0.1 - safaridriver: 1.0.0 - split2: 4.2.0 - wait-port: 1.1.0 - transitivePeerDependencies: - - bare-buffer - - supports-color - '@zip.js/zip.js@2.7.72': {} abort-controller@3.0.0: @@ -7069,8 +7014,6 @@ snapshots: alien-signals@0.4.14: {} - ansi-colors@4.1.3: {} - ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -7290,8 +7233,6 @@ snapshots: dependencies: fill-range: 7.1.1 - browser-stdout@1.3.1: {} - browserslist@4.25.3: dependencies: caniuse-lite: 1.0.30001736 @@ -7349,8 +7290,6 @@ snapshots: camelcase-css@2.0.1: {} - camelcase@6.3.0: {} - caniuse-lite@1.0.30001736: {} capital-case@1.0.4: @@ -7462,12 +7401,6 @@ snapshots: cli-width@4.1.0: {} - cliui@7.0.4: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -7670,8 +7603,6 @@ snapshots: optionalDependencies: supports-color: 8.1.1 - decamelize@4.0.0: {} - decamelize@6.0.1: {} deep-eql@5.0.2: {} @@ -7740,8 +7671,6 @@ snapshots: diff@4.0.2: {} - diff@5.2.0: {} - diff@8.0.2: {} dir-glob@3.0.1: @@ -8369,8 +8298,6 @@ snapshots: flatted: 3.3.3 hookified: 1.12.0 - flat@5.0.2: {} - flatted@3.3.3: {} for-each@0.3.5: @@ -8531,14 +8458,6 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 - glob@8.1.0: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 5.1.6 - once: 1.4.0 - global-dirs@3.0.1: dependencies: ini: 2.0.0 @@ -8813,8 +8732,6 @@ snapshots: is-path-inside@3.0.3: {} - is-plain-obj@2.1.0: {} - is-plain-obj@4.1.0: {} is-plain-object@5.0.0: {} @@ -8851,8 +8768,6 @@ snapshots: dependencies: which-typed-array: 1.1.19 - is-unicode-supported@0.1.0: {} - is-unicode-supported@2.1.0: {} is-weakmap@2.0.2: {} @@ -9158,11 +9073,6 @@ snapshots: lodash@4.17.21: {} - log-symbols@4.1.0: - dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 - loglevel-plugin-prefix@0.8.4: {} loglevel@1.9.2: {} @@ -9259,29 +9169,6 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.1 - mocha@10.8.2: - dependencies: - ansi-colors: 4.1.3 - browser-stdout: 1.3.1 - chokidar: 3.6.0 - debug: 4.4.1(supports-color@8.1.1) - diff: 5.2.0 - escape-string-regexp: 4.0.0 - find-up: 5.0.0 - glob: 8.1.0 - he: 1.2.0 - js-yaml: 4.1.0 - log-symbols: 4.1.0 - minimatch: 5.1.6 - ms: 2.1.3 - serialize-javascript: 6.0.2 - strip-json-comments: 3.1.1 - supports-color: 8.1.1 - workerpool: 6.5.1 - yargs: 16.2.0 - yargs-parser: 20.2.9 - yargs-unparser: 2.0.0 - mrmime@2.0.1: {} ms@2.1.2: {} @@ -9781,10 +9668,6 @@ snapshots: quick-format-unescaped@4.0.4: {} - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - react-is@17.0.2: {} react-is@18.3.1: {} @@ -10030,10 +9913,6 @@ snapshots: dependencies: type-fest: 4.41.0 - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 - set-cookie-parser@2.7.1: {} set-function-length@1.2.2: @@ -11040,8 +10919,6 @@ snapshots: word-wrap@1.2.5: {} - workerpool@6.5.1: {} - wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -11081,27 +10958,8 @@ snapshots: yaml@2.8.0: {} - yargs-parser@20.2.9: {} - yargs-parser@21.1.1: {} - yargs-unparser@2.0.0: - dependencies: - camelcase: 6.3.0 - decamelize: 4.0.0 - flat: 5.0.2 - is-plain-obj: 2.1.0 - - yargs@16.2.0: - dependencies: - cliui: 7.0.4 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 20.2.9 - yargs@17.7.2: dependencies: cliui: 8.0.1