-module(dream_test@gherkin@feature). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/dream_test/gherkin/feature.gleam"). -export([to_test_suite/2, scenario/2, with_tags/2, given/1, 'when'/1, then/1, 'and'/1, but/1, feature/3, background/1, feature_with_background/4, to_test_cases/2]). -export_type([feature_config/0, inline_scenario/0, inline_step/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Feature execution and TestSuite conversion for Gherkin tests.\n" "\n" " This module converts parsed Gherkin features to dream_test TestSuites\n" " and provides an inline DSL for defining features directly in Gleam.\n" "\n" " ## Two Approaches\n" "\n" " 1. **File-based**: Parse `.feature` files with standard Gherkin syntax\n" " 2. **Inline DSL**: Define features directly in Gleam code\n" "\n" " Both approaches share the same step definitions and execution engine.\n" "\n" " ## File-Based Usage\n" "\n" " Parse a `.feature` file and convert to TestSuite:\n" "\n" " ```gleam\n" " import dream_test/gherkin/feature.{FeatureConfig, to_test_suite}\n" " import dream_test/gherkin/parser\n" " import dream_test/gherkin/steps.{new_registry, given, when_, then_}\n" " import dream_test/runner\n" "\n" " pub fn main() {\n" " let steps = new_registry()\n" " |> given(\"I have {int} items\", have_items)\n" " |> when_(\"I add {int} items\", add_items)\n" " |> then_(\"the total is ${float}\", check_total)\n" "\n" " let assert Ok(parsed) = parser.parse_file(\"features/cart.feature\")\n" " let config = FeatureConfig(feature: parsed, step_registry: steps)\n" " \n" " to_test_suite(\"cart_test\", config)\n" " |> runner.run_suite()\n" " }\n" " ```\n" "\n" " ## Inline DSL Usage\n" "\n" " Define features directly in Gleam without `.feature` files:\n" "\n" " ```gleam\n" " import dream_test/gherkin/feature.{\n" " feature, scenario, given, when, then, and, with_tags,\n" " }\n" "\n" " pub fn tests() -> TestSuite {\n" " let steps = cart_steps()\n" " \n" " feature(\"Shopping Cart\", steps, [\n" " scenario(\"Adding items\", [\n" " given(\"I have an empty cart\"),\n" " when(\"I add 2 apples to the cart\"),\n" " then(\"the cart should contain 2 items\"),\n" " and(\"the total should be $3.00\"),\n" " ])\n" " |> with_tags([\"happy-path\"]),\n" " ])\n" " }\n" " ```\n" "\n" ). -type feature_config() :: {feature_config, dream_test@gherkin@types:feature(), dream_test@gherkin@steps:step_registry()}. -type inline_scenario() :: {inline_scenario, binary(), list(inline_step()), list(binary())}. -type inline_step() :: {inline_step, binary(), binary()}. -file("src/dream_test/gherkin/feature.gleam", 250). -spec build_full_name(binary(), binary(), gleam@option:option(binary())) -> list(binary()). build_full_name(Feature_name, Scenario_name, Example_suffix) -> case Example_suffix of none -> [Feature_name, Scenario_name]; {some, Suffix} -> [Feature_name, <<<>/binary, Suffix/binary>>] end. -file("src/dream_test/gherkin/feature.gleam", 289). -spec build_substitution_map(list(binary()), list(binary())) -> gleam@dict:dict(binary(), binary()). build_substitution_map(Headers, Values) -> _pipe = gleam@list:zip(Headers, Values), gleam@list:fold( _pipe, maps:new(), fun(Acc, Pair) -> {Header, Value} = Pair, gleam@dict:insert(Acc, Header, Value) end ). -file("src/dream_test/gherkin/feature.gleam", 319). -spec substitute_placeholders(binary(), gleam@dict:dict(binary(), binary())) -> binary(). substitute_placeholders(Text, Substitutions) -> gleam@dict:fold( Substitutions, Text, fun(Acc, Header, Value) -> Placeholder = <<<<"<"/utf8, Header/binary>>/binary, ">"/utf8>>, gleam@string:replace(Acc, Placeholder, Value) end ). -file("src/dream_test/gherkin/feature.gleam", 307). -spec substitute_step( dream_test@gherkin@types:step(), gleam@dict:dict(binary(), binary()) ) -> dream_test@gherkin@types:step(). substitute_step(Step, Substitutions) -> New_text = substitute_placeholders(erlang:element(3, Step), Substitutions), {step, erlang:element(2, Step), New_text, erlang:element(4, Step)}. -file("src/dream_test/gherkin/feature.gleam", 300). -spec substitute_steps( list(dream_test@gherkin@types:step()), gleam@dict:dict(binary(), binary()) ) -> list(dream_test@gherkin@types:step()). substitute_steps(Steps, Substitutions) -> gleam@list:map(Steps, fun(Step) -> substitute_step(Step, Substitutions) end). -file("src/dream_test/gherkin/feature.gleam", 421). -spec extract_table_from_step(dream_test@gherkin@types:step()) -> gleam@option:option(list(list(binary()))). extract_table_from_step(Step) -> case erlang:element(4, Step) of {some, {data_table, Rows}} -> {some, Rows}; _ -> none end. -file("src/dream_test/gherkin/feature.gleam", 430). -spec extract_doc_string_from_step(dream_test@gherkin@types:step()) -> gleam@option:option(binary()). extract_doc_string_from_step(Step) -> case erlang:element(4, Step) of {some, {doc_string, Content, _}} -> {some, Content}; _ -> none end. -file("src/dream_test/gherkin/feature.gleam", 405). -spec build_step_context( dream_test@gherkin@step_trie:step_match(fun((dream_test@gherkin@steps:step_context()) -> dream_test@types:assertion_result())), dream_test@gherkin@types:step(), dream_test@gherkin@world:world() ) -> dream_test@gherkin@steps:step_context(). build_step_context(Match, Step, The_world) -> Table = extract_table_from_step(Step), Doc_string = extract_doc_string_from_step(Step), {step_context, erlang:element(3, Match), Table, Doc_string, The_world}. -file("src/dream_test/gherkin/feature.gleam", 379). -spec execute_step( dream_test@gherkin@types:step(), dream_test@gherkin@steps:step_registry(), dream_test@gherkin@world:world(), dream_test@gherkin@types:step_keyword() ) -> dream_test@types:assertion_result(). execute_step(Step, Registry, The_world, Previous_keyword) -> Effective_keyword = dream_test@gherkin@types:resolve_keyword( erlang:element(2, Step), Previous_keyword ), case dream_test@gherkin@steps:find_step( Registry, Effective_keyword, erlang:element(3, Step) ) of {ok, Match} -> Context = build_step_context(Match, Step, The_world), (erlang:element(2, Match))(Context); {error, Msg} -> {assertion_failed, {assertion_failure, <<"step"/utf8>>, Msg, none}} end. -file("src/dream_test/gherkin/feature.gleam", 358). -spec execute_steps( list(dream_test@gherkin@types:step()), dream_test@gherkin@steps:step_registry(), dream_test@gherkin@world:world(), dream_test@gherkin@types:step_keyword() ) -> dream_test@types:assertion_result(). execute_steps(Steps, Registry, The_world, Previous_keyword) -> case Steps of [] -> assertion_ok; [Step | Rest] -> case execute_step(Step, Registry, The_world, Previous_keyword) of assertion_ok -> Effective_keyword = dream_test@gherkin@types:resolve_keyword( erlang:element(2, Step), Previous_keyword ), execute_steps(Rest, Registry, The_world, Effective_keyword); Failure -> Failure end end. -file("src/dream_test/gherkin/feature.gleam", 341). -spec execute_scenario( binary(), list(dream_test@gherkin@types:step()), dream_test@gherkin@steps:step_registry() ) -> dream_test@types:assertion_result(). execute_scenario(Scenario_id, Steps, Registry) -> The_world = dream_test@gherkin@world:new_world(Scenario_id), Result = execute_steps(Steps, Registry, The_world, given), dream_test@gherkin@world:cleanup(The_world), Result. -file("src/dream_test/gherkin/feature.gleam", 333). -spec build_scenario_runner( binary(), list(dream_test@gherkin@types:step()), dream_test@gherkin@steps:step_registry() ) -> fun(() -> dream_test@types:assertion_result()). build_scenario_runner(Scenario_id, Steps, Registry) -> fun() -> execute_scenario(Scenario_id, Steps, Registry) end. -file("src/dream_test/gherkin/feature.gleam", 213). -spec build_scenario_test_case( dream_test@gherkin@types:feature(), binary(), list(binary()), list(dream_test@gherkin@types:step()), feature_config(), gleam@option:option(binary()) ) -> dream_test@types:test_case(). build_scenario_test_case( Feature, Scenario_name, Scenario_tags, Steps, Config, Example_suffix ) -> Full_name = build_full_name( erlang:element(2, Feature), Scenario_name, Example_suffix ), Scenario_id = gleam@string:join(Full_name, <<"::"/utf8>>), All_tags = lists:append(erlang:element(4, Feature), Scenario_tags), All_steps = case erlang:element(5, Feature) of {some, {background, Background_steps}} -> lists:append(Background_steps, Steps); none -> Steps end, Run_fn = build_scenario_runner( Scenario_id, All_steps, erlang:element(3, Config) ), Single_config = {single_test_config, Scenario_name, Full_name, All_tags, {gherkin_scenario, Scenario_id}, Run_fn, none, [], []}, {test_case, Single_config}. -file("src/dream_test/gherkin/feature.gleam", 261). -spec expand_scenario_outline( dream_test@gherkin@types:feature(), binary(), list(binary()), list(dream_test@gherkin@types:step()), dream_test@gherkin@types:examples_table(), feature_config() ) -> list(dream_test@types:test_suite_item()). expand_scenario_outline(Feature, Name, Tags, Steps, Examples, Config) -> Headers = erlang:element(2, Examples), gleam@list:index_map( erlang:element(3, Examples), fun(Row, Index) -> Substitutions = build_substitution_map(Headers, Row), Expanded_steps = substitute_steps(Steps, Substitutions), Suffix = <<<<"(Example "/utf8, (erlang:integer_to_binary(Index + 1))/binary>>/binary, ")"/utf8>>, Test_case = build_scenario_test_case( Feature, Name, Tags, Expanded_steps, Config, {some, Suffix} ), {suite_test, Test_case} end ). -file("src/dream_test/gherkin/feature.gleam", 196). -spec scenario_to_suite_items( dream_test@gherkin@types:feature(), dream_test@gherkin@types:scenario(), feature_config() ) -> list(dream_test@types:test_suite_item()). scenario_to_suite_items(Feature, Scenario, Config) -> case Scenario of {scenario, Name, Tags, Steps} -> Test_case = build_scenario_test_case( Feature, Name, Tags, Steps, Config, none ), [{suite_test, Test_case}]; {scenario_outline, Name@1, Tags@1, Steps@1, Examples} -> expand_scenario_outline( Feature, Name@1, Tags@1, Steps@1, Examples, Config ) end. -file("src/dream_test/gherkin/feature.gleam", 187). -spec build_suite_items(dream_test@gherkin@types:feature(), feature_config()) -> list(dream_test@types:test_suite_item()). build_suite_items(Feature, Config) -> gleam@list:flat_map( erlang:element(6, Feature), fun(Scenario) -> scenario_to_suite_items(Feature, Scenario, Config) end ). -file("src/dream_test/gherkin/feature.gleam", 141). ?DOC( " Convert a Feature to a TestSuite.\n" "\n" " Creates a TestSuite with one test per scenario. Background steps are\n" " prepended to each scenario. ScenarioOutlines are expanded based on\n" " their examples table.\n" "\n" " ## Parameters\n" "\n" " - `module_name`: Name for the suite (usually the test module name)\n" " - `config`: FeatureConfig with feature and step registry\n" "\n" " ## Returns\n" "\n" " A TestSuite that can be run with `runner.run_suite()`\n" ). -spec to_test_suite(binary(), feature_config()) -> dream_test@types:test_suite(). to_test_suite(Module_name, Config) -> Feature = erlang:element(2, Config), Items = build_suite_items(Feature, Config), {test_suite, erlang:element(2, Feature), [Module_name, erlang:element(2, Feature)], [], [], Items}. -file("src/dream_test/gherkin/feature.gleam", 496). ?DOC( " Define an inline scenario.\n" "\n" " ## Parameters\n" "\n" " - `name`: Scenario name\n" " - `steps`: List of inline steps\n" "\n" " ## Example\n" "\n" " ```gleam\n" " scenario(\"Adding items\", [\n" " given(\"I have an empty cart\"),\n" " when_step(\"I add 5 items\"),\n" " then_step(\"I should have 5 items\"),\n" " ])\n" " ```\n" ). -spec scenario(binary(), list(inline_step())) -> inline_scenario(). scenario(Name, Inline_steps) -> {inline_scenario, Name, Inline_steps, []}. -file("src/dream_test/gherkin/feature.gleam", 517). ?DOC( " Add tags to a Gherkin scenario for filtering.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " scenario(\"Adding items\", [\n" " when(\"I add 2 apples to the cart\"),\n" " then(\"the cart should contain 2 items\"),\n" " ])\n" " |> with_tags([\"happy-path\", \"smoke\"])\n" " ```\n" "\n" " ## Note\n" "\n" " This function is for Gherkin scenarios. For unit tests (`it`), use\n" " `dream_test/unit.with_tags` instead.\n" ). -spec with_tags(inline_scenario(), list(binary())) -> inline_scenario(). with_tags(Inline_scenario, Tags) -> {inline_scenario, erlang:element(2, Inline_scenario), erlang:element(3, Inline_scenario), Tags}. -file("src/dream_test/gherkin/feature.gleam", 532). ?DOC( " Create a Given step for inline DSL.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " given(\"I have {int} items in my cart\")\n" " ```\n" ). -spec given(binary()) -> inline_step(). given(Text) -> {inline_step, <<"Given"/utf8>>, Text}. -file("src/dream_test/gherkin/feature.gleam", 544). ?DOC( " Create a When step for inline DSL.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " when(\"I add {int} items\")\n" " ```\n" ). -spec 'when'(binary()) -> inline_step(). 'when'(Text) -> {inline_step, <<"When"/utf8>>, Text}. -file("src/dream_test/gherkin/feature.gleam", 556). ?DOC( " Create a Then step for inline DSL.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " then(\"I should have {int} items\")\n" " ```\n" ). -spec then(binary()) -> inline_step(). then(Text) -> {inline_step, <<"Then"/utf8>>, Text}. -file("src/dream_test/gherkin/feature.gleam", 568). ?DOC( " Create an And step for inline DSL.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " and(\"I have a coupon\")\n" " ```\n" ). -spec 'and'(binary()) -> inline_step(). 'and'(Text) -> {inline_step, <<"And"/utf8>>, Text}. -file("src/dream_test/gherkin/feature.gleam", 580). ?DOC( " Create a But step for inline DSL.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " but(\"I should not see errors\")\n" " ```\n" ). -spec but(binary()) -> inline_step(). but(Text) -> {inline_step, <<"But"/utf8>>, Text}. -file("src/dream_test/gherkin/feature.gleam", 598). -spec parse_keyword(binary()) -> dream_test@gherkin@types:step_keyword(). parse_keyword(Keyword_str) -> case Keyword_str of <<"Given"/utf8>> -> given; <<"When"/utf8>> -> 'when'; <<"Then"/utf8>> -> then; <<"And"/utf8>> -> 'and'; <<"But"/utf8>> -> but; _ -> given end. -file("src/dream_test/gherkin/feature.gleam", 593). -spec inline_to_parsed_step(inline_step()) -> dream_test@gherkin@types:step(). inline_to_parsed_step(Inline) -> Keyword = parse_keyword(erlang:element(2, Inline)), {step, Keyword, erlang:element(3, Inline), none}. -file("src/dream_test/gherkin/feature.gleam", 584). -spec inline_to_parsed_scenario(inline_scenario()) -> dream_test@gherkin@types:scenario(). inline_to_parsed_scenario(Inline) -> Parsed_steps = gleam@list:map( erlang:element(3, Inline), fun inline_to_parsed_step/1 ), {scenario, erlang:element(2, Inline), erlang:element(4, Inline), Parsed_steps}. -file("src/dream_test/gherkin/feature.gleam", 460). ?DOC( " Define a feature inline in Gleam code.\n" "\n" " Creates a TestSuite from inline scenario definitions without needing\n" " a `.feature` file.\n" "\n" " ## Parameters\n" "\n" " - `name`: Feature name\n" " - `registry`: Step registry with step definitions\n" " - `scenarios`: List of inline scenarios\n" "\n" " ## Returns\n" "\n" " A TestSuite that can be run with `runner.run_suite()`\n" "\n" " ## Example\n" "\n" " ```gleam\n" " feature(\"Shopping Cart\", steps, [\n" ). -spec feature( binary(), dream_test@gherkin@steps:step_registry(), list(inline_scenario()) ) -> dream_test@types:test_suite(). feature(Name, Registry, Scenarios) -> Parsed_scenarios = gleam@list:map( Scenarios, fun inline_to_parsed_scenario/1 ), Parsed_feature = {feature, Name, none, [], none, Parsed_scenarios}, Config = {feature_config, Parsed_feature, Registry}, to_test_suite(<>, Config). -file("src/dream_test/gherkin/feature.gleam", 628). ?DOC( " Define a background for inline features.\n" "\n" " Background steps run before each scenario.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let bg = background([\n" " given(\"I am logged in\"),\n" " given(\"I have an empty cart\"),\n" " ])\n" "\n" " feature_with_background(\"Shopping\", steps, bg, [...scenarios...])\n" " ```\n" ). -spec background(list(inline_step())) -> list(dream_test@gherkin@types:step()). background(Inline_steps) -> gleam@list:map(Inline_steps, fun inline_to_parsed_step/1). -file("src/dream_test/gherkin/feature.gleam", 641). ?DOC( " Define a feature with a background.\n" "\n" " ## Parameters\n" "\n" " - `name`: Feature name\n" " - `registry`: Step registry\n" " - `background_steps`: Steps to run before each scenario\n" " - `scenarios`: List of inline scenarios\n" ). -spec feature_with_background( binary(), dream_test@gherkin@steps:step_registry(), list(dream_test@gherkin@types:step()), list(inline_scenario()) ) -> dream_test@types:test_suite(). feature_with_background(Name, Registry, Background_steps, Scenarios) -> Parsed_scenarios = gleam@list:map( Scenarios, fun inline_to_parsed_scenario/1 ), Parsed_feature = {feature, Name, none, [], {some, {background, Background_steps}}, Parsed_scenarios}, Config = {feature_config, Parsed_feature, Registry}, to_test_suite(<>, Config). -file("src/dream_test/gherkin/feature.gleam", 180). -spec flatten_item(dream_test@types:test_suite_item()) -> list(dream_test@types:test_case()). flatten_item(Item) -> case Item of {suite_test, Test_case} -> [Test_case]; {suite_group, Nested_suite} -> flatten_suite(Nested_suite) end. -file("src/dream_test/gherkin/feature.gleam", 176). -spec flatten_suite(dream_test@types:test_suite()) -> list(dream_test@types:test_case()). flatten_suite(Suite) -> gleam@list:flat_map(erlang:element(6, Suite), fun flatten_item/1). -file("src/dream_test/gherkin/feature.gleam", 168). ?DOC( " Convert a Feature to a flat list of TestCases.\n" "\n" " Unlike `to_test_suite`, this flattens the feature to a simple list.\n" " Use this when you don't need before_all/after_all hooks.\n" "\n" " ## Parameters\n" "\n" " - `module_name`: Name prefix for test paths\n" " - `config`: FeatureConfig with feature and step registry\n" "\n" " ## Returns\n" "\n" " A list of TestCases that can be run with `runner.run_all()`\n" ). -spec to_test_cases(binary(), feature_config()) -> list(dream_test@types:test_case()). to_test_cases(Module_name, Config) -> Suite = to_test_suite(Module_name, Config), flatten_suite(Suite).