cooklang 1.0.3 → 1.1.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.
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Cooklang::Formatters::Hash do
6
+ describe "#generate" do
7
+ let(:recipe) do
8
+ recipe_text = File.read(File.join(__dir__, "..", "fixtures", "comprehensive_recipe.cook"))
9
+ Cooklang.parse(recipe_text)
10
+ end
11
+ let(:result) { described_class.new(recipe).generate }
12
+
13
+ it "generates hash with all required top-level keys" do
14
+ expect(result).to have_key(:metadata)
15
+ expect(result).to have_key(:sections)
16
+ expect(result).to have_key(:ingredients)
17
+ expect(result).to have_key(:cookware)
18
+ expect(result).to have_key(:timers)
19
+ expect(result).to have_key(:data)
20
+ end
21
+
22
+ it "includes expected metadata" do
23
+ metadata = result[:metadata][:map]
24
+ expect(metadata["title"]).to eq("Uncle Roger's Ultimate Fried Rice & Dumplings")
25
+ expect(metadata["servings"]).to eq(4)
26
+ expect(metadata["difficulty"]).to eq("intermediate")
27
+ end
28
+
29
+ it "parses ingredients with decimal quantities and nil quantities" do
30
+ ingredients = result[:ingredients]
31
+
32
+ # Find MSG with decimal quantity
33
+ msg = ingredients.find { |i| i[:name] == "msg" }
34
+ expect(msg[:quantity][:value][:value][:value]).to eq(0.5)
35
+
36
+ # Find ingredient with nil quantity (like "some")
37
+ white_pepper = ingredients.find { |i| i[:name] == "white pepper" }
38
+ expect(white_pepper[:quantity]).to be_nil
39
+ end
40
+
41
+ it "includes timers with named and decimal durations" do
42
+ timers = result[:timers]
43
+ expect(timers.size).to be >= 5
44
+
45
+ # Find named timer
46
+ named_timer = timers.find { |t| t[:name] == "quick scramble" }
47
+ expect(named_timer).not_to be_nil
48
+
49
+ # Find decimal duration timer
50
+ decimal_timer = timers.find { |t| t[:quantity][:value][:value][:value] == 2.5 }
51
+ expect(decimal_timer).not_to be_nil
52
+ end
53
+
54
+ it "generates steps with proper indexing" do
55
+ steps = result[:sections].first[:content]
56
+ expect(steps.size).to be >= 10
57
+
58
+ # Check step numbers are sequential
59
+ step_numbers = steps.map { |s| s[:value][:number] }
60
+ expect(step_numbers).to eq((1..step_numbers.size).to_a)
61
+ end
62
+ end
63
+
64
+ describe "fixed quantities" do
65
+ let(:recipe) { Cooklang.parse("Add @salt{=1%tsp} and @flour{500%g} and @pepper.") }
66
+ let(:result) { described_class.new(recipe).generate }
67
+
68
+ it "marks fixed ingredients as 'fixed' in data section" do
69
+ ingredient_data = result[:data][:ingredients]
70
+
71
+ # salt is fixed, flour is scaled, pepper has no quantity
72
+ expect(ingredient_data).to eq(%w[fixed scaled noQuantity])
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Cooklang::Formatters::Json do
6
+ describe "#generate" do
7
+ let(:recipe) do
8
+ recipe_text = File.read(File.join(__dir__, "..", "fixtures", "comprehensive_recipe.cook"))
9
+ Cooklang.parse(recipe_text)
10
+ end
11
+ let(:json_output) { described_class.new(recipe).generate }
12
+ let(:parsed) { JSON.parse(json_output) }
13
+
14
+ it "generates valid JSON" do
15
+ expect { JSON.parse(json_output) }.not_to raise_error
16
+ end
17
+
18
+ it "includes metadata with proper JSON types" do
19
+ expect(parsed["metadata"]["map"]["title"]).to eq("Uncle Roger's Ultimate Fried Rice & Dumplings")
20
+ expect(parsed["metadata"]["map"]["servings"]).to eq(4)
21
+ end
22
+
23
+ it "converts decimal quantities correctly" do
24
+ ingredients = parsed["ingredients"]
25
+
26
+ # MSG with decimal quantity
27
+ msg = ingredients.find { |i| i["name"] == "msg" }
28
+ expect(msg["quantity"]["value"]["value"]["value"]).to eq(0.5)
29
+
30
+ # White pepper with null quantity
31
+ white_pepper = ingredients.find { |i| i["name"] == "white pepper" }
32
+ expect(white_pepper["quantity"]).to be_nil
33
+ end
34
+
35
+ it "handles named timers and decimal durations" do
36
+ timers = parsed["timers"]
37
+
38
+ # Named timer
39
+ named_timer = timers.find { |t| t["name"] == "quick scramble" }
40
+ expect(named_timer["quantity"]["value"]["value"]["value"]).to eq(45.0)
41
+
42
+ # Decimal duration
43
+ decimal_timer = timers.find { |t| t["quantity"]["value"]["value"]["value"] == 2.5 }
44
+ expect(decimal_timer).not_to be_nil
45
+ end
46
+
47
+ it "formats with indentation when requested" do
48
+ formatted = described_class.new(recipe).generate(indent: " ")
49
+ expect(formatted).to include(" \"metadata\"")
50
+ # Just check that it's longer than compact format (indentation adds space)
51
+ compact = described_class.new(recipe).generate
52
+ expect(formatted.length).to be > compact.length
53
+ end
54
+ end
55
+ end
@@ -3,187 +3,49 @@
3
3
  require "spec_helper"
