@allurereport/core-api
Advanced tools
| export declare const toPosixPath: (path: string) => string; | ||
| export declare const joinPosixPath: (...parts: string[]) => string; |
| export const toPosixPath = (path) => path.replace(/\\/g, "/"); | ||
| export const joinPosixPath = (...parts) => { | ||
| const segments = parts.map(toPosixPath).join("/").split("/"); | ||
| const nonEmptySegments = []; | ||
| for (const segment of segments) { | ||
| if (segment.length > 0) { | ||
| nonEmptySegments.push(segment); | ||
| } | ||
| } | ||
| return nonEmptySegments.join("/"); | ||
| }; |
| export declare const sanitizeExternalUrl: (value: unknown) => string | undefined; |
| const ALLOWED_EXTERNAL_URL_PROTOCOLS = new Set(["http:", "https:", "mailto:", "tel:"]); | ||
| export const sanitizeExternalUrl = (value) => { | ||
| if (typeof value !== "string") { | ||
| return undefined; | ||
| } | ||
| const normalized = value.trim(); | ||
| if (normalized.length === 0) { | ||
| return undefined; | ||
| } | ||
| const schemeMatch = normalized.match(/^([A-Za-z][A-Za-z0-9+.-]*):/); | ||
| if (!schemeMatch) { | ||
| return undefined; | ||
| } | ||
| const protocol = `${schemeMatch[1].toLowerCase()}:`; | ||
| if (!ALLOWED_EXTERNAL_URL_PROTOCOLS.has(protocol)) { | ||
| return undefined; | ||
| } | ||
| try { | ||
| return new URL(normalized).toString(); | ||
| } | ||
| catch { | ||
| return undefined; | ||
| } | ||
| }; |
@@ -41,2 +41,3 @@ import type { Statistic } from "./aggregate.js"; | ||
| export type CategoryRule = { | ||
| id?: string; | ||
| name: string; | ||
@@ -59,2 +60,3 @@ matchers?: CategoryMatcher; | ||
| export interface CategoryDefinition extends Pick<CategoryRule, "name" | "expand" | "hide" | "groupEnvironments"> { | ||
| id: string; | ||
| matchers: Matcher[]; | ||
@@ -61,0 +63,0 @@ groupBy: CategoryGroupSelector[]; |
+42
-2
@@ -35,2 +35,24 @@ export const EMPTY_VALUE = "<Empty>"; | ||
| const isMatcherArray = (value) => Array.isArray(value); | ||
| const hasControlChars = (value) => { | ||
| for (let index = 0; index < value.length; index++) { | ||
| const code = value.charCodeAt(index); | ||
| if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| }; | ||
| const normalizeCategoryId = (id) => { | ||
| if (typeof id !== "string") { | ||
| return { valid: false, reason: "id must be a string" }; | ||
| } | ||
| const normalized = id.trim(); | ||
| if (normalized.length === 0) { | ||
| return { valid: false, reason: "id must not be empty" }; | ||
| } | ||
| if (hasControlChars(normalized)) { | ||
| return { valid: false, reason: "id must not contain control characters" }; | ||
| } | ||
| return { valid: true, normalized }; | ||
| }; | ||
| const normalizeMatchers = (rule, index) => { | ||
@@ -89,2 +111,3 @@ const compatKeysUsed = rule.matchedStatuses !== undefined || | ||
| const seen = new Map(); | ||
| const sourceIdsByNormalizedId = new Map(); | ||
| const applyRule = (rule, index) => { | ||
@@ -97,4 +120,12 @@ if (!isPlainObject(rule)) { | ||
| } | ||
| const idValidationResult = normalizeCategoryId(rule.id ?? rule.name); | ||
| if (!idValidationResult.valid) { | ||
| throw new Error(`categories[${index}].id ${idValidationResult.reason}`); | ||
| } | ||
| const normalizedId = idValidationResult.normalized; | ||
| const sourceIds = sourceIdsByNormalizedId.get(normalizedId) ?? new Set(); | ||
| sourceIds.add(rule.id ?? rule.name); | ||
| sourceIdsByNormalizedId.set(normalizedId, sourceIds); | ||
| const matchers = normalizeMatchers(rule, index); | ||
| const existing = seen.get(rule.name); | ||
| const existing = seen.get(normalizedId); | ||
| if (existing) { | ||
@@ -124,2 +155,3 @@ existing.matchers.push(...matchers); | ||
| const norm = { | ||
| id: normalizedId, | ||
| name: rule.name, | ||
@@ -134,3 +166,3 @@ matchers, | ||
| }; | ||
| seen.set(rule.name, norm); | ||
| seen.set(normalizedId, norm); | ||
| normalized.push(norm); | ||
@@ -140,2 +172,10 @@ }; | ||
| DEFAULT_ERROR_CATEGORIES.forEach((rule, index) => applyRule(rule, rules.length + index)); | ||
| sourceIdsByNormalizedId.forEach((sourceIds, normalizedId) => { | ||
| if (sourceIds.size <= 1) { | ||
| return; | ||
| } | ||
| throw new Error(`categories: normalized id ${JSON.stringify(normalizedId)} is produced by source ids [${Array.from(sourceIds) | ||
| .map((id) => JSON.stringify(id)) | ||
| .join(",")}]`); | ||
| }); | ||
| return normalized; | ||
@@ -142,0 +182,0 @@ }; |
@@ -6,2 +6,3 @@ import type { Statistic } from "./aggregate.js"; | ||
| export declare const severityLabelName = "severity"; | ||
| export declare const fallbackTestCaseIdLabelName = "_fallbackTestCaseId"; | ||
| export declare const unsuccessfulStatuses: Set<TestStatus>; | ||
@@ -8,0 +9,0 @@ export declare const successfulStatuses: Set<TestStatus>; |
| export const statusesList = ["failed", "broken", "passed", "skipped", "unknown"]; | ||
| export const severityLevels = ["blocker", "critical", "normal", "minor", "trivial"]; | ||
| export const severityLabelName = "severity"; | ||
| export const fallbackTestCaseIdLabelName = "_fallbackTestCaseId"; | ||
| export const unsuccessfulStatuses = new Set(["failed", "broken"]); | ||
@@ -5,0 +6,0 @@ export const successfulStatuses = new Set(["passed"]); |
@@ -6,2 +6,6 @@ import type { TestLabel } from "./metadata.js"; | ||
| } | ||
| export interface EnvironmentIdentity { | ||
| id: string; | ||
| name: string; | ||
| } | ||
| export type ReportVariables = Record<string, string>; | ||
@@ -12,2 +16,3 @@ export type EnvironmentMatcherPayload = { | ||
| export type EnvironmentDescriptor = { | ||
| name?: string; | ||
| variables?: ReportVariables; | ||
@@ -14,0 +19,0 @@ matcher: (payload: EnvironmentMatcherPayload) => boolean; |
+2
-0
@@ -26,1 +26,3 @@ export type * from "./aggregate.js"; | ||
| export * from "./utils/dictionary.js"; | ||
| export * from "./utils/path.js"; | ||
| export * from "./utils/url.js"; |
+2
-0
@@ -16,1 +16,3 @@ export * from "./constants.js"; | ||
| export * from "./utils/dictionary.js"; | ||
| export * from "./utils/path.js"; | ||
| export * from "./utils/url.js"; |
+1
-0
@@ -9,2 +9,3 @@ export declare const createScriptTag: (src: string, options?: { | ||
| export declare const createBaseUrlScript: () => string; | ||
| export declare const stringifyForInlineScript: (value: unknown) => string; | ||
| export declare const createReportDataScript: (reportFiles?: { | ||
@@ -11,0 +12,0 @@ name: string; |
+11
-1
@@ -26,2 +26,10 @@ export const createScriptTag = (src, options) => { | ||
| }; | ||
| export const stringifyForInlineScript = (value) => { | ||
| return JSON.stringify(value) | ||
| .replaceAll("<", "\\u003C") | ||
| .replaceAll(">", "\\u003E") | ||
| .replaceAll("&", "\\u0026") | ||
| .replaceAll("\u2028", "\\u2028") | ||
| .replaceAll("\u2029", "\\u2029"); | ||
| }; | ||
| export const createReportDataScript = (reportFiles = []) => { | ||
@@ -35,3 +43,5 @@ if (!reportFiles?.length) { | ||
| } | ||
| const reportFilesDeclaration = reportFiles.map(({ name, value }) => `d('${name}','${value}')`).join(","); | ||
| const reportFilesDeclaration = reportFiles | ||
| .map(({ name, value }) => `d(${JSON.stringify(name)},${JSON.stringify(value)})`) | ||
| .join(","); | ||
| return ` | ||
@@ -38,0 +48,0 @@ <script async> |
@@ -1,5 +0,18 @@ | ||
| import type { EnvironmentsConfig } from "../environment.js"; | ||
| import type { TestEnvGroup, TestResult } from "../model.js"; | ||
| import type { EnvironmentIdentity } from "../environment.js"; | ||
| import type { TestEnvGroup } from "../model.js"; | ||
| export declare const DEFAULT_ENVIRONMENT = "default"; | ||
| export declare const matchEnvironment: (envConfig: EnvironmentsConfig, tr: Pick<TestResult, "labels">) => string; | ||
| export declare const MAX_ENVIRONMENT_NAME_LENGTH = 64; | ||
| export declare const MAX_ENVIRONMENT_ID_LENGTH = 64; | ||
| export declare const DEFAULT_ENVIRONMENT_IDENTITY: EnvironmentIdentity; | ||
| export type EnvironmentValidationResult = { | ||
| valid: true; | ||
| normalized: string; | ||
| } | { | ||
| valid: false; | ||
| reason: string; | ||
| }; | ||
| export declare const validateEnvironmentName: (name: unknown) => EnvironmentValidationResult; | ||
| export declare const validateEnvironmentId: (environmentId: unknown) => EnvironmentValidationResult; | ||
| export declare const assertValidEnvironmentName: (name: unknown, source?: string) => string; | ||
| export declare const formatNormalizedEnvironmentCollision: (sourcePath: string, normalized: string, originalKeys: string[]) => string; | ||
| export declare const getRealEnvsCount: (group: TestEnvGroup) => number; |
| export const DEFAULT_ENVIRONMENT = "default"; | ||
| export const matchEnvironment = (envConfig, tr) => { | ||
| return (Object.entries(envConfig).find(([, { matcher }]) => matcher({ labels: tr.labels }))?.[0] ?? DEFAULT_ENVIRONMENT); | ||
| export const MAX_ENVIRONMENT_NAME_LENGTH = 64; | ||
| export const MAX_ENVIRONMENT_ID_LENGTH = 64; | ||
| export const DEFAULT_ENVIRONMENT_IDENTITY = { | ||
| id: DEFAULT_ENVIRONMENT, | ||
| name: DEFAULT_ENVIRONMENT, | ||
| }; | ||
| const hasControlChars = (value) => { | ||
| for (let i = 0; i < value.length; i++) { | ||
| const code = value.charCodeAt(i); | ||
| if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| }; | ||
| const hasPathLikeSegments = (value) => { | ||
| if (value.includes("/") || value.includes("\\")) { | ||
| return true; | ||
| } | ||
| return value === "." || value === ".."; | ||
| }; | ||
| export const validateEnvironmentName = (name) => { | ||
| if (typeof name !== "string") { | ||
| return { valid: false, reason: "name must be a string" }; | ||
| } | ||
| const normalized = name.trim(); | ||
| if (normalized.length === 0) { | ||
| return { valid: false, reason: "name must not be empty" }; | ||
| } | ||
| if (normalized.length > MAX_ENVIRONMENT_NAME_LENGTH) { | ||
| return { | ||
| valid: false, | ||
| reason: `name must not exceed ${MAX_ENVIRONMENT_NAME_LENGTH} characters`, | ||
| }; | ||
| } | ||
| if (hasControlChars(normalized)) { | ||
| return { valid: false, reason: "name must not contain control characters" }; | ||
| } | ||
| if (hasPathLikeSegments(normalized)) { | ||
| return { valid: false, reason: "name must not contain path-like segments" }; | ||
| } | ||
| return { valid: true, normalized }; | ||
| }; | ||
| export const validateEnvironmentId = (environmentId) => { | ||
| if (typeof environmentId !== "string") { | ||
| return { valid: false, reason: "id must be a string" }; | ||
| } | ||
| const normalized = environmentId.trim(); | ||
| if (normalized.length === 0) { | ||
| return { valid: false, reason: "id must not be empty" }; | ||
| } | ||
| if (normalized.length > MAX_ENVIRONMENT_ID_LENGTH) { | ||
| return { | ||
| valid: false, | ||
| reason: `id must not exceed ${MAX_ENVIRONMENT_ID_LENGTH} characters`, | ||
| }; | ||
| } | ||
| if (!/^[A-Za-z0-9_-]+$/.test(normalized)) { | ||
| return { | ||
| valid: false, | ||
| reason: "id must contain only latin letters, digits, underscores, and hyphens", | ||
| }; | ||
| } | ||
| return { valid: true, normalized }; | ||
| }; | ||
| export const assertValidEnvironmentName = (name, source = "environment name") => { | ||
| const validationResult = validateEnvironmentName(name); | ||
| if (!validationResult.valid) { | ||
| throw new Error(`Invalid ${source} ${JSON.stringify(name)}: ${validationResult.reason}`); | ||
| } | ||
| return validationResult.normalized; | ||
| }; | ||
| export const formatNormalizedEnvironmentCollision = (sourcePath, normalized, originalKeys) => `${sourcePath}: normalized key ${JSON.stringify(normalized)} is produced by original keys [${originalKeys.map((key) => JSON.stringify(key)).join(",")}]`; | ||
| export const getRealEnvsCount = (group) => { | ||
| const { testResultsByEnv = {} } = group ?? {}; | ||
| const envsCount = Object.keys(testResultsByEnv).length ?? 0; | ||
| const envsCount = Object.keys(testResultsByEnv).length; | ||
| if (envsCount <= 1 && DEFAULT_ENVIRONMENT in testResultsByEnv) { | ||
@@ -9,0 +79,0 @@ return 0; |
| import type { HistoryDataPoint, HistoryTestResult } from "../history.js"; | ||
| import type { TestParameter } from "../metadata.js"; | ||
| import type { TestResult } from "../model.js"; | ||
| export declare const stringifyHistoryParams: (parameters?: TestParameter[]) => string; | ||
| export declare const getFallbackHistoryId: (tr: Pick<TestResult, "labels" | "parameters">) => string | undefined; | ||
| export declare const getHistoryIdCandidates: (tr: Pick<TestResult, "historyId" | "labels" | "parameters">) => string[]; | ||
| export declare const filterUnknownByKnownIssues: (trs: TestResult[], knownIssueHistoryIds: ReadonlySet<string>) => TestResult[]; | ||
| export declare const normalizeHistoryDataPointUrls: (historyDataPoint: HistoryDataPoint) => HistoryDataPoint; | ||
| export declare const selectHistoryTestResults: (historyDataPoints: HistoryDataPoint[], historyIdCandidates: readonly string[]) => HistoryTestResult[]; | ||
| export declare const htrsByTr: (hdps: HistoryDataPoint[], tr: TestResult | HistoryTestResult) => HistoryTestResult[]; |
+82
-15
@@ -1,19 +0,80 @@ | ||
| export const htrsByTr = (hdps, tr) => { | ||
| if (!tr?.historyId) { | ||
| import { createHash } from "node:crypto"; | ||
| import { fallbackTestCaseIdLabelName } from "../constants.js"; | ||
| import { findLastByLabelName } from "./label.js"; | ||
| const md5 = (data) => createHash("md5").update(data).digest("hex"); | ||
| const parametersCompare = (a, b) => { | ||
| return (a.name ?? "").localeCompare(b.name ?? "") || (a.value ?? "").localeCompare(b.value ?? ""); | ||
| }; | ||
| export const stringifyHistoryParams = (parameters = []) => { | ||
| return [...parameters] | ||
| .filter((parameter) => !parameter?.excluded) | ||
| .sort(parametersCompare) | ||
| .map((parameter) => `${parameter.name}:${parameter.value}`) | ||
| .join(","); | ||
| }; | ||
| export const getFallbackHistoryId = (tr) => { | ||
| const fallbackTestCaseId = findLastByLabelName(tr.labels ?? [], fallbackTestCaseIdLabelName); | ||
| if (!fallbackTestCaseId) { | ||
| return undefined; | ||
| } | ||
| return `${fallbackTestCaseId}.${md5(stringifyHistoryParams(tr.parameters ?? []))}`; | ||
| }; | ||
| export const getHistoryIdCandidates = (tr) => { | ||
| const result = []; | ||
| if (tr.historyId) { | ||
| result.push(tr.historyId); | ||
| } | ||
| const fallbackHistoryId = getFallbackHistoryId(tr); | ||
| if (fallbackHistoryId && !result.includes(fallbackHistoryId)) { | ||
| result.push(fallbackHistoryId); | ||
| } | ||
| return result; | ||
| }; | ||
| export const filterUnknownByKnownIssues = (trs, knownIssueHistoryIds) => { | ||
| return trs.filter((tr) => { | ||
| const historyIdCandidates = getHistoryIdCandidates(tr); | ||
| if (historyIdCandidates.length === 0) { | ||
| return true; | ||
| } | ||
| return historyIdCandidates.every((historyId) => !knownIssueHistoryIds.has(historyId)); | ||
| }); | ||
| }; | ||
| export const normalizeHistoryDataPointUrls = (historyDataPoint) => { | ||
| const { url } = historyDataPoint; | ||
| if (!url) { | ||
| return historyDataPoint; | ||
| } | ||
| let testResults = historyDataPoint.testResults; | ||
| for (const [historyId, historyTestResult] of Object.entries(historyDataPoint.testResults)) { | ||
| if (historyTestResult.url) { | ||
| continue; | ||
| } | ||
| if (testResults === historyDataPoint.testResults) { | ||
| testResults = { ...historyDataPoint.testResults }; | ||
| } | ||
| testResults[historyId] = { | ||
| ...historyTestResult, | ||
| url, | ||
| }; | ||
| } | ||
| if (testResults === historyDataPoint.testResults) { | ||
| return historyDataPoint; | ||
| } | ||
| return { | ||
| ...historyDataPoint, | ||
| testResults, | ||
| }; | ||
| }; | ||
| export const selectHistoryTestResults = (historyDataPoints, historyIdCandidates) => { | ||
| if (historyIdCandidates.length === 0) { | ||
| return []; | ||
| } | ||
| return hdps.reduce((acc, dp) => { | ||
| const htr = dp.testResults[tr.historyId]; | ||
| if (htr) { | ||
| if (dp.url) { | ||
| const url = new URL(dp.url); | ||
| url.hash = tr.id; | ||
| acc.push({ | ||
| ...htr, | ||
| url: url.toString(), | ||
| }); | ||
| return historyDataPoints.reduce((acc, historyDataPoint) => { | ||
| for (const historyId of historyIdCandidates) { | ||
| const historyTestResult = historyDataPoint.testResults[historyId]; | ||
| if (!historyTestResult) { | ||
| continue; | ||
| } | ||
| else { | ||
| acc.push(htr); | ||
| } | ||
| acc.push(historyTestResult); | ||
| break; | ||
| } | ||
@@ -23,1 +84,7 @@ return acc; | ||
| }; | ||
| export const htrsByTr = (hdps, tr) => { | ||
| if (!tr?.historyId) { | ||
| return []; | ||
| } | ||
| return selectHistoryTestResults(hdps, [tr.historyId]); | ||
| }; |
| import type { TestLabel } from "../index.js"; | ||
| export declare const findByLabelName: (labels: TestLabel[], name: string) => string | undefined; | ||
| export declare const findLastByLabelName: (labels: TestLabel[], name: string) => string | undefined; | ||
| export declare const shouldHideLabel: (labelName: string, matchers?: readonly (string | RegExp)[]) => boolean; |
+11
-0
@@ -12,1 +12,12 @@ export const findByLabelName = (labels, name) => { | ||
| }; | ||
| export const shouldHideLabel = (labelName, matchers = []) => { | ||
| if (labelName.startsWith("_")) { | ||
| return true; | ||
| } | ||
| return matchers.some((matcher) => { | ||
| if (typeof matcher === "string") { | ||
| return matcher === labelName; | ||
| } | ||
| return new RegExp(matcher.source, matcher.flags).test(labelName); | ||
| }); | ||
| }; |
+11
-22
| { | ||
| "name": "@allurereport/core-api", | ||
| "version": "3.3.1", | ||
| "version": "3.4.0", | ||
| "description": "Allure Core API", | ||
@@ -8,21 +8,21 @@ "keywords": [ | ||
| ], | ||
| "repository": "https://github.com/allure-framework/allure3", | ||
| "license": "Apache-2.0", | ||
| "author": "Qameta Software", | ||
| "repository": "https://github.com/allure-framework/allure3", | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "type": "module", | ||
| "main": "./dist/index.js", | ||
| "module": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "exports": { | ||
| ".": "./dist/index.js" | ||
| }, | ||
| "main": "./dist/index.js", | ||
| "module": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "scripts": { | ||
| "build": "run clean && tsc --project ./tsconfig.json", | ||
| "clean": "rimraf ./dist", | ||
| "eslint": "eslint ./src/**/*.{js,jsx,ts,tsx}", | ||
| "eslint:format": "eslint --fix ./src/**/*.{js,jsx,ts,tsx}", | ||
| "test": "rimraf ./out && vitest run" | ||
| "test": "rimraf ./out && vitest run", | ||
| "lint": "oxlint --import-plugin src test features stories", | ||
| "lint:fix": "oxlint --import-plugin --fix src test features stories" | ||
| }, | ||
@@ -33,18 +33,7 @@ "dependencies": { | ||
| "devDependencies": { | ||
| "@stylistic/eslint-plugin": "^2.6.1", | ||
| "@types/d3-shape": "^3.1.6", | ||
| "@types/eslint": "^8.56.11", | ||
| "@types/node": "^20.17.9", | ||
| "@typescript-eslint/eslint-plugin": "^8.0.0", | ||
| "@typescript-eslint/parser": "^8.0.0", | ||
| "@vitest/runner": "^2.1.9", | ||
| "@vitest/snapshot": "^2.1.9", | ||
| "allure-vitest": "^3.3.3", | ||
| "eslint": "^8.57.0", | ||
| "eslint-config-prettier": "^9.1.0", | ||
| "eslint-plugin-import": "^2.29.1", | ||
| "eslint-plugin-jsdoc": "^50.0.0", | ||
| "eslint-plugin-n": "^17.10.1", | ||
| "eslint-plugin-no-null": "^1.0.2", | ||
| "eslint-plugin-prefer-arrow": "^1.2.3", | ||
| "rimraf": "^6.0.1", | ||
@@ -51,0 +40,0 @@ "typescript": "^5.6.3", |
51118
25.1%8
-57.89%58
7.41%1354
25.02%