@g_package/jest-cucumber-fusion 2.0.0 → 3.0.0
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.
- package/README.md +444 -38
- package/dist/THIRD_PARTY_LICENSES.txt +91 -0
- package/dist/index.cjs +9917 -0
- package/dist/index.d.cts +145 -0
- package/package.json +60 -10
- package/scripts/prepare-hooks.js +23 -0
- package/src/code-suggestion.js +240 -0
- package/src/configuration.js +147 -0
- package/src/feature-source.js +322 -0
- package/src/index.d.ts +123 -13
- package/src/index.js +153 -420
- package/src/keywords.js +56 -0
- package/src/scenario-name.js +128 -0
- package/src/shared-state.js +36 -0
- package/src/step-argument.js +56 -0
- package/src/step-matching.js +89 -0
- package/src/tag-filter.js +90 -0
- package/src/test-registration.js +178 -0
- package/src/value-description.js +26 -0
- package/.prettierignore +0 -16
- package/.prettierrc.json +0 -0
- package/codecov +0 -0
- package/codecov.SHA256SUM +0 -1
- package/codecov.SHA256SUM.sig +0 -16
- package/docs/AdditionalConfiguration.md +0 -155
- package/docs/GherkinTables.md +0 -61
- package/docs/Language.md +0 -76
- package/docs/ReusingStepDefinitions.md +0 -110
- package/docs/RunningTheExamples.md +0 -16
- package/docs/ScenarioOutlines.md +0 -43
- package/docs/StepDefinitionArguments.md +0 -37
- package/docs/product/expectations/fix-l2-outline-regex/outline-regex-binds.md +0 -148
- package/docs/product/expectations/fix-l4-escaped-parens-outline/escaped-parens-bind-in-outlines.md +0 -106
- package/docs/product/expectations/fix-m3-singleton-reset/clean-slate-per-feature.md +0 -133
- package/test/specs/features/basic-scenarios.feature +0 -28
- package/test/specs/features/l3-step-argument-delivery.feature +0 -44
- package/test/specs/features/language.feature +0 -41
- package/test/specs/features/m6-hooks-once-per-test.feature +0 -21
- package/test/specs/features/m6-hooks-outline-only.feature +0 -12
- package/test/specs/features/reuse-definition.feature +0 -13
- package/test/specs/features/scenario-outline2.feature +0 -73
- package/test/specs/features/scenario-outlines.feature +0 -88
- package/test/specs/features/step-definitions/ambiguous-step-shadowing.steps.js +0 -92
- package/test/specs/features/step-definitions/basic-scenarios.steps.js +0 -62
- package/test/specs/features/step-definitions/fuzz-properties.steps.js +0 -170
- package/test/specs/features/step-definitions/hook-error.steps.js +0 -59
- package/test/specs/features/step-definitions/l2-outline-edge-cases.steps.js +0 -327
- package/test/specs/features/step-definitions/l3-step-argument-delivery.steps.js +0 -191
- package/test/specs/features/step-definitions/l4-escaped-parens-outline.steps.js +0 -256
- package/test/specs/features/step-definitions/language.steps.js +0 -86
- package/test/specs/features/step-definitions/m1-before-hooks-clobber.steps.js +0 -64
- package/test/specs/features/step-definitions/m2-duplicate-matcher.steps.js +0 -60
- package/test/specs/features/step-definitions/m3-singleton-reset.steps.js +0 -275
- package/test/specs/features/step-definitions/m4-callsite-resolution.steps.js +0 -76
- package/test/specs/features/step-definitions/m5-errors-false-silent-skip.steps.js +0 -80
- package/test/specs/features/step-definitions/m6-hooks-once-per-test.steps.js +0 -90
- package/test/specs/features/step-definitions/missing-feature-file.steps.js +0 -23
- package/test/specs/features/step-definitions/reuse-code.js +0 -22
- package/test/specs/features/step-definitions/reuse-definition.steps.js +0 -30
- package/test/specs/features/step-definitions/scenario-outline2.steps.js +0 -57
- package/test/specs/features/step-definitions/scenario-outlines.steps.js +0 -110
- package/test/specs/features/step-definitions/undefined-step.steps.js +0 -39
- package/test/specs/features/step-definitions/using-dynamic-values.steps.js +0 -70
- package/test/specs/features/step-definitions/using-gherkin-tables.steps.js +0 -42
- package/test/specs/features/undefined-step.feature +0 -4
- package/test/specs/features/using-dynamic-values.feature +0 -34
- package/test/specs/features/using-gherkin-tables.feature +0 -16
- package/test/src/bank-account.js +0 -17
- package/test/src/online-sales.js +0 -33
- package/test/src/rocket.js +0 -13
- package/test/src/todo-list.js +0 -20
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
Feature: Online sales
|
|
2
|
-
|
|
3
|
-
Scenario Outline: Selling an <Item>
|
|
4
|
-
Given I have a(n) <Item>
|
|
5
|
-
When I sell the <Item>
|
|
6
|
-
Then I should get $<Amount>
|
|
7
|
-
|
|
8
|
-
Examples:
|
|
9
|
-
|
|
10
|
-
| Item | Amount |
|
|
11
|
-
| Autographed Neil deGrasse Tyson book | 100 |
|
|
12
|
-
| Rick Astley t-shirt | 22 |
|
|
13
|
-
| An idea to replace EVERYTHING with blockchains | 0 |
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Scenario Outline: Selling all of one
|
|
17
|
-
Given I want to sell all my <Item>
|
|
18
|
-
When I sell all my <Item> at the price of $<Amount> CAD
|
|
19
|
-
Then I should still get $<Amount>
|
|
20
|
-
|
|
21
|
-
Examples:
|
|
22
|
-
|
|
23
|
-
| Item | Amount |
|
|
24
|
-
| Autographed Neil deGrasse Tyson book | 100 |
|
|
25
|
-
| Rick Astley t-shirt | 22 |
|
|
26
|
-
| An idea to replace EVERYTHING with blockchains | 0 |
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Scenario: Selling a thing
|
|
30
|
-
Given I have an Item named '<ThatCouldLookLikeAnOutlineVariable>'
|
|
31
|
-
When I sell <ThatCouldLookLikeAnOutlineVariable With Spaces in it>
|
|
32
|
-
Then I get $<Amount>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
Scenario Outline: Additional regexp in outline line
|
|
36
|
-
Given I want to sell all my <Item>
|
|
37
|
-
When I sell all my <Item> at the price of $100 CAD
|
|
38
|
-
Then I should still get $<Amount>
|
|
39
|
-
|
|
40
|
-
Examples:
|
|
41
|
-
| Item | Amount |
|
|
42
|
-
| Autographed Neil deGrasse Tyson book | 100 |
|
|
43
|
-
| Rick Astley t-shirt | 100 |
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
Scenario Outline: Additional regexp in outline line with non-string variables for <Item>
|
|
47
|
-
Given I want to sell all my <Item>
|
|
48
|
-
When I sell all my <Item> with a starting price of $<StartingPrice> at the rebate price of $100
|
|
49
|
-
Then I should still get $<Amount>
|
|
50
|
-
|
|
51
|
-
Examples:
|
|
52
|
-
| Item | StartingPrice | Amount |
|
|
53
|
-
| Autographed Neil deGrasse Tyson book | 100 | 100 |
|
|
54
|
-
| Rick Astley t-shirt | 22 | 100 |
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
Scenario Outline: Additional regexp in outline line with non-string variables and static one in the middle for <Item>
|
|
58
|
-
Given I want to sell all my <Item>
|
|
59
|
-
When I sell all my <Item> with a starting price of $100 at the rebate price of $<RebatePrice>
|
|
60
|
-
Then I should still get $<Amount>
|
|
61
|
-
|
|
62
|
-
Examples:
|
|
63
|
-
| Item | RebatePrice | Amount |
|
|
64
|
-
| Autographed Neil deGrasse Tyson book | 20 | 20 |
|
|
65
|
-
| Rick Astley t-shirt | 50 | 50 |
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
Scenario Outline: Additional regepx in outline line with non-string variable at the end for <Item>
|
|
69
|
-
Given I want to sell all my <Item>
|
|
70
|
-
When I sell all my <Item> with a starting price of $100 at the rebate price of $<RebatePrice>
|
|
71
|
-
Then the 1st item 1st price has a price amount of <Amount> which is a 'Number'
|
|
72
|
-
|
|
73
|
-
Examples:
|
|
74
|
-
| Item | RebatePrice | Amount |
|
|
75
|
-
| Autographed Neil deGrasse Tyson book | 20 | 20 |
|
|
76
|
-
| Rick Astley t-shirt | 50 | 50 |
|
|
77
|
-
# Ability: this ones are not currently possible
|
|
78
|
-
##
|
|
79
|
-
# Scenario Outline: Additional mix regexp and non-regexp in outline line with non-string variables and static one in the middle for <Item>
|
|
80
|
-
# Given I want to sell all my <Item>
|
|
81
|
-
# When I sell all my <Item> with a starting price <Description> price of <RebatePrice>$ <Currency> which is nice
|
|
82
|
-
# Then I should still get $<Amount>
|
|
83
|
-
#
|
|
84
|
-
# Examples:
|
|
85
|
-
# | Item | Description | RebatePrice | Amount | Currency |
|
|
86
|
-
# | Autographed Neil deGrasse Tyson book | of $100 at the fantastic | 20 | 20 | USD |
|
|
87
|
-
# | Rick Astley t-shirt | of $100 at the fantastic | 50 | 50 | CAD |
|
|
88
|
-
# | Rick Astley t-shirt | of $100 at the rebate | 50 | 50 | CAD |
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression test — H1: regex first-match shadowing.
|
|
3
|
-
* RCA: docs/feature/review-hardening/discuss/rca.md (H1, HIGH — only silent-wrong-result bug).
|
|
4
|
-
*
|
|
5
|
-
* findMatchingStep (src/index.js:168-188) uses Object.keys(...).find(...) → the FIRST
|
|
6
|
-
* registered definition that matches a step wins. Register a broad regex before a specific
|
|
7
|
-
* one for the same keyword and the broad (wrong) handler is bound silently with the wrong
|
|
8
|
-
* captured args; jest-cucumber's re-validation is still satisfied, so the scenario passes.
|
|
9
|
-
* jest-cucumber's own automatic binding raises an ambiguity error here — this wrapper
|
|
10
|
-
* suppresses it.
|
|
11
|
-
*
|
|
12
|
-
* LOCKED FIX CONTRACT (DELIVER implements; this test asserts it): when MORE THAN ONE step
|
|
13
|
-
* definition matches a step, Fusion must THROW an ambiguity error (naming the step text /
|
|
14
|
-
* competing matchers) rather than silently taking the first.
|
|
15
|
-
*
|
|
16
|
-
* MECHANISM / caveat: the wrapper's step matching (findMatchingStep) runs inside the scenario
|
|
17
|
-
* callback that Fusion hands to jest-cucumber's defineFeature — it is NOT reachable
|
|
18
|
-
* synchronously through Fusion's return value, and an ambiguous feature can never resolve to
|
|
19
|
-
* a green jest-cucumber scenario (post-fix it throws by design). So we drive the REAL wrapper
|
|
20
|
-
* (Fusion + real src/index.js) and substitute ONLY the external collaborator jest-cucumber
|
|
21
|
-
* (the non-deterministic test-runner port) with a double that captures the scenario callback,
|
|
22
|
-
* then invoke that callback synchronously and assert it throws. This is the "drive the SUT
|
|
23
|
-
* through its public surface, fake only external ports" mandate applied to this JS wrapper.
|
|
24
|
-
*
|
|
25
|
-
* CURRENT STATUS: RED — findMatchingStep does not throw on ambiguity, so the captured callback
|
|
26
|
-
* completes without error and toThrow fails ("received function did not throw").
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
const mockState = { capturedCallback: null, feature: null };
|
|
30
|
-
|
|
31
|
-
jest.mock("jest-cucumber", () => ({
|
|
32
|
-
loadFeature: jest.fn(() => mockState.feature),
|
|
33
|
-
defineFeature: jest.fn((feature, callback) => {
|
|
34
|
-
mockState.capturedCallback = callback;
|
|
35
|
-
}),
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
const { Given, Fusion } = require("../../../../src");
|
|
39
|
-
|
|
40
|
-
// Broad definition registered FIRST — this is the one that shadows.
|
|
41
|
-
Given(/^I have (.*)$/, () => {});
|
|
42
|
-
// Specific definition registered SECOND — the handler the author actually intends.
|
|
43
|
-
Given(/^I have (\d+) apples$/, () => {});
|
|
44
|
-
|
|
45
|
-
describe("H1 — ambiguous step definitions", () => {
|
|
46
|
-
test("a step matching two definitions is rejected as ambiguous, not silently shadowed", () => {
|
|
47
|
-
// The feature file is never read (loadFeature is faked); this is the parsed shape the
|
|
48
|
-
// wrapper consumes. The step "I have 3 apples" matches BOTH definitions above.
|
|
49
|
-
mockState.feature = {
|
|
50
|
-
title: "Ambiguous step matching",
|
|
51
|
-
scenarios: [
|
|
52
|
-
{
|
|
53
|
-
title: "counting apples",
|
|
54
|
-
steps: [
|
|
55
|
-
{
|
|
56
|
-
keyword: "given",
|
|
57
|
-
stepText: "I have 3 apples",
|
|
58
|
-
stepArgument: undefined,
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
scenarioOutlines: [],
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// Drive the real wrapper; the faked jest-cucumber captures the scenario callback.
|
|
67
|
-
Fusion("../ambiguous-step-shadowing.feature");
|
|
68
|
-
|
|
69
|
-
const runScenarioMatching = () => {
|
|
70
|
-
// capturedCallback = (testFn) =>
|
|
71
|
-
// matchJestTestSuiteWithCucumberFeature(registry, scenarios, testFn, ...)
|
|
72
|
-
// (hooks are wired once per feature by registerHooks, outside this call: M6)
|
|
73
|
-
mockState.capturedCallback((_title, scenarioBody) => {
|
|
74
|
-
// scenarioBody performs the wrapper's step matching (findMatchingStep) synchronously.
|
|
75
|
-
scenarioBody({
|
|
76
|
-
given: () => {},
|
|
77
|
-
when: () => {},
|
|
78
|
-
then: () => {},
|
|
79
|
-
and: () => {},
|
|
80
|
-
but: () => {},
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
// Locked contract: ambiguity must throw. Pre-fix this does NOT throw (silent first-match).
|
|
86
|
-
// Match the full contract signature (not a loose /ambig/i) so an incidental "ambiguous"
|
|
87
|
-
// in some unrelated error can't satisfy the assertion.
|
|
88
|
-
expect(runScenarioMatching).toThrow(
|
|
89
|
-
/Ambiguous step definition.*matches \d+ step definitions/i
|
|
90
|
-
);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
const { Given, When, Then, And, But, Fusion } = require("../../../../src");
|
|
2
|
-
|
|
3
|
-
const { Rocket } = require("../../../src/rocket");
|
|
4
|
-
|
|
5
|
-
let rocket;
|
|
6
|
-
let rocketOnTheLaunchpad;
|
|
7
|
-
|
|
8
|
-
const observableRocketState = () => ({
|
|
9
|
-
isInSpace: rocket.isInSpace,
|
|
10
|
-
boostersLanded: rocket.boostersLanded,
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
Given("I am Elon Musk attempting to launch a rocket into space", () => {
|
|
14
|
-
rocket = new Rocket();
|
|
15
|
-
// Remember how the rocket sat on the pad, so a later step can prove it moved
|
|
16
|
-
rocketOnTheLaunchpad = observableRocketState();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
When("I launch the rocket", () => {
|
|
20
|
-
rocket.launch();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
When("I launch the '<rocket>'", () => {
|
|
24
|
-
rocket.launch();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
When(/^I launch my personal rocket named '(.*)'$/, (nameRocket) => {
|
|
28
|
-
expect(nameRocket).toBe("<space poney==>");
|
|
29
|
-
rocket.launch();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
Then("the rocket should end up in space", () => {
|
|
33
|
-
expect(rocket.isInSpace).toBe(true);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
/// Complex Regex : at position (\[(?: *\d+(?: |, |,)*)+\]) with no value
|
|
37
|
-
And(
|
|
38
|
-
/^my position in 2D space is (\[(?: *\d+(?: |, |,)*)+\])$/,
|
|
39
|
-
(arrayPosition) => {
|
|
40
|
-
const isAnArray = JSON.parse(arrayPosition);
|
|
41
|
-
expect(isAnArray).toBeInstanceOf(Array);
|
|
42
|
-
}
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
And(/^the booster\(s\) should land back on the launch pad$/, () => {
|
|
46
|
-
expect(rocket.boostersLanded).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
But("nobody should doubt me ever again", () => {
|
|
50
|
-
// The doubt is that the rocket never really flew. It sat on the pad — grounded,
|
|
51
|
-
// boosters intact — and it is now in space with its boosters recovered.
|
|
52
|
-
expect(rocketOnTheLaunchpad).toEqual({
|
|
53
|
-
isInSpace: false,
|
|
54
|
-
boostersLanded: true,
|
|
55
|
-
});
|
|
56
|
-
expect(observableRocketState()).toEqual({
|
|
57
|
-
isInSpace: true,
|
|
58
|
-
boostersLanded: true,
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
Fusion("../basic-scenarios.feature");
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
// Property-based fuzz of the public surface. Each sampled case writes its own feature
|
|
2
|
-
// file, registers its own steps and calls Fusion(); since M3 every Fusion() starts from
|
|
3
|
-
// an empty registry, so one module can host many generated features.
|
|
4
|
-
//
|
|
5
|
-
// The seed is fixed so a failure reproduces: it appears in every feature title.
|
|
6
|
-
// Widen locally with FUZZ_SEED=<n> FUZZ_RUNS=<n> npx jest fuzz-properties.
|
|
7
|
-
// Sensitivity was proven against the published 1.0.0: P5 reproduces the L4 crash and
|
|
8
|
-
// P4 fails every case on the L3 dropped docstring.
|
|
9
|
-
const fs = require("fs");
|
|
10
|
-
const os = require("os");
|
|
11
|
-
const path = require("path");
|
|
12
|
-
const fc = require("fast-check");
|
|
13
|
-
|
|
14
|
-
const { Given, When, Then, Fusion } = require("../../../../src");
|
|
15
|
-
|
|
16
|
-
const SEED = Number(process.env.FUZZ_SEED || 20261002);
|
|
17
|
-
const RUNS = Number(process.env.FUZZ_RUNS || 40);
|
|
18
|
-
|
|
19
|
-
const featureDir = fs.mkdtempSync(path.join(os.tmpdir(), "fusion-fuzz-"));
|
|
20
|
-
let featureCount = 0;
|
|
21
|
-
const writeFeature = (body) => {
|
|
22
|
-
featureCount += 1;
|
|
23
|
-
const featurePath = path.join(featureDir, `case-${featureCount}.feature`);
|
|
24
|
-
fs.writeFileSync(featurePath, body);
|
|
25
|
-
return featurePath;
|
|
26
|
-
};
|
|
27
|
-
afterAll(() => {
|
|
28
|
-
fs.rmSync(featureDir, { recursive: true, force: true });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const sample = (arbitrary) =>
|
|
32
|
-
fc.sample(arbitrary, { seed: SEED, numRuns: RUNS });
|
|
33
|
-
const title = (property, caseIndex) =>
|
|
34
|
-
`fuzz ${property} case ${caseIndex} (seed ${SEED})`;
|
|
35
|
-
|
|
36
|
-
// Words that are safe inside Gherkin step text: no structural characters
|
|
37
|
-
// (| # " < > @ :) and no leading or trailing whitespace.
|
|
38
|
-
const word = fc.stringMatching(/^[A-Za-z0-9][A-Za-z0-9_\-.,'!?]{0,11}$/);
|
|
39
|
-
const sentence = fc
|
|
40
|
-
.array(word, { minLength: 1, maxLength: 6 })
|
|
41
|
-
.map((words) => words.join(" "));
|
|
42
|
-
|
|
43
|
-
// P1: a string matcher binds arbitrary step text, and the step runs exactly once.
|
|
44
|
-
sample(sentence).forEach((stepText, caseIndex) => {
|
|
45
|
-
let calls = 0;
|
|
46
|
-
Given(`p1 ${stepText}`, () => {
|
|
47
|
-
calls += 1;
|
|
48
|
-
});
|
|
49
|
-
Then("p1 it ran once", () => {
|
|
50
|
-
expect(calls).toBe(1);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
Fusion(
|
|
54
|
-
writeFeature(
|
|
55
|
-
`Feature: ${title("P1", caseIndex)}\n` +
|
|
56
|
-
` Scenario: string matcher\n` +
|
|
57
|
-
` Given p1 ${stepText}\n` +
|
|
58
|
-
` Then p1 it ran once\n`
|
|
59
|
-
)
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// P2: regex captures arrive exactly as written, in order.
|
|
64
|
-
sample(fc.tuple(word, fc.nat({ max: 1e6 }), word)).forEach(
|
|
65
|
-
([first, number, last], caseIndex) => {
|
|
66
|
-
When(/^p2 take "(.*)" then (\d+) then "(.*)"$/, (a, n, b) => {
|
|
67
|
-
expect([a, n, b]).toEqual([first, String(number), last]);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
Fusion(
|
|
71
|
-
writeFeature(
|
|
72
|
-
`Feature: ${title("P2", caseIndex)}\n` +
|
|
73
|
-
` Scenario: regex captures\n` +
|
|
74
|
-
` When p2 take "${first}" then ${number} then "${last}"\n`
|
|
75
|
-
)
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
// P3: every outline row hands its own values to the step, in table order.
|
|
81
|
-
sample(
|
|
82
|
-
fc.array(fc.tuple(word, fc.nat({ max: 9999 })), {
|
|
83
|
-
minLength: 1,
|
|
84
|
-
maxLength: 5,
|
|
85
|
-
})
|
|
86
|
-
).forEach((rows, caseIndex) => {
|
|
87
|
-
const seenRows = [];
|
|
88
|
-
Given(/^p3 row "(.*)" (\d+)$/, (name, number) => {
|
|
89
|
-
seenRows.push([name, number]);
|
|
90
|
-
});
|
|
91
|
-
Then("p3 the row was delivered", () => {
|
|
92
|
-
const [name, number] = rows[seenRows.length - 1];
|
|
93
|
-
expect(seenRows[seenRows.length - 1]).toEqual([name, String(number)]);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const examples = rows
|
|
97
|
-
.map(([name, number]) => ` | ${name} | ${number} |`)
|
|
98
|
-
.join("\n");
|
|
99
|
-
Fusion(
|
|
100
|
-
writeFeature(
|
|
101
|
-
`Feature: ${title("P3", caseIndex)}\n` +
|
|
102
|
-
` Scenario Outline: outline row <name>\n` +
|
|
103
|
-
` Given p3 row "<name>" <num>\n` +
|
|
104
|
-
` Then p3 the row was delivered\n\n` +
|
|
105
|
-
` Examples:\n` +
|
|
106
|
-
` | name | num |\n` +
|
|
107
|
-
`${examples}\n`
|
|
108
|
-
)
|
|
109
|
-
);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// P4: a non-empty, multi-line docstring arrives byte for byte. (An EMPTY docstring
|
|
113
|
-
// inside an outline is the known upstream defect L5, so it is not generated.)
|
|
114
|
-
const docLine = fc
|
|
115
|
-
.stringMatching(/^[A-Za-z0-9 _\-.,'!?|#<>@:"]{0,30}$/)
|
|
116
|
-
.filter(
|
|
117
|
-
(line) =>
|
|
118
|
-
line.trim() === line &&
|
|
119
|
-
!line.startsWith('"""') &&
|
|
120
|
-
!line.startsWith("#") &&
|
|
121
|
-
!line.startsWith("@")
|
|
122
|
-
);
|
|
123
|
-
sample(
|
|
124
|
-
fc
|
|
125
|
-
.array(docLine, { minLength: 1, maxLength: 5 })
|
|
126
|
-
.filter((lines) => lines.join("").length > 0)
|
|
127
|
-
).forEach((lines, caseIndex) => {
|
|
128
|
-
When(/^p4 I read the doc$/, (docString) => {
|
|
129
|
-
expect(docString).toBe(lines.join("\n"));
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const docBody = lines.map((line) => ` ${line}`).join("\n");
|
|
133
|
-
Fusion(
|
|
134
|
-
writeFeature(
|
|
135
|
-
`Feature: ${title("P4", caseIndex)}\n` +
|
|
136
|
-
` Scenario: docstring\n` +
|
|
137
|
-
` When p4 I read the doc\n` +
|
|
138
|
-
` """\n${docBody}\n """\n`
|
|
139
|
-
)
|
|
140
|
-
);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// P5: a regex matcher with escaped parentheses binds in a plain scenario AND an outline
|
|
144
|
-
// (the L4 crash shape).
|
|
145
|
-
sample(fc.stringMatching(/^[a-z][a-zA-Z0-9]{0,10}$/)).forEach(
|
|
146
|
-
(functionName, caseIndex) => {
|
|
147
|
-
const calls = [];
|
|
148
|
-
Given(/^p5 call (\w+\(\))$/, (call) => {
|
|
149
|
-
calls.push(call);
|
|
150
|
-
});
|
|
151
|
-
Then("p5 the call was bound", () => {
|
|
152
|
-
expect(calls[calls.length - 1]).toBe(`${functionName}()`);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
Fusion(
|
|
156
|
-
writeFeature(
|
|
157
|
-
`Feature: ${title("P5", caseIndex)}\n` +
|
|
158
|
-
` Scenario: plain\n` +
|
|
159
|
-
` Given p5 call ${functionName}()\n` +
|
|
160
|
-
` Then p5 the call was bound\n\n` +
|
|
161
|
-
` Scenario Outline: outline\n` +
|
|
162
|
-
` Given p5 call <f>\n` +
|
|
163
|
-
` Then p5 the call was bound\n\n` +
|
|
164
|
-
` Examples:\n` +
|
|
165
|
-
` | f |\n` +
|
|
166
|
-
` | ${functionName}() |\n`
|
|
167
|
-
)
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
);
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression test — T1: negative/sad-path coverage, item 4 (hook error).
|
|
3
|
-
* RCA: docs/feature/review-hardening/discuss/rca.md (T1, MED).
|
|
4
|
-
*
|
|
5
|
-
* A Before/After hook that throws must surface (fail loudly), not be swallowed. The wrapper
|
|
6
|
-
* routes hooks into jest by calling the global beforeEach/afterEach with the registered hook
|
|
7
|
-
* (src/index.js:122-133). This characterization pins CURRENT behaviour: a throwing Before hook
|
|
8
|
-
* IS wired into jest's per-test setup (so jest will report the failure), rather than dropped.
|
|
9
|
-
*
|
|
10
|
-
* MECHANISM: drive the REAL wrapper (Fusion + real src/index.js); fake ONLY the external
|
|
11
|
-
* jest-cucumber (capture its callback), and spy on the global beforeEach so the hook wiring is
|
|
12
|
-
* captured without registering a real, throwing hook that would poison the suite.
|
|
13
|
-
*
|
|
14
|
-
* CURRENT STATUS: GREEN (guard/characterization) — the wrapper already wires throwing hooks
|
|
15
|
-
* into beforeEach. This test exists so that regression to silent-swallow is caught.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const mockState = { capturedCallback: null, feature: null };
|
|
19
|
-
|
|
20
|
-
jest.mock("jest-cucumber", () => ({
|
|
21
|
-
loadFeature: jest.fn(() => mockState.feature),
|
|
22
|
-
defineFeature: jest.fn((feature, callback) => {
|
|
23
|
-
mockState.capturedCallback = callback;
|
|
24
|
-
}),
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
const { Before, Given, Fusion } = require("../../../../src");
|
|
28
|
-
|
|
29
|
-
Before(() => {
|
|
30
|
-
throw new Error("hook failure — Before threw");
|
|
31
|
-
});
|
|
32
|
-
Given(/^some precondition$/, () => {});
|
|
33
|
-
|
|
34
|
-
describe("T1.4 — hook errors surface", () => {
|
|
35
|
-
test("a throwing Before hook is wired into jest's beforeEach, not swallowed", () => {
|
|
36
|
-
mockState.feature = {
|
|
37
|
-
title: "Hook wiring",
|
|
38
|
-
scenarios: [{ title: "any scenario", steps: [] }],
|
|
39
|
-
scenarioOutlines: [],
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
Fusion("hook-error.feature");
|
|
43
|
-
|
|
44
|
-
const beforeEachSpy = jest
|
|
45
|
-
.spyOn(global, "beforeEach")
|
|
46
|
-
.mockImplementation(() => {});
|
|
47
|
-
try {
|
|
48
|
-
// Exercise the wrapper's suite wiring; the scenario body is irrelevant here.
|
|
49
|
-
mockState.capturedCallback(() => {});
|
|
50
|
-
|
|
51
|
-
// The wrapper must have handed the throwing hook to jest's beforeEach.
|
|
52
|
-
expect(beforeEachSpy).toHaveBeenCalledTimes(1);
|
|
53
|
-
const wiredHook = beforeEachSpy.mock.calls[0][0];
|
|
54
|
-
expect(() => wiredHook()).toThrow("hook failure — Before threw");
|
|
55
|
-
} finally {
|
|
56
|
-
beforeEachSpy.mockRestore();
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
});
|