4
4
 
5
5
  RSpec.describe Cooklang::Formatters::Text do
6
- let(:recipe) do
7
- Cooklang::Recipe.new(
8
- ingredients: ingredients,
9
- steps: steps,
10
- cookware: [],
11
- timers: [],
12
- metadata: {},
13
- sections: [],
14
- notes: []
15
- )
16
- end
17
- let(:formatter) { described_class.new(recipe) }
18
-
19
- describe "#to_s" do
20
- context "with ingredients only" do
21
- let(:ingredients) do
22
- [
23
- Cooklang::Ingredient.new(name: "flour", quantity: 125, unit: "g"),
24
- Cooklang::Ingredient.new(name: "milk", quantity: 250, unit: "ml"),
25
- Cooklang::Ingredient.new(name: "eggs", quantity: 3),
26
- Cooklang::Ingredient.new(name: "butter"),
27
- Cooklang::Ingredient.new(name: "sea salt", quantity: 1, unit: "pinch")
28
- ]
29
- end
30
- let(:steps) { [] }
31
-
32
- it "formats ingredients with aligned columns" do
33
- expected = <<~OUTPUT.strip
34
- Ingredients:
35
- flour 125 g
36
- milk 250 ml
37
- eggs 3
38
- butter some
39
- sea salt 1 pinch
40
- OUTPUT
41
-
42
- expect(formatter.to_s).to eq(expected)
43
- end
6
+ describe "#generate" do
7
+ let(:recipe) do
8
+ recipe_text = File.read(File.join(__dir__, "..", "fixtures", "comprehensive_recipe.cook"))
9
+ Cooklang.parse(recipe_text)
44
10
  end
11
+ let(:output) { described_class.new(recipe).generate }
45
12
 
46
- context "with steps only" do
47
- let(:ingredients) { [] }
48
- let(:steps) do
49
- [
50
- Cooklang::Step.new(segments: [
51
- { type: "text", value: "Crack the " },
52
- { type: "ingredient", value: "eggs", name: "eggs" },
53
- { type: "text", value: " into a blender" }
54
- ]),
55
- Cooklang::Step.new(segments: [
56
- { type: "text", value: "Pour into a bowl and leave to stand for " },
57
- { type: "timer", value: "15 minutes", name: nil },
58
- { type: "text", value: "." }
59
- ])
60
- ]
61
- end
62
-
63
- it "formats steps with numbered list" do
64
- expected = <<~OUTPUT.strip
65
- Steps:
66
- 1. Crack the eggs into a blender
67
- 2. Pour into a bowl and leave to stand for 15 minutes.
68
- OUTPUT
69
-
70
- expect(formatter.to_s).to eq(expected)
71
- end
13
+ it "includes Ingredients and Steps sections" do
14
+ expect(output).to include("Ingredients:")
15
+ expect(output).to include("Steps:")
72
16
  end
73
17
 
