UNPKG

playwright-bdd

Version:
192 lines 8.42 kB
"use strict"; /** * Executed hooks of test run. * * In Playwright: * worker-level hooks (BeforeAll / AfterAll) are still part of * particular test results. E.g. BeforeAll is reported as a step of the first test in a worker. * * In Cucumber: * Worker-level hooks are not considered to be a part of any test case. * They will be reported as separate messages TestRunHookStarted / TestRunHookFinished. * See: https://github.com/cucumber/messages/pull/102 * * In playwright-bdd: * We do map worker-level hooks to BEFORE_TEST_RUN / AFTER_TEST_RUN hook types, * but we don't emit TestRunHookStarted / TestRunHookFinished messages. * Instead, these hooks are attached to per-scenario testCase steps in the report. * This could be changed in the future, when cucumber-js will add it. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.TestCaseRunHooks = void 0; exports.isHookCandidate = isHookCandidate; const Hook_1 = require("./Hook"); const pwStepUtils_1 = require("./pwStepUtils"); const TestStepRun_1 = require("./TestStepRun"); const PW_STEP_CATEGORIES_FOR_HOOKS = ['hook', 'fixture', 'test.step']; function isHookCandidate(pwStep) { return PW_STEP_CATEGORIES_FOR_HOOKS.includes(pwStep.category); } class TestCaseRunHooks { static getRootPwStep(result, hookType) { return (0, pwStepUtils_1.getHooksRootPwStep)(result.result, hookType); } constructor(testCaseRun, hookType, bgRoots) { this.testCaseRun = testCaseRun; this.hookType = hookType; this.bgRoots = bgRoots; this.candidates = []; this.hookPwSteps = new Set(); this.executedHooks = new Map(); } get isAfterHooksType() { return this.hookType === 'after'; } fill() { this.setRootPwStep(); // todo: refactor to this early return if (!this.rootPwStep) return this; this.setCandidates(); this.addStepsWithName(); this.addStepWithTimeout(); this.addStepWithError(); this.addStepsWithAttachment(); if (this.isAfterHooksType) { this.addUnprocessedErrors(); this.addUnprocessedAttachments(); } this.setExecutedHooks(); return this; } buildMessages() { const messages = []; this.testCaseRun .getTestCase() .getHooks(this.hookType) .forEach((hookInfo) => { const executedHook = this.executedHooks.get(hookInfo.hook.internalId); // If no pwStep is found for this hook, it means the hook was not executed in this attempt. // This happens for BEFORE_TEST_RUN (BeforeAll) hooks on retries: PW credits BeforeAll to the // first scenario in each worker. When a later scenario is retried, its attempt 1 runs BeforeAll // in the new worker (and has a real pwStep), which causes TestCase.addHooks() to add BeforeAll // to the shared TestCase object. buildMessages() for attempt 0 then finds BeforeAll in the // TestCase but has no matching pwStep. Passing undefined pwStep lets TestStepRun emit SKIPPED, // which correctly reflects that the hook ran for a different scenario in the same worker. const pwStep = executedHook?.pwStep; const testStepRun = new TestStepRun_1.TestStepRun(this.testCaseRun, hookInfo.testStep, pwStep); messages.push(...testStepRun.buildMessages()); }); return messages; } setRootPwStep() { // todo: refactor to better handle empty root step (e.g. skipped test) this.rootPwStep = (0, pwStepUtils_1.getHooksRootPwStep)(this.testCaseRun.result, this.hookType); } setCandidates() { // collect steps, not entering into bg roots this.candidates = (0, pwStepUtils_1.walkSteps)(this.rootPwStep, (pwStep) => !this.bgRoots.has(pwStep)).filter((pwStep) => isHookCandidate(pwStep)); } addStepsWithName() { this.candidates .filter((pwStep) => pwStep.category === 'test.step' && pwStep.title) .forEach((pwStep) => this.hookPwSteps.add(pwStep)); } addStepWithTimeout() { if (!this.testCaseRun.isTimeouted()) return; if (this.testCaseRun.timeoutedStep) return; // Search timeouted step by duration = -1. // This is not 100% method, sometimes timeouted steps have real duration value. // But allows to better place timeout error message in report. const timeoutedStep = (0, pwStepUtils_1.findDeepestStepWith)(this.candidates, (pwStep) => { return (0, pwStepUtils_1.isUnknownDuration)(pwStep); }); if (!timeoutedStep) return; this.hookPwSteps.add(timeoutedStep); this.testCaseRun.registerTimeoutedStep(timeoutedStep); } addStepWithError() { // In case of several errors in after hooks (as they all run), // parent pwStep and test result contain only the last error, // but each hook step itself contains own error. // Here we find only first step with error. // Todo: find and show all errors for after hooks. const stepWithError = (0, pwStepUtils_1.findDeepestStepWith)(this.candidates, (pwStep) => Boolean(pwStep.error)); if (!stepWithError) return; const error = stepWithError.error; // If step is already added to errorSteps, don't register it as hookStep. // This is mainly for timeout steps in hooks or bg: // They have duration -1 and no 'error' field, but their parent has 'error' field. // Here we find this parent again and avoid reporting the error twice. if (!this.testCaseRun.hasRegisteredError(error)) { this.registerErrorStep(stepWithError, error); } } /** * If there are unprocessed errors, attach them to After Hooks root step. */ addUnprocessedErrors() { const unprocessedErrors = this.testCaseRun.getUnprocessedErrors(); if (unprocessedErrors.length === 0) return; const error = buildFallbackError(unprocessedErrors); // if there is timeouted step without attached error, attach all unprocessed errors to it, // otherwise attach unprocessed error to root 'After Hooks' step. const { timeoutedStep } = this.testCaseRun; if (timeoutedStep && !this.testCaseRun.getStepError(timeoutedStep)) { this.testCaseRun.registerErrorStep(timeoutedStep, error); } else { this.registerErrorStep(this.rootPwStep, error); } } addStepsWithAttachment() { const { attachmentMapper } = this.testCaseRun; this.candidates.forEach((pwStep) => { const attachments = attachmentMapper.populateStepAttachments(pwStep, { fromHook: true }); if (attachments.length > 0) this.hookPwSteps.add(pwStep); }); } addUnprocessedAttachments() { const { attachmentMapper } = this.testCaseRun; if (attachmentMapper.hasUnprocessedAttachments()) { attachmentMapper.mapUnprocessedAttachments(this.rootPwStep); this.hookPwSteps.add(this.rootPwStep); } } setExecutedHooks() { this.hookPwSteps.forEach((pwStep) => { const internalId = Hook_1.Hook.getInternalId(pwStep); const hook = this.getOrRegisterHook(pwStep); this.executedHooks.set(internalId, { hook, pwStep }); }); } getOrRegisterHook(pwStep) { const internalId = Hook_1.Hook.getInternalId(pwStep); return this.testCaseRun.hooks.getOrCreate(internalId, () => new Hook_1.Hook(internalId, this.hookType, pwStep)); } registerErrorStep(pwStep, error) { this.hookPwSteps.add(pwStep); this.testCaseRun.registerErrorStep(pwStep, error); } } exports.TestCaseRunHooks = TestCaseRunHooks; function buildFallbackError(unprocessedErrors) { const firstError = unprocessedErrors[0]; return firstError && unprocessedErrors.length === 1 ? firstError : buildConcatenatedError(unprocessedErrors); } function buildConcatenatedError(errors) { const message = errors .map((e) => e.message) .filter(Boolean) .join('\n\n'); return { message }; } //# sourceMappingURL=TestCaseRunHooks.js.map