@cementic/cementic-test 0.2.3

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 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/commands/new.ts","../src/commands/normalize.ts","../src/commands/test.ts","../src/commands/tc.ts","../src/core/prefix.ts","../src/core/llm.ts","../src/core/scrape.ts","../src/commands/report.ts","../src/commands/serve.ts","../src/commands/flow.ts","../src/commands/ci.ts"],"sourcesContent":["// src/cli.ts\nimport { Command } from 'commander';\nimport { newCmd } from './commands/new.js';\nimport { normalizeCmd } from './commands/normalize.js';\nimport { genCmd } from './commands/gen.js';\nimport { testCmd } from './commands/test.js';\nimport { tcCmd } from './commands/tc.js';\nimport { reportCmd } from './commands/report.js';\nimport { serveCmd } from './commands/serve.js';\nimport { flowCmd } from './commands/flow.js';\nimport { ciCmd } from './commands/ci.js';\n\nconst program = new Command();\nprogram\n .name('cementic-test')\n .description('CementicTest CLI: cases β†’ normalized β†’ POM tests β†’ Playwright')\n .version('0.2.0');\n\nprogram.addCommand(newCmd());\nprogram.addCommand(normalizeCmd());\nprogram.addCommand(genCmd());\nprogram.addCommand(testCmd());\nprogram.addCommand(tcCmd());\nprogram.addCommand(reportCmd());\nprogram.addCommand(serveCmd());\nprogram.addCommand(flowCmd());\nprogram.addCommand(ciCmd());\n\nprogram.parseAsync(process.argv);","import { Command } from 'commander';\nimport { mkdirSync, writeFileSync, existsSync, readFileSync, cpSync, readdirSync, statSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport { execSync } from 'node:child_process';\nimport { fileURLToPath } from 'node:url';\nimport { dirname } from 'node:path';\nimport { platform, release } from 'node:os';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nexport function newCmd() {\n const cmd = new Command('new')\n .arguments('<projectName>')\n .description('Scaffold a new CementicTest + Playwright project from scratch')\n .addHelpText('after', `\nExamples:\n $ ct new my-awesome-test-suite\n $ ct new e2e-tests --no-browsers\n`)\n .option('--mode <mode>', 'greenfield|enhance', 'greenfield')\n .option('--no-browsers', 'do not run \"npx playwright install\" during setup')\n .action((projectName: string, opts) => {\n const root = process.cwd();\n const projectPath = join(root, projectName);\n\n console.log(`πŸš€ Initializing new CementicTest project in ${projectName}...`);\n\n // 1. Create project directory\n if (existsSync(projectPath)) {\n console.error(`❌ Directory ${projectName} already exists.`);\n process.exit(1);\n }\n mkdirSync(projectPath, { recursive: true });\n\n // 2. Copy template files\n // We need to locate the template directory relative to the built CLI file\n // In development (ts-node), it might be different than in production (dist)\n // We'll assume the templates are copied to 'dist/templates' or similar during build\n // For now, let's try to find it relative to __dirname\n \n // In the source structure: src/commands/new.ts -> templates/student-framework is ../../templates/student-framework\n // In the dist structure: dist/cli.js -> templates/student-framework is ./templates/student-framework\n \n // We will rely on the build process to place templates correctly.\n \n // 1. Try relative to __dirname (dist/templates) - most likely for production\n let templatePath = resolve(__dirname, 'templates/student-framework');\n \n // 2. Try sibling of dist (e.g. if running from dist/ but templates are in root/templates)\n if (!existsSync(templatePath)) {\n templatePath = resolve(__dirname, '../templates/student-framework');\n }\n\n // 3. Try source structure (src/commands/new.ts -> ../../templates)\n if (!existsSync(templatePath)) {\n templatePath = resolve(__dirname, '../../templates/student-framework');\n }\n\n // 4. Fallback to CWD (development)\n if (!existsSync(templatePath)) {\n templatePath = resolve(process.cwd(), 'templates/student-framework');\n }\n\n if (!existsSync(templatePath)) {\n console.error(`❌ Could not locate template at ${templatePath}`);\n console.error('Please ensure the package is built correctly with templates included.');\n process.exit(1);\n }\n\n console.log(`πŸ“¦ Copying template from ${templatePath}...`);\n \n // Recursive copy function\n function copyRecursive(src: string, dest: string) {\n if (statSync(src).isDirectory()) {\n mkdirSync(dest, { recursive: true });\n readdirSync(src).forEach(child => {\n copyRecursive(join(src, child), join(dest, child));\n });\n } else {\n cpSync(src, dest);\n }\n }\n\n copyRecursive(templatePath, projectPath);\n\n // 2.5 Adjust for OS compatibility (macOS 13/Ventura or older)\n if (platform() === 'darwin') {\n const osRelease = release();\n const majorVersion = parseInt(osRelease.split('.')[0], 10);\n \n // macOS 13 is Darwin 22. macOS 14 is Darwin 23.\n if (majorVersion < 23) {\n console.log('🍎 Detected macOS 13 or older. Adjusting versions for compatibility...');\n const pkgJsonPath = join(projectPath, 'package.json');\n if (existsSync(pkgJsonPath)) {\n try {\n const pkgContent = readFileSync(pkgJsonPath, 'utf-8');\n const pkg = JSON.parse(pkgContent);\n if (pkg.devDependencies) {\n pkg.devDependencies['@playwright/test'] = '^1.48.2';\n pkg.devDependencies['allure-playwright'] = '^2.15.1';\n writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2));\n console.log('βœ… Downgraded @playwright/test and allure-playwright for legacy macOS support.');\n }\n } catch (err) {\n console.warn('⚠️ Failed to adjust package.json for OS compatibility:', err);\n }\n }\n }\n }\n\n // 3. Initialize git\n try {\n execSync('git init', { cwd: projectPath, stdio: 'ignore' });\n // Create .gitignore if not exists (template should have it, but just in case)\n const gitignorePath = join(projectPath, '.gitignore');\n if (!existsSync(gitignorePath)) {\n writeFileSync(gitignorePath, 'node_modules\\n.env\\ntest-results\\nplaywright-report\\n.cementic\\n');\n }\n } catch (e) {\n console.warn('⚠️ Failed to initialize git repository.');\n }\n\n // 4. Install dependencies\n console.log('πŸ“¦ Installing dependencies...');\n try {\n execSync('npm install', { cwd: projectPath, stdio: 'inherit' });\n } catch (e) {\n console.error('❌ Failed to install dependencies. Please run \"npm install\" manually.');\n }\n\n // 5. Install browsers (optional)\n // Commander maps --no-browsers to opts.browsers = false\n if (opts.browsers !== false) {\n console.log('🌐 Installing Playwright browsers...');\n try {\n execSync('npx playwright install', { cwd: projectPath, stdio: 'inherit' });\n } catch (e) {\n console.warn('⚠️ Failed to install browsers. Run \"npx playwright install\" manually.');\n }\n }\n\n console.log(`\\nβœ… Project ${projectName} created successfully!`);\n console.log(`\\nTo get started:\\n`);\n console.log(` cd ${projectName}`);\n console.log(` npx playwright test`);\n console.log(`\\nHappy testing! πŸ§ͺ`);\n });\n\n return cmd;\n}\n","import { Command } from 'commander';\nimport fg from 'fast-glob';\nimport { readFileSync, mkdirSync, writeFileSync, statSync } from 'node:fs';\nimport { join, basename, resolve } from 'node:path';\n\ntype NormalizedCase = {\n id?: string;\n title: string;\n tags?: string[];\n steps: string[];\n expected: string[];\n needs_review: boolean;\n source: string;\n};\n\n/** Grab @tags from the title line */\nfunction parseTags(title: string): { clean: string; tags: string[] } {\n const tags = Array.from(title.matchAll(/@([\\w-]+)/g)).map((m) => m[1]);\n const clean = title.replace(/@[\\w-]+/g, '').trim();\n return { clean, tags };\n}\n\n/** Try to extract an ID prefix like AUTH-001 from the title */\nfunction parseId(title: string): string | undefined {\n const m = title.match(/\\b([A-Z]+-\\d+)\\b/);\n return m?.[1];\n}\n\n/** Split a markdown file into case blocks by top-level \"# \" headings */\nfunction splitCasesByHeading(fileText: string): Array<{ titleLine: string; body: string }> {\n const lines = fileText.split(/\\r?\\n/);\n const blocks: Array<{ titleLine: string; body: string }> = [];\n let currentTitle: string | null = null;\n let buf: string[] = [];\n\n const flush = () => {\n if (currentTitle !== null) {\n blocks.push({ titleLine: currentTitle, body: buf.join('\\n') });\n }\n buf = [];\n };\n\n for (const line of lines) {\n const h1 = line.match(/^\\s*#\\s+(.+)$/); // \"# Title\"\n if (h1) {\n if (currentTitle !== null) flush();\n currentTitle = h1[1].trim();\n } else {\n buf.push(line);\n }\n }\n if (currentTitle !== null) flush();\n\n // If no H1 at all, treat whole file as one case (first non-empty line is title).\n if (blocks.length === 0) {\n const first = lines.find((l) => l.trim());\n const title = first?.replace(/^#\\s*/, '').trim() || 'Untitled';\n return [{ titleLine: title, body: lines.join('\\n') }];\n }\n return blocks;\n}\n\n/** From a case body, extract Steps and Expected sections */\nfunction extractSections(body: string): { steps: string[]; expected: string[] } {\n // Find sections by H2 headings\n // Supports: \"## Steps\", \"## Expected\", \"## Expected Results\"\n const sectionRegex = /^\\s*##\\s*(.+?)\\s*$/gim;\n const sections: Record<string, string> = {};\n let match: RegExpExecArray | null;\n const indices: Array<{ name: string; index: number }> = [];\n\n while ((match = sectionRegex.exec(body))) {\n indices.push({ name: match[1].toLowerCase(), index: match.index });\n }\n\n // Add end sentinel\n indices.push({ name: '__END__', index: body.length });\n\n for (let i = 0; i < indices.length - 1; i++) {\n const name = indices[i].name;\n const slice = body.slice(indices[i].index, indices[i + 1].index);\n sections[name] = slice;\n }\n\n const stepsBlock =\n sections['steps'] ??\n ''; // if absent, we’ll infer from bullets later\n\n const expectedBlock =\n sections['expected'] ??\n sections['expected results'] ??\n sections['then'] ??\n '';\n\n const bullet = /^\\s*(?:\\d+\\.|[-*])\\s+(.+)$/gm;\n\n const steps =\n Array.from(stepsBlock.matchAll(bullet)).map((m) => m[1].trim()) ||\n [];\n\n // Fallback: if no dedicated steps section, collect bullets until a new H2\n if (steps.length === 0) {\n const alt = Array.from(body.matchAll(bullet)).map((m) => m[1].trim());\n // Heuristic: take the first bullet run in the body\n steps.push(...alt);\n }\n\n const expectedLines: string[] = Array.from(expectedBlock.matchAll(bullet)).map((m) =>\n m[1].trim()\n );\n\n // Fallback: lines that start with Expected/Then/Verify/Assert in the whole body\n if (expectedLines.length === 0) {\n const exp = Array.from(\n body.matchAll(/^\\s*(?:Expected|Then|Verify|Assert)[^\\n]*:?[\\s-]*(.+)$/gim)\n ).map((m) => m[1].trim());\n expectedLines.push(...exp);\n }\n\n return { steps, expected: expectedLines };\n}\n\n/** Build one normalized case from a title line + body */\nfunction normalizeOne(titleLine: string, body: string, source: string): NormalizedCase {\n const { clean, tags } = parseTags(titleLine);\n const id = parseId(clean);\n const { steps, expected } = extractSections(body);\n\n return {\n id,\n title: clean,\n tags: tags.length ? tags : undefined,\n steps,\n expected,\n needs_review: steps.length === 0 || expected.length === 0,\n source\n };\n}\n\n/** ===== Commander command ===== */\nexport function normalizeCmd() {\n const cmd = new Command('normalize')\n .argument('<path>', 'Input directory or file pattern containing test cases (Markdown, Text, CSV)')\n .description('Convert human-readable test cases into machine-readable JSON format')\n .addHelpText('after', `\nExamples:\n $ ct normalize ./cases\n $ ct normalize \"cases/**/*.md\"\n $ ct normalize ./cases --and-gen --lang ts (Normalize and generate tests in one go)\n`)\n .option('--report', 'Generate a summary report of the normalization process', true)\n .option('--and-gen', 'Automatically run test generation after normalization', false)\n .option('--lang <lang>', 'Target language for generation (ts|js) when using --and-gen', 'ts')\n .action(async (inputPath: string, opts: { report?: boolean; andGen?: boolean; lang?: string }) => {\n // Accept directory OR glob\n let patterns: string[] = [];\n try {\n const abs = resolve(inputPath);\n if (statSync(abs).isDirectory()) {\n const base = inputPath.replace(/\\/$/, '');\n patterns = [`${base}/**/*.{md,markdown,txt,feature,csv,json}`];\n } else {\n patterns = [inputPath];\n }\n } catch {\n patterns = [inputPath];\n }\n\n const files = await fg(patterns, { dot: false, onlyFiles: true });\n if (files.length === 0) {\n console.error(`No files found for: ${inputPath}`);\n process.exit(2);\n }\n\n const outDir = '.cementic/normalized';\n mkdirSync(outDir, { recursive: true });\n\n const index = {\n summary: { total: 0, parsed: 0, withWarnings: 0 },\n cases: [] as Array<{ file: string; normalized: string; status: string }>\n };\n\n for (const f of files) {\n const content = readFileSync(f, 'utf8');\n\n // Split into multiple cases by \"# \"\n const blocks = splitCasesByHeading(content);\n for (const block of blocks) {\n const norm = normalizeOne(block.titleLine, block.body, f);\n\n // filename: <stem>.<ID or sanitized-title>.json\n const stem = basename(f).replace(/\\.[^/.]+$/, '');\n const suffix = (norm.id || norm.title).replace(/[^\\w-]+/g, '-');\n const outFile = join(outDir, `${stem}.${suffix}.json`);\n\n writeFileSync(outFile, JSON.stringify(norm, null, 2));\n\n index.summary.total++;\n index.summary.parsed++;\n if (norm.needs_review) index.summary.withWarnings++;\n index.cases.push({ file: f, normalized: outFile, status: norm.needs_review ? 'warning' : 'ok' });\n }\n }\n\n writeFileSync(join(outDir, '_index.json'), JSON.stringify(index, null, 2));\n\n if (opts.report !== false) {\n const lines = [\n '# Normalize Report',\n '',\n `Total cases: ${index.summary.total} | Parsed: ${index.summary.parsed} | With warnings: ${index.summary.withWarnings}`,\n '',\n '| Source File | Normalized JSON | Status |',\n '|-------------|-----------------|--------|',\n ...index.cases.map(c => `| ${c.file} | ${c.normalized} | ${c.status} |`)\n ];\n mkdirSync('.cementic/reports', { recursive: true });\n writeFileSync('.cementic/reports/normalize-report.md', lines.join('\\n'));\n }\n\n console.log(`βœ… Normalized ${index.summary.parsed} case(s). Output β†’ .cementic/normalized/`);\n\n if (opts.andGen) {\n const { gen } = await import('./gen.js');\n await gen({ lang: opts.lang || 'ts', out: 'tests/generated' });\n }\n });\n\n return cmd;\n}","import { Command } from 'commander';\nimport { spawn } from 'node:child_process';\n\nexport function testCmd() {\n const cmd = new Command('test')\n .description('Run Playwright tests via \"npx playwright test\"')\n .allowUnknownOption(true)\n .allowExcessArguments(true)\n .argument('[...pwArgs]', 'Arguments to pass through to Playwright')\n .action((pwArgs: string[] = []) => {\n const child = spawn(\n 'npx',\n ['playwright', 'test', ...pwArgs],\n {\n stdio: 'inherit',\n shell: process.platform === 'win32', // needed for Windows\n }\n );\n\n child.on('exit', (code) => {\n process.exit(code ?? 0);\n });\n });\n\n return cmd;\n}","// src/commands/tc.ts\nimport { Command } from 'commander';\nimport { mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { createInterface } from 'node:readline/promises';\nimport { stdin as input, stdout as output } from 'node:process';\nimport { inferPrefix } from '../core/prefix.js';\nimport { generateTcMarkdownWithAi } from '../core/llm.js';\nimport { scrapePageSummary } from '../core/scrape.js';\n\nfunction slugify(text: string): string {\n return (\n text\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '') || 'tc'\n );\n}\n\nfunction buildManualCasesMarkdown(opts: {\n prefix: string;\n feature: string;\n url?: string;\n numCases: number;\n startIndex?: number;\n}): string {\n const { prefix, feature, url, numCases } = opts;\n const startIndex = opts.startIndex ?? 1;\n\n const lines: string[] = [];\n\n for (let i = 0; i < numCases; i++) {\n const idx = startIndex + i;\n const id = `${prefix}-${String(idx).padStart(3, '0')}`;\n\n const title = `${feature} - scenario ${idx}`;\n const tags = '@regression @ui';\n\n lines.push(`# ${id} β€” ${title} ${tags}`);\n lines.push(`## Steps`);\n lines.push(`1. Navigate to ${url ?? '<PAGE_URL>'}`);\n lines.push(`2. Perform the main user action for this scenario`);\n lines.push(`3. Observe the result`);\n lines.push('');\n lines.push(`## Expected Results`);\n lines.push(`- The page responds correctly for this scenario`);\n lines.push(`- UI reflects the expected change`);\n lines.push('');\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\nasync function promptBasicQuestions(opts: {\n url?: string;\n feature?: string;\n appDescription?: string;\n numCases?: number;\n}) {\n // If we have the bare minimum args for non-interactive mode, use them.\n if (opts.feature) {\n let n = opts.numCases ?? 3;\n if (n < 1) n = 3;\n if (n > 10) n = 10;\n return {\n feature: opts.feature,\n appDescription: opts.appDescription || '',\n numCases: n,\n url: opts.url,\n };\n }\n\n const rl = createInterface({ input, output });\n\n const feature = (await rl.question('🧩 Feature or page to test: ')).trim();\n const appDescription = (await rl.question('πŸ“ Short app description (optional): ')).trim();\n const numCasesRaw = (await rl.question('πŸ”’ How many test cases? (1-10) [3]: ')).trim();\n\n rl.close();\n\n let numCases = parseInt(numCasesRaw, 10);\n if (isNaN(numCases) || numCases < 1) numCases = 3;\n if (numCases > 10) numCases = 10;\n\n return { feature, appDescription, numCases, url: opts.url };\n}\n\n// πŸ”‘ Single source of truth: did the user pass --ai anywhere?\nfunction hasAiFlagInArgv(): boolean {\n return process.argv.includes('--ai');\n}\n\nasync function runTcInteractive(params: {\n url?: string;\n explicitPrefix?: string;\n feature?: string;\n appDescription?: string;\n numCases?: number;\n}) {\n const { feature, appDescription, numCases, url } = await promptBasicQuestions({\n url: params.url,\n feature: params.feature,\n appDescription: params.appDescription,\n numCases: params.numCases,\n });\n\n const prefix = inferPrefix({\n featureText: feature,\n url,\n explicitPrefix: params.explicitPrefix,\n });\n\n mkdirSync('cases', { recursive: true });\n const fileName = `${prefix.toLowerCase()}-${slugify(feature)}.md`;\n const fullPath = join('cases', fileName);\n\n const useAi = hasAiFlagInArgv();\n console.log(`βš™οΈ Debug: useAi=${useAi}, argv=${JSON.stringify(process.argv)}`);\n\n let markdown: string;\n\n if (useAi) {\n let pageSummaryJson: any = undefined;\n\n if (url) {\n try {\n console.log(`πŸ” Scraping page for AI context: ${url}`);\n pageSummaryJson = await scrapePageSummary(url);\n console.log(\n `πŸ”Ž Scrape summary: title=\"${pageSummaryJson.title || ''}\", ` +\n `headings=${pageSummaryJson.headings.length}, ` +\n `buttons=${pageSummaryJson.buttons.length}, ` +\n `inputs=${pageSummaryJson.inputs.length}`\n );\n } catch (e: any) {\n console.warn(\n `⚠️ Failed to scrape ${url} (${e?.message || e}). Continuing without page summary.`\n );\n }\n }\n\n try {\n console.log('πŸ€– AI: generating test cases...');\n markdown = await generateTcMarkdownWithAi({\n appDescription: appDescription || undefined,\n feature,\n url,\n pageSummaryJson,\n prefix,\n startIndex: 1,\n numCases,\n });\n console.log('βœ… AI: generated test case markdown.');\n } catch (err: any) {\n console.warn(\n `⚠️ AI generation failed (${err?.message || err}). Falling back to manual templates.`\n );\n markdown = buildManualCasesMarkdown({\n prefix,\n feature,\n url,\n numCases,\n startIndex: 1,\n });\n console.log('πŸ“ Manual: generated test case templates instead.');\n }\n } else {\n markdown = buildManualCasesMarkdown({\n prefix,\n feature,\n url,\n numCases,\n startIndex: 1,\n });\n console.log('πŸ“ Manual: generated test case templates (no --ai).');\n }\n\n writeFileSync(fullPath, markdown);\n\n console.log(`✍️ Wrote ${numCases} test case(s) β†’ ${fullPath}`);\n console.log('Next steps:');\n console.log(' ct normalize ./cases --and-gen --lang ts');\n console.log(' ct test');\n}\n\nexport function tcCmd() {\n const root = new Command('tc')\n .description('Create CT-style test cases (Markdown) under ./cases')\n // we still declare the option so Commander doesn't choke on --ai,\n // but we IGNORE the parsed value and instead use process.argv.\n .option('--ai', 'Use AI if configured (BYO LLM API key).', false)\n .option('--prefix <prefix>', 'Explicit ID prefix, e.g. AUTH, DASH, CART')\n .option('--feature <name>', 'Feature name (non-interactive)')\n .option('--desc <text>', 'App description (non-interactive)')\n .option('--count <n>', 'Number of cases (non-interactive)', parseInt)\n .action(async (opts: { prefix?: string; feature?: string; desc?: string; count?: number }) => {\n await runTcInteractive({\n url: undefined,\n explicitPrefix: opts.prefix,\n feature: opts.feature,\n appDescription: opts.desc,\n numCases: opts.count,\n });\n });\n\n root\n .command('url')\n .argument('<url>', 'Page URL to use as context')\n .option('--ai', 'Use AI if configured (BYO LLM API key).', false)\n .option('--prefix <prefix>', 'Explicit ID prefix, e.g. AUTH, DASH, CART')\n .option('--feature <name>', 'Feature name (non-interactive)')\n .option('--desc <text>', 'App description (non-interactive)')\n .option('--count <n>', 'Number of cases (non-interactive)', parseInt)\n .description('Create test cases with awareness of a specific page URL')\n .action(async (url: string, opts: { prefix?: string; feature?: string; desc?: string; count?: number }) => {\n await runTcInteractive({\n url,\n explicitPrefix: opts.prefix,\n feature: opts.feature,\n appDescription: opts.desc,\n numCases: opts.count,\n });\n });\n\n return root;\n}\n","// src/core/prefix.ts\n\nconst PREFIX_MAP: Array<{ keywords: string[]; prefix: string }> = [\n { keywords: ['login', 'sign in', 'signin', 'auth', 'authentication'], prefix: 'AUTH' },\n { keywords: ['dashboard', 'home'], prefix: 'DASH' },\n { keywords: ['profile', 'account'], prefix: 'PROF' },\n { keywords: ['cart', 'basket'], prefix: 'CART' },\n { keywords: ['checkout', 'payment', 'pay'], prefix: 'CHK' },\n { keywords: ['order', 'orders'], prefix: 'ORD' },\n { keywords: ['settings', 'preferences', 'config'], prefix: 'SET' },\n];\n\nfunction normalizeText(text: string): string {\n return text.toLowerCase().trim();\n}\n\nfunction deriveFromFreeText(text: string): string | undefined {\n const norm = normalizeText(text);\n for (const entry of PREFIX_MAP) {\n if (entry.keywords.some(k => norm.includes(k))) {\n return entry.prefix;\n }\n }\n // fallback: first word, first 4 alphanumerics\n const firstWord = norm.split(/\\s+/).find(w => /[a-z0-9]/.test(w));\n if (!firstWord) return undefined;\n return firstWord.replace(/[^a-z0-9]/gi, '').slice(0, 4).toUpperCase() || undefined;\n}\n\nfunction deriveFromUrl(url: string): string | undefined {\n try {\n const u = new URL(url);\n const segments = u.pathname.split('/').filter(Boolean);\n const last = segments[segments.length - 1] || '';\n if (!last) return undefined;\n return deriveFromFreeText(last);\n } catch {\n return undefined;\n }\n}\n\nexport function inferPrefix(params: {\n featureText?: string;\n url?: string;\n explicitPrefix?: string;\n}): string {\n // 1) explicit flag wins\n if (params.explicitPrefix) {\n return params.explicitPrefix.trim().toUpperCase();\n }\n\n // 2) feature text\n if (params.featureText) {\n const fromFeature = deriveFromFreeText(params.featureText);\n if (fromFeature) return fromFeature;\n }\n\n // 3) url\n if (params.url) {\n const fromUrl = deriveFromUrl(params.url);\n if (fromUrl) return fromUrl;\n }\n\n // 4) last-resort default\n return 'TC';\n}","// src/core/llm.ts\n\nexport type TcAiContext = {\n appDescription?: string;\n feature: string;\n url?: string;\n pageSummaryJson?: any; // optional, from scrape\n prefix: string;\n startIndex: number;\n numCases: number;\n};\n\nfunction buildSystemMessage(): string {\n return `\nYou are a senior QA engineer and test case designer.\n\nYour job:\n- Take the context of a web feature or page,\n- And generate high-quality UI test cases\n- In a very strict Markdown format that another tool will parse.\n\nYou MUST follow this format exactly for each test case:\n\n# <ID> β€” <Short title> @<tag1> @<tag2> ...\n## Steps\n1. <step>\n2. <step>\n\n## Expected Results\n- <assertion>\n- <assertion>\n\nRules:\n- <ID> must be PREFIX-XXX where PREFIX is provided to you (e.g., AUTH, DASH, CART).\n- XXX must be a 3-digit number starting from the startIndex provided in the context.\n For example: DASH-005, DASH-006, DASH-007 if startIndex is 5.\n- Use 1–3 tags per test (e.g., @smoke, @regression, @auth, @ui, @critical).\n- \"Steps\" should describe user actions in sequence.\n- \"Expected Results\" should describe verifiable outcomes (URL change, element visible, message shown, etc.).\n- Do NOT add any explanation before or after the test cases.\n- Output ONLY the test cases, back-to-back, in Markdown.\n- No code blocks, no extra headings outside the pattern described.\n`.trim();\n}\n\nfunction buildUserMessage(ctx: TcAiContext): string {\n const lines: string[] = [];\n\n lines.push(`App / Product description (optional):`);\n lines.push(ctx.appDescription || 'N/A');\n lines.push('');\n\n lines.push(`Feature or page to test:`);\n lines.push(ctx.feature);\n lines.push('');\n\n if (ctx.url) {\n lines.push(`Page URL:`);\n lines.push(ctx.url);\n lines.push('');\n }\n\n if (ctx.pageSummaryJson) {\n lines.push(`Page structure summary (JSON):`);\n lines.push('```json');\n lines.push(JSON.stringify(ctx.pageSummaryJson, null, 2));\n lines.push('```');\n lines.push('');\n }\n\n lines.push(`Test ID prefix to use: ${ctx.prefix}`);\n lines.push(\n `Start numbering from: ${String(ctx.startIndex).padStart(3, '0')}`\n );\n lines.push(`Number of test cases to generate: ${ctx.numCases}`);\n lines.push('');\n lines.push(`Important formatting rules:`);\n lines.push(`- Use IDs like ${ctx.prefix}-NNN where NNN is 3-digit, sequential from the start index.`);\n lines.push(`- Each test case must follow this pattern exactly:`);\n lines.push(`# ${ctx.prefix}-NNN β€” <short title> @tag1 @tag2`);\n lines.push(`## Steps`);\n lines.push(`1. ...`);\n lines.push(`2. ...`);\n lines.push(``);\n lines.push(`## Expected Results`);\n lines.push(`- ...`);\n lines.push(`- ...`);\n lines.push('');\n lines.push(`Do NOT add any explanation before or after the test cases. Only output the test cases.`);\n\n return lines.join('\\n');\n}\n\n/**\n * Simple OpenAI-compatible chat call.\n * BYO key via:\n * - CT_LLM_API_KEY or OPENAI_API_KEY\n * BYO model via:\n * - CT_LLM_MODEL (default: gpt-4.1-mini)\n * BYO endpoint via:\n * - CT_LLM_BASE_URL (default: https://api.openai.com/v1)\n */\nexport async function generateTcMarkdownWithAi(ctx: TcAiContext): Promise<string> {\n const apiKey =\n process.env.CT_LLM_API_KEY ||\n process.env.OPENAI_API_KEY ||\n '';\n\n if (!apiKey) {\n throw new Error(\n 'No LLM API key found. Set CT_LLM_API_KEY or OPENAI_API_KEY.'\n );\n }\n\n const baseUrl =\n process.env.CT_LLM_BASE_URL || 'https://api.openai.com/v1';\n const model =\n process.env.CT_LLM_MODEL || 'gpt-4.1-mini';\n\n const system = buildSystemMessage();\n const user = buildUserMessage(ctx);\n\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n model,\n messages: [\n { role: 'system', content: system },\n { role: 'user', content: user },\n ],\n temperature: 0.2,\n }),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(\n `LLM request failed: ${response.status} ${response.statusText} β€” ${text}`\n );\n }\n\n const json: any = await response.json();\n const content =\n json.choices?.[0]?.message?.content?.trim() || '';\n\n if (!content) {\n throw new Error('LLM response had no content');\n }\n\n return content;\n}","// src/core/scrape.ts\n\nexport type PageSummary = {\n url: string;\n title?: string;\n headings: string[];\n buttons: string[];\n links: string[];\n inputs: string[];\n rawLength: number;\n};\n\n/**\n * Very lightweight HTML scraper using fetch + regex.\n * v0: good enough to give the LLM some real context (titles, headings, buttons, inputs).\n * Not a full browser; doesn’t execute JS.\n */\nexport async function scrapePageSummary(url: string): Promise<PageSummary> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);\n }\n\n const html = await res.text();\n const rawLength = html.length;\n\n const titleMatch = html.match(/<title[^>]*>([^<]*)<\\/title>/i);\n const title = titleMatch?.[1]?.trim() || undefined;\n\n const headings = Array.from(html.matchAll(/<(h1|h2)[^>]*>([^<]*)<\\/\\1>/gi))\n .map(m => m[2].replace(/\\s+/g, ' ').trim())\n .filter(Boolean);\n\n const buttons = Array.from(html.matchAll(/<button[^>]*>([^<]*)<\\/button>/gi))\n .map(m => m[1].replace(/\\s+/g, ' ').trim())\n .filter(Boolean);\n\n const links = Array.from(html.matchAll(/<a[^>]*>([^<]*)<\\/a>/gi))\n .map(m => m[1].replace(/\\s+/g, ' ').trim())\n .filter(Boolean)\n .slice(0, 50);\n\n const inputs: string[] = [];\n\n // labels like <label for=\"email\">Email</label>\n const labelMap = new Map<string, string>();\n for (const m of html.matchAll(/<label[^>]*for=[\"']?([^\"'>\\s]+)[\"']?[^>]*>([^<]*)<\\/label>/gi)) {\n const id = m[1];\n const text = m[2].replace(/\\s+/g, ' ').trim();\n if (id && text) labelMap.set(id, text);\n }\n\n for (const m of html.matchAll(/<input([^>]*)>/gi)) {\n const attrs = m[1];\n const nameMatch = attrs.match(/\\bname=[\"']?([^\"'>\\s]+)[\"']?/i);\n const idMatch = attrs.match(/\\bid=[\"']?([^\"'>\\s]+)[\"']?/i);\n const phMatch = attrs.match(/\\bplaceholder=[\"']([^\"']*)[\"']/i);\n\n const id = idMatch?.[1];\n const label = id ? labelMap.get(id) : undefined;\n const ph = phMatch?.[1]?.trim();\n const name = nameMatch?.[1];\n\n const descriptor = label || ph || name;\n if (descriptor) {\n inputs.push(descriptor);\n }\n }\n\n return {\n url,\n title,\n headings: headings.slice(0, 20),\n buttons: buttons.slice(0, 30),\n links,\n inputs: inputs.slice(0, 30),\n rawLength,\n };\n}","import { Command } from 'commander';\nimport { spawn } from 'node:child_process';\n\nexport function reportCmd() {\n const cmd = new Command('report')\n .description('Open the Playwright HTML report')\n .action(() => {\n console.log('πŸ“Š Opening Playwright HTML report...');\n \n const child = spawn(\n 'npx',\n ['playwright', 'show-report'],\n {\n stdio: 'inherit',\n shell: process.platform === 'win32',\n }\n );\n\n child.on('exit', (code) => {\n process.exit(code ?? 0);\n });\n });\n\n return cmd;\n}\n","import { Command } from 'commander';\nimport { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport function serveCmd() {\n const cmd = new Command('serve')\n .description('Serve the Allure report')\n .action(() => {\n console.log('πŸ“Š Serving Allure report...');\n\n // Try to find the local allure binary first (more reliable than global/npx sometimes)\n const localAllureBin = join(process.cwd(), 'node_modules', 'allure-commandline', 'bin', 'allure');\n \n let executable = 'npx';\n let args = ['allure', 'serve', './allure-results'];\n\n // If we can find the direct binary, use it (node node_modules/.../allure)\n // This bypasses the \"require('../')\" issue in the .bin wrapper\n if (existsSync(localAllureBin)) {\n executable = 'node';\n args = [localAllureBin, 'serve', './allure-results'];\n }\n\n console.log(`> ${executable} ${args.join(' ')}`);\n\n const child = spawn(\n executable,\n args,\n {\n stdio: 'inherit',\n shell: process.platform === 'win32',\n }\n );\n\n child.on('exit', (code) => {\n process.exit(code ?? 0);\n });\n });\n\n return cmd;\n}\n","import { Command } from 'commander';\nimport { spawn } from 'node:child_process';\nimport { resolve } from 'node:path';\n\nfunction runStep(cmd: string, args: string[], stepName: string): Promise<void> {\n return new Promise((resolve, reject) => {\n console.log(`\\n🌊 Flow Step: ${stepName}`);\n console.log(`> ${cmd} ${args.join(' ')}`);\n\n const child = spawn(cmd, args, {\n stdio: 'inherit',\n shell: process.platform === 'win32',\n });\n\n child.on('exit', (code) => {\n if (code === 0) {\n resolve();\n } else {\n reject(new Error(`${stepName} failed with exit code ${code}`));\n }\n });\n });\n}\n\nexport function flowCmd() {\n const cmd = new Command('flow')\n .description('End-to-end flow: Normalize -> Generate -> Run Tests')\n .argument('[casesDir]', 'Directory containing test cases', './cases')\n .option('--lang <lang>', 'Target language (ts|js)', 'ts')\n .option('--no-run', 'Skip running tests')\n .action(async (casesDir, opts) => {\n const cliBin = resolve(process.argv[1]); // The current CLI executable\n\n try {\n // 1. Normalize\n await runStep(process.execPath, [cliBin, 'normalize', casesDir], 'Normalize Cases');\n\n // 2. Generate\n await runStep(process.execPath, [cliBin, 'gen', '--lang', opts.lang], 'Generate Tests');\n\n // 3. Test (unless skipped)\n if (opts.run) {\n await runStep(process.execPath, [cliBin, 'test'], 'Run Playwright Tests');\n } else {\n console.log('\\n⏭️ Skipping test execution (--no-run)');\n }\n\n console.log('\\nβœ… Flow completed successfully!');\n } catch (err: any) {\n console.error(`\\n❌ Flow failed: ${err.message}`);\n process.exit(1);\n }\n });\n\n return cmd;\n}\n","import { Command } from 'commander';\nimport { mkdirSync, writeFileSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nconst WORKFLOW_CONTENT = `name: Playwright Tests\non:\n push:\n branches: [ main, master ]\n pull_request:\n branches: [ main, master ]\njobs:\n test:\n timeout-minutes: 60\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: lts/*\n - name: Install dependencies\n run: npm ci\n - name: Install Playwright Browsers\n run: npx playwright install --with-deps\n - name: Run Playwright tests\n run: npx playwright test\n - uses: actions/upload-artifact@v4\n if: always()\n with:\n name: playwright-report\n path: playwright-report/\n retention-days: 30\n`;\n\nexport function ciCmd() {\n const cmd = new Command('ci')\n .description('Generate GitHub Actions workflow for CI')\n .action(() => {\n const githubDir = join(process.cwd(), '.github');\n const workflowsDir = join(githubDir, 'workflows');\n const workflowFile = join(workflowsDir, 'cementic.yml');\n\n console.log('πŸ€– Setting up CI/CD workflow...');\n\n if (!existsSync(workflowsDir)) {\n mkdirSync(workflowsDir, { recursive: true });\n console.log(`Created directory: ${workflowsDir}`);\n }\n\n if (existsSync(workflowFile)) {\n console.warn(`⚠️ Workflow file already exists at ${workflowFile}. Skipping.`);\n return;\n }\n\n writeFileSync(workflowFile, WORKFLOW_CONTENT.trim() + '\\n');\n console.log(`βœ… CI workflow generated at: ${workflowFile}`);\n console.log('Next steps:');\n console.log('1. Commit and push the new file');\n console.log('2. Check the \"Actions\" tab in your GitHub repository');\n });\n\n return cmd;\n}\n"],"mappings":"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","names":["Command","Command","readFileSync","mkdirSync","writeFileSync","statSync","join","resolve","Command","Command","mkdirSync","writeFileSync","join","mkdirSync","join","writeFileSync","Command","Command","spawn","Command","spawn","existsSync","join","Command","spawn","resolve","Command","mkdirSync","writeFileSync","existsSync","join","Command"]}
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ gen,
4
+ genCmd
5
+ } from "./chunk-J63TUHIV.js";
6
+ export {
7
+ gen,
8
+ genCmd
9
+ };
10
+ //# sourceMappingURL=gen-54KYT3RO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,13 @@
1
+ # Test Project
2
+
3
+ This project was generated by [CementicTest](https://github.com/testamplify/cementic-test-cli).
4
+
5
+ ## Running Tests
6
+
7
+ ```bash
8
+ npx playwright test
9
+ ```
10
+
11
+ ## Writing Cases
12
+
13
+ Add your test cases to the `cases/` directory.
@@ -0,0 +1,204 @@
1
+ {
2
+ "name": "day-28-k12-harmony-hub-automation",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "day-28-k12-harmony-hub-automation",
9
+ "version": "1.0.0",
10
+ "license": "ISC",
11
+ "devDependencies": {
12
+ "@playwright/test": "^1.40.0",
13
+ "@types/node": "^20.10.0",
14
+ "allure-commandline": "^2.36.0",
15
+ "allure-playwright": "^2.15.1"
16
+ }
17
+ },
18
+ "node_modules/@playwright/test": {
19
+ "version": "1.42.1",
20
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz",
21
+ "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==",
22
+ "deprecated": "Please update to the latest version of Playwright to test up-to-date browsers.",
23
+ "dev": true,
24
+ "license": "Apache-2.0",
25
+ "dependencies": {
26
+ "playwright": "1.42.1"
27
+ },
28
+ "bin": {
29
+ "playwright": "cli.js"
30
+ },
31
+ "engines": {
32
+ "node": ">=16"
33
+ }
34
+ },
35
+ "node_modules/@types/node": {
36
+ "version": "20.19.27",
37
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
38
+ "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
39
+ "dev": true,
40
+ "license": "MIT",
41
+ "dependencies": {
42
+ "undici-types": "~6.21.0"
43
+ }
44
+ },
45
+ "node_modules/allure-commandline": {
46
+ "version": "2.36.0",
47
+ "resolved": "https://registry.npmjs.org/allure-commandline/-/allure-commandline-2.36.0.tgz",
48
+ "integrity": "sha512-ls/4fk2Psv2Tu2PbWFrQPmUnm3gmmO9MBan4MuPWwqdkJPEmln2KRwtvtWYr9Av+e5AnFK1fGXWVyxqJIPiPwA==",
49
+ "dev": true,
50
+ "license": "Apache-2.0",
51
+ "bin": {
52
+ "allure": "bin/allure"
53
+ }
54
+ },
55
+ "node_modules/allure-js-commons": {
56
+ "version": "2.15.1",
57
+ "resolved": "https://registry.npmjs.org/allure-js-commons/-/allure-js-commons-2.15.1.tgz",
58
+ "integrity": "sha512-5V/VINplbu0APnfSZOkYpKOzucO36Q2EtTD1kqjWjl7n6tj7Hh+IHCZsH3Vpk/LXRDfj9RuXugBBvwYKV5YMJw==",
59
+ "dev": true,
60
+ "license": "Apache-2.0",
61
+ "dependencies": {
62
+ "md5": "^2.3.0",
63
+ "properties": "^1.2.1",
64
+ "strip-ansi": "^5.2.0"
65
+ }
66
+ },
67
+ "node_modules/allure-playwright": {
68
+ "version": "2.15.1",
69
+ "resolved": "https://registry.npmjs.org/allure-playwright/-/allure-playwright-2.15.1.tgz",
70
+ "integrity": "sha512-P1Uu1j/ptDHdYp3V5ZAeBZyt33+L+OQu0otUIEl/zkOcv0KRycHqlHwC0GEJmpgnLKvVP7s+K37LcLoSUUj3Cg==",
71
+ "dev": true,
72
+ "license": "Apache-2.0",
73
+ "dependencies": {
74
+ "allure-js-commons": "2.15.1"
75
+ }
76
+ },
77
+ "node_modules/ansi-regex": {
78
+ "version": "4.1.1",
79
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
80
+ "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
81
+ "dev": true,
82
+ "license": "MIT",
83
+ "engines": {
84
+ "node": ">=6"
85
+ }
86
+ },
87
+ "node_modules/charenc": {
88
+ "version": "0.0.2",
89
+ "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
90
+ "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
91
+ "dev": true,
92
+ "license": "BSD-3-Clause",
93
+ "engines": {
94
+ "node": "*"
95
+ }
96
+ },
97
+ "node_modules/crypt": {
98
+ "version": "0.0.2",
99
+ "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
100
+ "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
101
+ "dev": true,
102
+ "license": "BSD-3-Clause",
103
+ "engines": {
104
+ "node": "*"
105
+ }
106
+ },
107
+ "node_modules/fsevents": {
108
+ "version": "2.3.2",
109
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
110
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
111
+ "dev": true,
112
+ "hasInstallScript": true,
113
+ "license": "MIT",
114
+ "optional": true,
115
+ "os": [
116
+ "darwin"
117
+ ],
118
+ "engines": {
119
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
120
+ }
121
+ },
122
+ "node_modules/is-buffer": {
123
+ "version": "1.1.6",
124
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
125
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
126
+ "dev": true,
127
+ "license": "MIT"
128
+ },
129
+ "node_modules/md5": {
130
+ "version": "2.3.0",
131
+ "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
132
+ "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
133
+ "dev": true,
134
+ "license": "BSD-3-Clause",
135
+ "dependencies": {
136
+ "charenc": "0.0.2",
137
+ "crypt": "0.0.2",
138
+ "is-buffer": "~1.1.6"
139
+ }
140
+ },
141
+ "node_modules/playwright": {
142
+ "version": "1.42.1",
143
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz",
144
+ "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==",
145
+ "dev": true,
146
+ "license": "Apache-2.0",
147
+ "dependencies": {
148
+ "playwright-core": "1.42.1"
149
+ },
150
+ "bin": {
151
+ "playwright": "cli.js"
152
+ },
153
+ "engines": {
154
+ "node": ">=16"
155
+ },
156
+ "optionalDependencies": {
157
+ "fsevents": "2.3.2"
158
+ }
159
+ },
160
+ "node_modules/playwright-core": {
161
+ "version": "1.42.1",
162
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz",
163
+ "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==",
164
+ "dev": true,
165
+ "license": "Apache-2.0",
166
+ "bin": {
167
+ "playwright-core": "cli.js"
168
+ },
169
+ "engines": {
170
+ "node": ">=16"
171
+ }
172
+ },
173
+ "node_modules/properties": {
174
+ "version": "1.2.1",
175
+ "resolved": "https://registry.npmjs.org/properties/-/properties-1.2.1.tgz",
176
+ "integrity": "sha512-qYNxyMj1JeW54i/EWEFsM1cVwxJbtgPp8+0Wg9XjNaK6VE/c4oRi6PNu5p7w1mNXEIQIjV5Wwn8v8Gz82/QzdQ==",
177
+ "dev": true,
178
+ "license": "MIT",
179
+ "engines": {
180
+ "node": ">=0.10"
181
+ }
182
+ },
183
+ "node_modules/strip-ansi": {
184
+ "version": "5.2.0",
185
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
186
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
187
+ "dev": true,
188
+ "license": "MIT",
189
+ "dependencies": {
190
+ "ansi-regex": "^4.1.0"
191
+ },
192
+ "engines": {
193
+ "node": ">=6"
194
+ }
195
+ },
196
+ "node_modules/undici-types": {
197
+ "version": "6.21.0",
198
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
199
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
200
+ "dev": true,
201
+ "license": "MIT"
202
+ }
203
+ }
204
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "cementic-test-template",
3
+ "version": "1.0.0",
4
+ "description": "Scaffolded project from CementicTest",
5
+ "type": "module",
6
+ "scripts": {
7
+ "test": "npx playwright test",
8
+ "test:debug": "npx playwright test --debug",
9
+ "test:headed": "npx playwright test --headed",
10
+ "test:student": "npx playwright test tests/student.spec.js",
11
+ "test:schedule": "npx playwright test tests/schedule.spec.js",
12
+ "report": "npx playwright show-report",
13
+ "allure:generate": "node node_modules/allure-commandline/bin/allure generate ./allure-results --clean -o ./allure-report",
14
+ "allure:open": "node node_modules/allure-commandline/bin/allure open ./allure-report",
15
+ "allure:serve": "node node_modules/allure-commandline/bin/allure serve ./allure-results"
16
+ },
17
+ "keywords": [
18
+ "playwright",
19
+ "automation",
20
+ "testing",
21
+ "javascript",
22
+ "allure",
23
+ "education"
24
+ ],
25
+ "author": "Testamplify Instructor",
26
+ "license": "Apache-2.0",
27
+ "devDependencies": {
28
+ "@playwright/test": "^1.57.0",
29
+ "@types/node": "^22.0.0",
30
+ "allure-commandline": "^2.36.0",
31
+ "allure-playwright": "^3.4.3"
32
+ }
33
+ }
@@ -0,0 +1,18 @@
1
+ export class LandingPage {
2
+ /**
3
+ * @param {import('@playwright/test').Page} page
4
+ */
5
+ constructor(page) {
6
+ this.page = page;
7
+ this.newArrivalsText = page.getByText('New Arrivals Just In!');
8
+ }
9
+
10
+ async goto() {
11
+ await this.page.goto('/');
12
+ }
13
+
14
+ async verifyNewArrivals() {
15
+ await this.newArrivalsText.waitFor();
16
+ return await this.newArrivalsText.isVisible();
17
+ }
18
+ }
@@ -0,0 +1,75 @@
1
+ // @ts-check
2
+ import { defineConfig, devices } from '@playwright/test';
3
+
4
+ const isCI = !!process.env.GITHUB_ACTIONS;
5
+
6
+ export default defineConfig({
7
+ testDir: './tests',
8
+
9
+ // πŸ•’ Global timeouts
10
+ timeout: 30_000,
11
+ expect: { timeout: 5_000 },
12
+
13
+ retries: isCI ? 2 : 0,
14
+
15
+ // run tests in files in parallel
16
+ fullyParallel: true,
17
+
18
+ // let Playwright choose a good number of workers (parallel runs)
19
+ // (you can override on CLI with --workers=6, etc.)
20
+ workers: isCI ? 2 : '100%',
21
+
22
+ // πŸ“¦ Test output folders
23
+ outputDir: 'test-results',
24
+
25
+ // πŸ“Š Reporters
26
+ reporter: [
27
+ ['github'],
28
+ ['html', { open: 'never' }],
29
+ ['allure-playwright', {
30
+ outputFolder: 'allure-results',
31
+ detail: true,
32
+ suiteTitle: true
33
+ }]
34
+ ],
35
+
36
+ use: {
37
+ // πŸ’» Full HD viewport (simulates full-screen desktop)
38
+ viewport: { width: 1920, height: 1080 },
39
+
40
+ // ⏱ Action & navigation timeouts
41
+ actionTimeout: 10_000,
42
+ navigationTimeout: 20_000,
43
+
44
+ // πŸŽ₯ Debug artifacts
45
+ trace: isCI ? 'retain-on-failure' : 'on-first-retry',
46
+ screenshot: 'only-on-failure',
47
+ video: 'retain-on-failure',
48
+
49
+ // 🌐 Base URL (optional)
50
+ baseURL: 'https://mini-shop.testamplify.com/',
51
+ },
52
+
53
+ // 🌍 Multi-browser projects
54
+ projects: [
55
+ {
56
+ name: 'chromium',
57
+ use: { ...devices['Desktop Chrome'] }
58
+ },
59
+ // {
60
+ // name: 'firefox',
61
+ // use: { ...devices['Desktop Firefox'] }
62
+ // },
63
+ // {
64
+ // name: 'webkit',
65
+ // use: { ...devices['Desktop Safari'] }
66
+ // },
67
+ // {
68
+ // name: 'edge',
69
+ // use: {
70
+ // ...devices['Desktop Edge'],
71
+ // channel: 'msedge', // runs real Microsoft Edge
72
+ // },
73
+ // },
74
+ ],
75
+ });
@@ -0,0 +1,10 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { LandingPage } from '../pages/LandingPage';
3
+
4
+ test.describe('Mini Shop Landing Page', () => {
5
+ test('should display New Arrivals Just In text', async ({ page }) => {
6
+ const landingPage = new LandingPage(page);
7
+ await landingPage.goto();
8
+ await expect(landingPage.newArrivalsText).toBeVisible();
9
+ });
10
+ });
@@ -0,0 +1,114 @@
1
+ name: Playwright Tests with Allure Report
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+ workflow_dispatch:
9
+
10
+ # Add permissions for GitHub Pages deployment
11
+ permissions:
12
+ contents: write
13
+ pages: write
14
+ id-token: write
15
+
16
+ jobs:
17
+ test:
18
+ timeout-minutes: 60
19
+ runs-on: ubuntu-22.04
20
+
21
+ steps:
22
+ - name: Checkout code
23
+ uses: actions/checkout@v4
24
+
25
+ - name: Setup Node.js
26
+ uses: actions/setup-node@v4
27
+ with:
28
+ node-version: "20"
29
+ cache: "npm"
30
+
31
+ - name: Install dependencies
32
+ run: npm ci
33
+
34
+ - name: Install Playwright Browsers
35
+ run: npx playwright install --with-deps chromium firefox
36
+
37
+ - name: Run Playwright tests
38
+ run: npm run test:student
39
+ continue-on-error: true
40
+
41
+ - name: Upload Playwright Report
42
+ uses: actions/upload-artifact@v4
43
+ if: always()
44
+ with:
45
+ name: playwright-report
46
+ path: playwright-report/
47
+ retention-days: 30
48
+
49
+ - name: Upload Test Results
50
+ uses: actions/upload-artifact@v4
51
+ if: always()
52
+ with:
53
+ name: test-results
54
+ path: test-results/
55
+ retention-days: 30
56
+
57
+ - name: Upload Allure Results
58
+ uses: actions/upload-artifact@v4
59
+ if: always()
60
+ with:
61
+ name: allure-results
62
+ path: allure-results/
63
+ retention-days: 30
64
+
65
+ # Generate and deploy Allure Report
66
+ allure-report:
67
+ needs: test
68
+ runs-on: ubuntu-22.04
69
+ if: always()
70
+
71
+ steps:
72
+ - name: Checkout code
73
+ uses: actions/checkout@v4
74
+
75
+ - name: Download Allure Results
76
+ uses: actions/download-artifact@v4
77
+ with:
78
+ name: allure-results
79
+ path: allure-results
80
+
81
+ - name: Setup Java (required for Allure)
82
+ uses: actions/setup-java@v4
83
+ with:
84
+ distribution: "temurin"
85
+ java-version: "17"
86
+
87
+ - name: Install Allure CLI
88
+ run: |
89
+ wget -q https://github.com/allure-framework/allure2/releases/download/2.24.1/allure-2.24.1.tgz
90
+ tar -zxf allure-2.24.1.tgz
91
+ sudo mv allure-2.24.1 /opt/allure
92
+ sudo ln -s /opt/allure/bin/allure /usr/bin/allure
93
+ allure --version
94
+
95
+ - name: Generate Allure Report
96
+ run: allure generate allure-results --clean -o allure-report
97
+
98
+ - name: Upload Allure Report
99
+ uses: actions/upload-artifact@v4
100
+ with:
101
+ name: allure-report
102
+ path: allure-report/
103
+ retention-days: 30
104
+
105
+ - name: Deploy to GitHub Pages
106
+ uses: peaceiris/actions-gh-pages@v4
107
+ if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
108
+ with:
109
+ github_token: ${{ secrets.GITHUB_TOKEN }}
110
+ publish_dir: ./allure-report
111
+ publish_branch: gh-pages
112
+ keep_files: false
113
+ user_name: "github-actions[bot]"
114
+ user_email: "github-actions[bot]@users.noreply.github.com"