74
- context "with both ingredients and steps" do
75
- let(:ingredients) do
76
- [
77
- Cooklang::Ingredient.new(name: "butter"),
78
- Cooklang::Ingredient.new(name: "eggs", quantity: 3),
79
- Cooklang::Ingredient.new(name: "flour", quantity: 125, unit: "g"),
80
- Cooklang::Ingredient.new(name: "milk", quantity: 250, unit: "ml"),
81
- Cooklang::Ingredient.new(name: "sea salt", quantity: 1, unit: "pinch")
82
- ]
83
- end
84
- let(:steps) do
85
- [
86
- Cooklang::Step.new(segments: [
87
- { type: "text", value: "Crack the " },
88
- { type: "ingredient", value: "eggs", name: "eggs" },
89
- { type: "text", value: " into a blender, then add the " },
90
- { type: "ingredient", value: "flour", name: "flour" },
91
- { type: "text", value: ", " },
92
- { type: "ingredient", value: "milk", name: "milk" },
93
- { type: "text", value: " and " },
94
- { type: "ingredient", value: "sea salt", name: "sea salt" },
95
- { type: "text", value: "." }
96
- ]),
97
- Cooklang::Step.new(segments: [
98
- { type: "text", value: "Pour into a bowl and leave to stand for " },
99
- { type: "timer", value: "15 minutes", name: nil },
100
- { type: "text", value: "." }
101
- ]),
102
- Cooklang::Step.new(segments: [
103
- { type: "text", value: "Melt the " },
104
- { type: "ingredient", value: "butter", name: "butter" },
105
- { type: "text", value: " in a large non-stick " },
106
- { type: "cookware", value: "frying pan", name: "frying pan" },
107
- { type: "text", value: "." }
108
- ])
109
- ]
110
- end
111
-
112
- it "formats complete recipe" do
113
- expected = <<~OUTPUT.strip
114
- Ingredients:
115
- butter some
116
- eggs 3
117
- flour 125 g
118
- milk 250 ml
119
- sea salt 1 pinch
120
-
121
- Steps:
122
- 1. Crack the eggs into a blender, then add the flour, milk and sea salt.
123
- 2. Pour into a bowl and leave to stand for 15 minutes.
124
- 3. Melt the butter in a large non-stick frying pan.
125
- OUTPUT
126
-
127
- expect(formatter.to_s).to eq(expected)
128
- end
18
+ it "formats ingredients with quantities and units" do
19
+ expect(output).to match(/shallots\s+2/)
20
+ expect(output).to match(/garlic\s+4 cloves/)
21
+ expect(output).to match(/msg\s+0\.5 tsp/)
22
+ expect(output).to match(/white pepper\s+some/)
129
23
  end
130
24
 
131
- context "with empty recipe" do
132
- let(:ingredients) { [] }
133
- let(:steps) { [] }
134
-
135
- it "returns empty string" do
136
- expect(formatter.to_s).to eq("")
137
- end
138
- end
139
-
140
- context "with various ingredient formats" do
141
- let(:ingredients) do
142
- [
143
- Cooklang::Ingredient.new(name: "onion", quantity: 1),
144
- Cooklang::Ingredient.new(name: "olive oil", unit: "drizzle"),
145
- Cooklang::Ingredient.new(name: "salt"),
146
- Cooklang::Ingredient.new(name: "pepper", quantity: "some", unit: "grinds")
147
- ]
148
- end
149
- let(:steps) { [] }
150
-
151
- it "handles missing quantities and units gracefully" do
152
- expected = <<~OUTPUT.strip
153
- Ingredients:
154
- onion 1
155
- olive oil drizzle
156
- salt some
157
- pepper some grinds
158
- OUTPUT
159
-
160
- expect(formatter.to_s).to eq(expected)
161
- end
162
- end
163
- end
164
-
165
- describe "#format_quantity_unit" do
166
- let(:formatter) { described_class.new(recipe) }
167
- let(:recipe) { double("recipe") }
25
+ it "formats steps with sequential numbering" do
26
+ lines = output.split("\n")
27
+ step_lines = lines.select { |line| line.match(/^\s*\d+\./) }
168
28
 
169
- it "formats quantity with unit" do
170
- ingredient = Cooklang::Ingredient.new(name: "flour", quantity: 125, unit: "g")
171
- expect(formatter.send(:format_quantity_unit, ingredient)).to eq("125 g")
29
+ expect(step_lines.size).to be >= 10
30
+ expect(step_lines.first).to match(/1\..*/)
31
+ expect(step_lines.last).to match(/\d+\..*/)
172
32
  end
173
33
 
174
- it "formats quantity without unit" do
175
- ingredient = Cooklang::Ingredient.new(name: "eggs", quantity: 3)
176
- expect(formatter.send(:format_quantity_unit, ingredient)).to eq("3")
34
+ it "includes ingredient names in step text" do
35
+ expect(output).to include("shallots")
36
+ expect(output).to include("garlic")
37
+ expect(output).to include("day-old rice")
177
38
  end
