mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-12 00:44:57 +03:00
Auto-build: opencode (powershell) from 6d119eb
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
// web-test cli/test-runner/assertions v1.0 — ctx.assert API
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
export function createAssertions() {
|
||||
class AssertionError extends Error {
|
||||
constructor(msg, actual, expected) {
|
||||
super(msg);
|
||||
this.name = 'AssertionError';
|
||||
this.actual = actual;
|
||||
this.expected = expected;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok(value, msg) {
|
||||
if (!value) throw new AssertionError(msg || `Expected truthy, got ${JSON.stringify(value)}`, value, true);
|
||||
},
|
||||
equal(actual, expected, msg) {
|
||||
if (actual !== expected) throw new AssertionError(msg || `Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`, actual, expected);
|
||||
},
|
||||
notEqual(actual, expected, msg) {
|
||||
if (actual === expected) throw new AssertionError(msg || `Expected not ${JSON.stringify(expected)}`, actual, expected);
|
||||
},
|
||||
deepEqual(actual, expected, msg) {
|
||||
const a = JSON.stringify(actual), b = JSON.stringify(expected);
|
||||
if (a !== b) throw new AssertionError(msg || `Deep equal failed:\n actual: ${a}\n expected: ${b}`, actual, expected);
|
||||
},
|
||||
includes(haystack, needle, msg) {
|
||||
const h = Array.isArray(haystack) ? haystack : String(haystack);
|
||||
if (!h.includes(needle)) throw new AssertionError(msg || `Expected ${JSON.stringify(h)} to include ${JSON.stringify(needle)}`, haystack, needle);
|
||||
},
|
||||
match(string, regex, msg) {
|
||||
if (!regex.test(string)) throw new AssertionError(msg || `Expected ${JSON.stringify(string)} to match ${regex}`, string, regex);
|
||||
},
|
||||
async throws(fn, msg) {
|
||||
try { await fn(); } catch { return; }
|
||||
throw new AssertionError(msg || 'Expected function to throw');
|
||||
},
|
||||
// 1C-specific
|
||||
formHasField(state, fieldName, msg) {
|
||||
if (!state?.fields?.[fieldName]) throw new AssertionError(msg || `Field "${fieldName}" not found in form. Available: ${Object.keys(state?.fields || {}).join(', ')}`, null, fieldName);
|
||||
},
|
||||
formTitle(state, expected, msg) {
|
||||
if (!state?.title?.includes(expected)) throw new AssertionError(msg || `Form title "${state?.title}" does not contain "${expected}"`, state?.title, expected);
|
||||
},
|
||||
tableHasRow(table, predicate, msg) {
|
||||
const rows = table?.rows || [];
|
||||
let found;
|
||||
if (typeof predicate === 'function') {
|
||||
found = rows.some(predicate);
|
||||
} else {
|
||||
found = rows.some(r => Object.entries(predicate).every(([k, v]) => r[k] === v));
|
||||
}
|
||||
if (!found) throw new AssertionError(msg || `No row matching predicate in table (${rows.length} rows)`, null, predicate);
|
||||
},
|
||||
tableRowCount(table, expected, msg) {
|
||||
const actual = table?.rows?.length ?? 0;
|
||||
if (actual !== expected) throw new AssertionError(msg || `Expected ${expected} rows, got ${actual}`, actual, expected);
|
||||
},
|
||||
noErrors(state, msg) {
|
||||
if (state?.errors) throw new AssertionError(msg || `Form has errors: ${JSON.stringify(state.errors)}`, state.errors, null);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// web-test cli/test-runner/discover v1.1 — test file discovery + state reset between tests
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import { existsSync, readdirSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// Accepts a single path or an array of paths (files and/or dirs). Each .test.mjs file is
|
||||
// taken directly; each directory is walked recursively (skipping _ / . prefixes). Results
|
||||
// are deduped and sorted — sorting preserves the numeric-prefix order the suite relies on
|
||||
// (00-, 01-, …) even when paths are listed out of order.
|
||||
export function discoverTests(testPaths) {
|
||||
const paths = Array.isArray(testPaths) ? testPaths : [testPaths];
|
||||
const files = [];
|
||||
function walk(dir) {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
|
||||
const full = resolve(dir, entry.name);
|
||||
if (entry.isDirectory()) walk(full);
|
||||
else if (entry.name.endsWith('.test.mjs')) files.push(full);
|
||||
}
|
||||
}
|
||||
for (const p of paths) {
|
||||
const full = resolve(p);
|
||||
if (full.endsWith('.test.mjs')) {
|
||||
if (existsSync(full)) files.push(full);
|
||||
} else if (existsSync(full)) {
|
||||
walk(full);
|
||||
}
|
||||
}
|
||||
return [...new Set(files)].sort();
|
||||
}
|
||||
|
||||
export async function resetState(ctx) {
|
||||
try { if (typeof ctx.dismissPendingErrors === 'function') await ctx.dismissPendingErrors(); } catch {}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
try {
|
||||
const state = await ctx.getFormState();
|
||||
// form === null means no form open (desktop). form === 0 is a real background form
|
||||
// 1C exposes in some states — must still close it to fully reset.
|
||||
if (state.form == null) break;
|
||||
await ctx.closeForm({ save: false });
|
||||
} catch { break; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// web-test cli/test-runner/reporters v1.0 — Allure/JUnit writers + extras sync
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import { writeFileSync, existsSync, readdirSync, copyFileSync, statSync } from 'fs';
|
||||
import { resolve, dirname, basename, relative } from 'path';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { xmlEscape } from '../util.mjs';
|
||||
import { resolveSeverity } from './severity.mjs';
|
||||
|
||||
/**
|
||||
* Copy any files from `<testDir>/_allure/` into `reportDir`. Convention for
|
||||
* Allure customization that doesn't fit inside per-test JSON:
|
||||
* - `categories.json` — failure classification (regex → bucket)
|
||||
* - `environment.properties` — values shown in the Environment widget
|
||||
* - `executor.json` — CI/CD metadata
|
||||
* Underscored folder mirrors `_hooks.mjs` convention (infra, not a test).
|
||||
* Silent if folder absent.
|
||||
*/
|
||||
export function syncAllureExtras(testDir, reportDir) {
|
||||
const extrasDir = resolve(testDir, '_allure');
|
||||
if (!existsSync(extrasDir)) return;
|
||||
try {
|
||||
if (!statSync(extrasDir).isDirectory()) return;
|
||||
} catch { return; }
|
||||
for (const entry of readdirSync(extrasDir, { withFileTypes: true })) {
|
||||
if (!entry.isFile()) continue;
|
||||
try { copyFileSync(resolve(extrasDir, entry.name), resolve(reportDir, entry.name)); }
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
}
|
||||
|
||||
export function writeAllure(results, reportDir, severityIndex) {
|
||||
for (const tr of results) {
|
||||
if (tr.status === 'skipped') continue; // Allure ignores skipped without start/stop
|
||||
const uuid = randomUUID();
|
||||
const suite = dirname(tr.file);
|
||||
const suiteLabel = (suite && suite !== '.') ? suite : 'root';
|
||||
const severity = resolveSeverity(tr, severityIndex);
|
||||
const out = {
|
||||
uuid,
|
||||
name: tr.name,
|
||||
fullName: tr.file,
|
||||
status: tr.status,
|
||||
stage: 'finished',
|
||||
start: tr.start,
|
||||
stop: tr.stop,
|
||||
labels: [
|
||||
...(tr.tags || []).map(t => ({ name: 'tag', value: t })),
|
||||
{ name: 'suite', value: suiteLabel },
|
||||
{ name: 'severity', value: severity },
|
||||
],
|
||||
steps: (tr.steps || []).map(allureStep),
|
||||
attachments: [
|
||||
...(tr.screenshot ? [{ name: 'Screenshot on failure', source: basename(tr.screenshot), type: 'image/png' }] : []),
|
||||
...(tr.video ? [{ name: 'Video', source: basename(tr.video), type: 'video/mp4' }] : []),
|
||||
],
|
||||
};
|
||||
if (tr.status === 'failed' && tr.error) {
|
||||
const traceParts = [];
|
||||
if (tr.output) traceParts.push(tr.output);
|
||||
const onecStack = tr.error.onecError?.stack?.raw;
|
||||
if (onecStack) {
|
||||
if (traceParts.length) traceParts.push('\n--- 1C stack ---\n');
|
||||
traceParts.push(onecStack);
|
||||
}
|
||||
out.statusDetails = { message: tr.error.message || '', trace: traceParts.join('') };
|
||||
}
|
||||
writeFileSync(resolve(reportDir, `${uuid}-result.json`), JSON.stringify(out, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
function allureStep(s) {
|
||||
const out = {
|
||||
name: s.name,
|
||||
status: s.status,
|
||||
stage: 'finished',
|
||||
start: s.start,
|
||||
stop: s.stop,
|
||||
steps: (s.steps || []).map(allureStep),
|
||||
};
|
||||
if (s.screenshot) {
|
||||
out.attachments = [{ name: 'Screenshot', source: basename(s.screenshot), type: 'image/png' }];
|
||||
}
|
||||
if (s.status === 'failed' && s.error) {
|
||||
out.statusDetails = { message: s.error, trace: s.error };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function buildJUnit(report, testDir) {
|
||||
const { summary, duration, tests } = report;
|
||||
const suiteName = relative(process.cwd(), testDir).replace(/\\/g, '/') || '.';
|
||||
const lines = ['<?xml version="1.0" encoding="UTF-8"?>'];
|
||||
lines.push(`<testsuites name="web-test" tests="${summary.total}" failures="${summary.failed}" skipped="${summary.skipped}" time="${duration.toFixed(3)}">`);
|
||||
lines.push(` <testsuite name="${xmlEscape(suiteName)}" tests="${summary.total}" failures="${summary.failed}" skipped="${summary.skipped}" time="${duration.toFixed(3)}">`);
|
||||
for (const t of tests) {
|
||||
const attrs = `name="${xmlEscape(t.name)}" classname="${xmlEscape(t.file)}" time="${(t.duration || 0).toFixed(3)}"`;
|
||||
if (t.status === 'passed') {
|
||||
lines.push(` <testcase ${attrs}/>`);
|
||||
} else if (t.status === 'skipped') {
|
||||
lines.push(` <testcase ${attrs}><skipped/></testcase>`);
|
||||
} else {
|
||||
lines.push(` <testcase ${attrs}>`);
|
||||
const msg = t.error?.message || '';
|
||||
const trace = t.output || '';
|
||||
lines.push(` <failure message="${xmlEscape(msg)}">${xmlEscape(trace)}</failure>`);
|
||||
if (t.screenshot) lines.push(` <system-out>screenshot: ${xmlEscape(t.screenshot)}</system-out>`);
|
||||
lines.push(` </testcase>`);
|
||||
}
|
||||
}
|
||||
lines.push(` </testsuite>`);
|
||||
lines.push(`</testsuites>`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// web-test cli/test-runner/severity v1.0 — Allure severity policy resolver
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import { die } from '../util.mjs';
|
||||
|
||||
export const SEVERITY_RANK = { blocker: 5, critical: 4, normal: 3, minor: 2, trivial: 1 };
|
||||
export const SEVERITY_LEVELS = Object.keys(SEVERITY_RANK);
|
||||
|
||||
/**
|
||||
* Validate config.severity (inverted map: severity → [tags]) at config load time.
|
||||
* Returns:
|
||||
* - tagToSeverity: Map<tag, severity> (precomputed lookup for the resolver)
|
||||
* - defaultSeverity: string (validated, defaults to 'normal')
|
||||
* Throws (via die) on invalid keys, invalid default, or duplicate tag across buckets.
|
||||
*/
|
||||
export function buildSeverityIndex(config) {
|
||||
const tagToSeverity = new Map();
|
||||
const sev = config.severity || {};
|
||||
if (typeof sev !== 'object' || Array.isArray(sev)) {
|
||||
die(`config.severity must be an object, got ${typeof sev}`);
|
||||
}
|
||||
for (const [level, tags] of Object.entries(sev)) {
|
||||
if (!SEVERITY_LEVELS.includes(level)) {
|
||||
die(`config.severity: unknown level "${level}". Allowed: ${SEVERITY_LEVELS.join('|')}`);
|
||||
}
|
||||
if (!Array.isArray(tags)) {
|
||||
die(`config.severity.${level} must be an array of tag names, got ${typeof tags}`);
|
||||
}
|
||||
for (const tag of tags) {
|
||||
if (tagToSeverity.has(tag)) {
|
||||
die(`config.severity: tag "${tag}" listed under both "${tagToSeverity.get(tag)}" and "${level}" — pick one`);
|
||||
}
|
||||
tagToSeverity.set(tag, level);
|
||||
}
|
||||
}
|
||||
const def = config.defaultSeverity || 'normal';
|
||||
if (!SEVERITY_LEVELS.includes(def)) {
|
||||
die(`config.defaultSeverity: "${def}" is not a valid level. Allowed: ${SEVERITY_LEVELS.join('|')}`);
|
||||
}
|
||||
return { tagToSeverity, defaultSeverity: def };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a test's severity. Precedence:
|
||||
* 1. explicit `export const severity` from the test module
|
||||
* 2. max-rank severity found among tags (either standard severity name, or mapped via config)
|
||||
* 3. defaultSeverity from config (or 'normal' if not set)
|
||||
* Returns one of SEVERITY_LEVELS.
|
||||
*/
|
||||
export function resolveSeverity(t, severityIndex) {
|
||||
if (t.severity) {
|
||||
if (!SEVERITY_LEVELS.includes(t.severity)) {
|
||||
return severityIndex.defaultSeverity;
|
||||
}
|
||||
return t.severity;
|
||||
}
|
||||
let best = null;
|
||||
for (const tag of t.tags || []) {
|
||||
let candidate = null;
|
||||
if (SEVERITY_LEVELS.includes(tag)) candidate = tag;
|
||||
else if (severityIndex.tagToSeverity.has(tag)) candidate = severityIndex.tagToSeverity.get(tag);
|
||||
if (candidate && (best === null || SEVERITY_RANK[candidate] > SEVERITY_RANK[best])) {
|
||||
best = candidate;
|
||||
}
|
||||
}
|
||||
return best || severityIndex.defaultSeverity;
|
||||
}
|
||||
Reference in New Issue
Block a user