178
39
 
179
- it "formats unit without quantity" do
180
- ingredient = Cooklang::Ingredient.new(name: "olive oil", unit: "drizzle")
181
- expect(formatter.send(:format_quantity_unit, ingredient)).to eq("drizzle")
40
+ it "includes timer references in steps" do
41
+ expect(output).to include("2 minutes")
42
+ expect(output).to include("30 seconds")
43
+ expect(output).to include("quick scramble")
182
44
  end
183
45
 
184
- it "handles missing quantity and unit" do
185
- ingredient = Cooklang::Ingredient.new(name: "salt")
186
- expect(formatter.send(:format_quantity_unit, ingredient)).to eq("some")
46
+ it "includes cookware references" do
47
+ expect(output).to include("wok")
48
+ expect(output).to include("mixing bowl")
187
49
  end
188
50
  end
189
51
  end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Canonical Metadata Support" do
6
+ let(:parser) { Cooklang::Parser.new }
7
+
8
+ describe "Complex YAML frontmatter" do
9
+ it "supports arrays in metadata" do
10
+ recipe_text = <<~COOK
11
+ ---
12
+ title: Pasta Dish
13
+ tags: [italian, pasta, quick]
14
+ diet:
15
+ - vegetarian
16
+ - gluten-free
17
+ ---
18
+ @pasta{200%g}
19
+ COOK
20
+
21
+ recipe = parser.parse(recipe_text)
22
+
23
+ expect(recipe.metadata["tags"]).to eq(["italian", "pasta", "quick"])
24
+ expect(recipe.metadata["diet"]).to eq(["vegetarian", "gluten-free"])
25
+ end
26
+
27
+ it "supports nested hash structures" do
28
+ recipe_text = <<~COOK
29
+ ---
30
+ title: Recipe
31
+ source:
32
+ name: My Cookbook
33
+ url: https://example.com/recipe
34
+ author: Jane Doe
35
+ time:
36
+ prep: 15 minutes
37
+ cook: 30 minutes
38
+ ---
39
+ @flour{100%g}
40
+ COOK
41
+
42
+ recipe = parser.parse(recipe_text)
43
+
44
+ expect(recipe.metadata["source"]).to be_a(Hash)
45
+ expect(recipe.metadata["source"]["name"]).to eq("My Cookbook")
46
+ expect(recipe.metadata["source"]["url"]).to eq("https://example.com/recipe")
47
+ expect(recipe.metadata["source"]["author"]).to eq("Jane Doe")
48
+
49
+ expect(recipe.metadata["time"]).to be_a(Hash)
50
+ expect(recipe.metadata["time"]["prep"]).to eq("15 minutes")
51
+ expect(recipe.metadata["time"]["cook"]).to eq("30 minutes")
52
+ end
53
+
54
+ it "supports mixed data types" do
55
+ recipe_text = <<~COOK
56
+ ---
57
+ title: Test Recipe
58
+ servings: 4
59
+ rating: 4.5
60
+ published: true
61
+ custom_field: Some value
62
+ numbers: [1, 2, 3]
63
+ ---
64
+ @salt
65
+ COOK
66
+
67
+ recipe = parser.parse(recipe_text)
68
+
69
+ expect(recipe.metadata["title"]).to eq("Test Recipe")
70
+ expect(recipe.metadata["servings"]).to eq(4)
71
+ expect(recipe.metadata["rating"]).to eq(4.5)
72
+ expect(recipe.metadata["published"]).to eq(true)
73
+ expect(recipe.metadata["custom_field"]).to eq("Some value")
74
+ expect(recipe.metadata["numbers"]).to eq([1, 2, 3])
75
+ end
76
+
77
+ it "raises error on invalid YAML" do
78
+ recipe_text = <<~COOK
79
+ ---
80
+ title: Test
81
+ tags: [invalid
82
+ not closed
83
+ ---
84
+ @salt
85
+ COOK
86
+
87
+ expect { parser.parse(recipe_text) }.to raise_error(Psych::SyntaxError)
88
+ end
89
+ end
90
+
91
+ describe "Canonical fields work as plain Hash values" do
92
+ let(:metadata) { Cooklang::Metadata.new }
93
+
94
+ it "stores servings as number or string" do
95
+ metadata["servings"] = 4
96
+ expect(metadata["servings"]).to eq(4)
97
+
98
+ metadata["servings"] = "6 cups worth"
99
+ expect(metadata["servings"]).to eq("6 cups worth")
100
+ end
101
+
102
+ it "stores time fields as nested or flat" do
103
+ metadata["time"] = { "prep" => "10 min", "cook" => "20 min" }
104
+ expect(metadata["time"]["prep"]).to eq("10 min")
105
+ expect(metadata["time"]["cook"]).to eq("20 min")
106
+
107
+ metadata.clear
108
+ metadata["prep_time"] = "15 minutes"
109
+ metadata["cook_time"] = "25 minutes"
110
+ expect(metadata["prep_time"]).to eq("15 minutes")
111
+ expect(metadata["cook_time"]).to eq("25 minutes")
112
+ end
113
+
114
+ it "stores source as string or nested hash" do
115
+ metadata["source"] = {
116
+ "name" => "The Cookbook",
117
+ "url" => "https://example.com",
118
+ "author" => "Chef Name"
119
+ }
120
+ expect(metadata["source"]["name"]).to eq("The Cookbook")
121
+ expect(metadata["source"]["url"]).to eq("https://example.com")
122
+ expect(metadata["source"]["author"]).to eq("Chef Name")
123
+
124
+ metadata.clear
125
+ metadata["source"] = "Grandma's recipe box"
126
+ expect(metadata["source"]).to eq("Grandma's recipe box")
127
+ end
128
+
129
+ it "stores tags and diet as arrays" do
130
+ metadata["tags"] = ["pasta", "italian"]
131
+ expect(metadata["tags"]).to eq(["pasta", "italian"])
132
+
133
+ metadata["diet"] = ["vegan", "gluten-free"]
134
+ expect(metadata["diet"]).to eq(["vegan", "gluten-free"])
135
+ end
136
+
137
+ it "stores other canonical fields" do
138
+ metadata["category"] = "dessert"
139
+ metadata["description"] = "A delicious recipe"
140
+ metadata["difficulty"] = "easy"
141
+ metadata["cuisine"] = "Italian"
142
+ metadata["locale"] = "en_US"
143
+ metadata["images"] = ["url1", "url2"]
144
+
145
+ expect(metadata["category"]).to eq("dessert")
146
+ expect(metadata["description"]).to eq("A delicious recipe")
147
+ expect(metadata["difficulty"]).to eq("easy")
148
+ expect(metadata["cuisine"]).to eq("Italian")
149
+ expect(metadata["locale"]).to eq("en_US")
150
+ expect(metadata["images"]).to eq(["url1", "url2"])
151
+ end
152
+ end
153
+
154
+ describe "Custom metadata fields" do
155
+ it "allows any custom fields" do
156
+ metadata = Cooklang::Metadata.new(
157
+ "wine_pairing" => "Pinot Noir",
158
+ "my_rating" => 5,
159
+ "special_equipment" => ["thermometer", "stand mixer"],
160
+ "notes_to_self" => "Double the garlic next time"
161
+ )
162
+
163
+ expect(metadata["wine_pairing"]).to eq("Pinot Noir")
164
+ expect(metadata["my_rating"]).to eq(5)
165
+ expect(metadata["special_equipment"]).to eq(["thermometer", "stand mixer"])
166
+ expect(metadata["notes_to_self"]).to eq("Double the garlic next time")
167
+ end
168
+ end
169
+ end
data/spec/lexer_spec.rb CHANGED
@@ -146,12 +146,12 @@ RSpec.describe Cooklang::Lexer do
146
146
  end
147
147
  end
148
148
 
149
- context "with section markers" do
149
+ context "with equals (section markers)" do
150
150
  it "tokenizes single equals" do
151
151
  lexer = described_class.new("= Dough")
152
152
  tokens = lexer.tokenize
153
153
 
154
- expect(tokens.map(&:type)).to eq(%i[section_marker text])
154
+ expect(tokens.map(&:type)).to eq(%i[equals text])
155
155
  expect(tokens.map(&:value)).to eq(["=", " Dough"])
156
156
  end
157
157
 
@@ -159,7 +159,7 @@ RSpec.describe Cooklang::Lexer do
159
159
  lexer = described_class.new("== Filling ==")
160
160
  tokens = lexer.tokenize
161
161
 
162
- expect(tokens.map(&:type)).to eq(%i[section_marker text section_marker])
162
+ expect(tokens.map(&:type)).to eq(%i[equals text equals])
163
163
  expect(tokens.map(&:value)).to eq(["==", " Filling ", "=="])
164
164
  end
165
165
 
@@ -167,7 +167,7 @@ RSpec.describe Cooklang::Lexer do
167
167
  lexer = described_class.new("=== Section ===")
168
168
  tokens = lexer.tokenize
169
169
 
170
- expect(tokens.map(&:type)).to eq(%i[section_marker text section_marker])
170
+ expect(tokens.map(&:type)).to eq(%i[equals text equals])
171
171
  expect(tokens.map(&:value)).to eq(["===", " Section ", "==="])
172
172
  end
173
173
  end
@@ -47,6 +47,20 @@ RSpec.describe Cooklang::Ingredient do
47
47
  expect(ingredient.notes).to eq("sifted")
48
48
  expect(ingredient.notes).to be_frozen
49
49
  end
50
+
51
+ it "defaults fixed to false" do
52
+ ingredient = described_class.new(name: "salt")
53
+
54
+ expect(ingredient.fixed).to be false
55
+ expect(ingredient.fixed?).to be false
56
+ end
57
+
58
+ it "accepts fixed parameter" do
59
+ ingredient = described_class.new(name: "salt", quantity: 1, unit: "tsp", fixed: true)
60
+
61
+ expect(ingredient.fixed).to be true
62
+ expect(ingredient.fixed?).to be true
63
+ end
50
64
  end
51
65
 
52
66
  describe "#to_s" do
@@ -94,16 +108,34 @@ RSpec.describe Cooklang::Ingredient do
94
108
  name: "flour",
95
109
  quantity: 125,
96
110
  unit: "g",
97
- notes: "sifted"
111
+ notes: "sifted",
112
+ fixed: false
98
113
  }
99
114
 
100
115
  expect(ingredient.to_h).to eq(expected)
101
116
  end
102
117
 
103
- it "omits nil attributes" do
118
+ it "omits nil attributes but includes fixed" do
104
119
  ingredient = described_class.new(name: "salt")
105
120
 
106
- expect(ingredient.to_h).to eq({ name: "salt" })
121
+ expect(ingredient.to_h).to eq({ name: "salt", fixed: false })
122
+ end
123
+
124
+ it "includes fixed when true" do
125
+ ingredient = described_class.new(name: "salt", quantity: 1, unit: "tsp", fixed: true)
126
+
127
+ expect(ingredient.to_h).to eq({
128
+ name: "salt",
129
+ quantity: 1,
130
+ unit: "tsp",
131
+ fixed: true
132
+ })
133
+ end
134
+
135
+ it "includes fixed as false when not fixed" do
136
+ ingredient = described_class.new(name: "salt", quantity: 1, unit: "tsp", fixed: false)
137
+
138
+ expect(ingredient.to_h[:fixed]).to be false
107
139
  end
108
140
  end
109
141
 
@@ -129,6 +161,13 @@ RSpec.describe Cooklang::Ingredient do
129
161
  expect(ingredient1).not_to eq(ingredient2)
130
162
  end
131
163
 
164
+ it "returns false for ingredients with different fixed values" do
165
+ ingredient1 = described_class.new(name: "salt", quantity: 1, fixed: true)
166
+ ingredient2 = described_class.new(name: "salt", quantity: 1, fixed: false)
167
+
168
+ expect(ingredient1).not_to eq(ingredient2)
169
+ end
170
+
132
171
  it "returns false for non-Ingredient objects" do
133
172
  ingredient = described_class.new(name: "flour")
134
173
 
@@ -172,6 +211,23 @@ RSpec.describe Cooklang::Ingredient do
172
211
  expect(ingredient).not_to have_notes
173
212
  end
174
213
  end
214
+
215
+ describe "#fixed?" do
216
+ it "returns true when fixed is true" do
217
+ ingredient = described_class.new(name: "salt", quantity: 1, fixed: true)
218
+ expect(ingredient).to be_fixed
219
+ end
220
+
221
+ it "returns false when fixed is false" do
222
+ ingredient = described_class.new(name: "salt", quantity: 1, fixed: false)
223
+ expect(ingredient).not_to be_fixed
224
+ end
225
+
226
+ it "returns false by default" do
227
+ ingredient = described_class.new(name: "salt")
228
+ expect(ingredient).not_to be_fixed
229
+ end
230
+ end
175
231
  end
176
232
 
177
233
  describe "#hash" do