air_test 0.1.1 → 0.1.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 040bb120383aa9508fb9ce509f8009352ad18cd1b46fecf6bc4e033ae0a0c3df
4
- data.tar.gz: aa06b9d771337b2060f4cd560c8be1cfbe8cae5d6539e55ba3fff82b211ffe53
3
+ metadata.gz: 13964be83c39a58d703815567f4b0beb8f56c89b5d3a178ea3c3cf333cbd7180
4
+ data.tar.gz: 8420831fd31266b7987425e96cfca1ad36c4cad815fbff36030ec00cb1fbf949
5
5
  SHA512:
6
- metadata.gz: 2389b3c05f0082746c89f5b5aaeb8a364101198b0ab7eb7b12c2878e9b0c2d6e11c68cdf5d2a7232947bb0af8f79f7572ae9748c554991cf40d42bd5b535ce69
7
- data.tar.gz: f344eac155533afc45aff9518b1bd9708e77ee211f42b744dbaaaf54badd85ec37eb5029067ccf4467150e5b7764369b0686f62db15dcd9852a486bf725c7998
6
+ metadata.gz: 92a5cb14f9398aa195d305c3c43e9a80b57befea1d330215e1f2608db31fe21100c297b14936a2400ef91739cc37a5ee49c3b9a593a00e17715a6c8f0dd4e096
7
+ data.tar.gz: 591bd3884a1709ddf9897ae990dfe5fcceada1b32d479bd028840e1dea29610ea0c9dddca500435c8b6db3f9ad700b7f14a37b82b792f61e675ac63b385022f9
data/.rubocop.yml CHANGED
@@ -6,3 +6,282 @@ Style/StringLiterals:
6
6
 
7
7
  Style/StringLiteralsInInterpolation:
8
8
  EnforcedStyle: double_quotes
9
+
10
+ Gemspec/AddRuntimeDependency:
11
+ Enabled: true
12
+ Gemspec/AttributeAssignment:
13
+ Enabled: true
14
+ Gemspec/DeprecatedAttributeAssignment:
15
+ Enabled: true
16
+ Gemspec/DevelopmentDependencies:
17
+ Enabled: true
18
+ Gemspec/RequireMFA:
19
+ Enabled: true
20
+ Layout/LineContinuationLeadingSpace:
21
+ Enabled: true
22
+ Layout/LineContinuationSpacing:
23
+ Enabled: true
24
+ Layout/LineEndStringConcatenationIndentation:
25
+ Enabled: true
26
+ Layout/SpaceBeforeBrackets:
27
+ Enabled: true
28
+ Lint/AmbiguousAssignment:
29
+ Enabled: true
30
+ Lint/AmbiguousOperatorPrecedence:
31
+ Enabled: true
32
+ Lint/AmbiguousRange:
33
+ Enabled: true
34
+ Lint/ArrayLiteralInRegexp:
35
+ Enabled: true
36
+ Lint/ConstantOverwrittenInRescue:
37
+ Enabled: true
38
+ Lint/ConstantReassignment:
39
+ Enabled: true
40
+ Lint/CopDirectiveSyntax:
41
+ Enabled: true
42
+ Lint/DeprecatedConstants:
43
+ Enabled: true
44
+ Lint/DuplicateBranch:
45
+ Enabled: true
46
+ Lint/DuplicateMagicComment:
47
+ Enabled: true
48
+ Lint/DuplicateMatchPattern:
49
+ Enabled: true
50
+ Lint/DuplicateRegexpCharacterClassElement:
51
+ Enabled: true
52
+ Lint/DuplicateSetElement:
53
+ Enabled: true
54
+ Lint/EmptyBlock:
55
+ Enabled: true
56
+ Lint/EmptyClass:
57
+ Enabled: true
58
+ Lint/EmptyInPattern:
59
+ Enabled: true
60
+ Lint/HashNewWithKeywordArgumentsAsDefault:
61
+ Enabled: true
62
+ Lint/IncompatibleIoSelectWithFiberScheduler:
63
+ Enabled: true
64
+ Lint/ItWithoutArgumentsInBlock:
65
+ Enabled: true
66
+ Lint/LambdaWithoutLiteralBlock:
67
+ Enabled: true
68
+ Lint/LiteralAssignmentInCondition:
69
+ Enabled: true
70
+ Lint/MixedCaseRange:
71
+ Enabled: true
72
+ Lint/NoReturnInBeginEndBlocks:
73
+ Enabled: true
74
+ Lint/NonAtomicFileOperation:
75
+ Enabled: true
76
+ Lint/NumberedParameterAssignment:
77
+ Enabled: true
78
+ Lint/NumericOperationWithConstantResult:
79
+ Enabled: true
80
+ Lint/OrAssignmentToConstant:
81
+ Enabled: true
82
+ Lint/RedundantDirGlobSort:
83
+ Enabled: true
84
+ Lint/RedundantRegexpQuantifiers:
85
+ Enabled: true
86
+ Lint/RedundantTypeConversion:
87
+ Enabled: true
88
+ Lint/RefinementImportMethods:
89
+ Enabled: true
90
+ Lint/RequireRangeParentheses:
91
+ Enabled: true
92
+ Lint/RequireRelativeSelfPath:
93
+ Enabled: true
94
+ Lint/SharedMutableDefault:
95
+ Enabled: true
96
+ Lint/SuppressedExceptionInNumberConversion:
97
+ Enabled: true
98
+ Lint/SymbolConversion:
99
+ Enabled: true
100
+ Lint/ToEnumArguments:
101
+ Enabled: true
102
+ Lint/TripleQuotes:
103
+ Enabled: true
104
+ Lint/UnescapedBracketInRegexp:
105
+ Enabled: true
106
+ Lint/UnexpectedBlockArity:
107
+ Enabled: true
108
+ Lint/UnmodifiedReduceAccumulator:
109
+ Enabled: true
110
+ Lint/UselessConstantScoping:
111
+ Enabled: true
112
+ Lint/UselessDefaultValueArgument:
113
+ Enabled: true
114
+ Lint/UselessDefined:
115
+ Enabled: true
116
+ Lint/UselessNumericOperation:
117
+ Enabled: true
118
+ Lint/UselessOr:
119
+ Enabled: true
120
+ Lint/UselessRescue:
121
+ Enabled: true
122
+ Lint/UselessRuby2Keywords:
123
+ Enabled: true
124
+ Metrics/CollectionLiteralLength:
125
+ Enabled: true
126
+ Naming/BlockForwarding:
127
+ Enabled: true
128
+ Naming/PredicateMethod:
129
+ Enabled: true
130
+ Security/CompoundHash:
131
+ Enabled: true
132
+ Security/IoMethods:
133
+ Enabled: true
134
+ Style/AmbiguousEndlessMethodDefinition:
135
+ Enabled: true
136
+ Style/ArgumentsForwarding:
137
+ Enabled: true
138
+ Style/ArrayIntersect:
139
+ Enabled: true
140
+ Style/BitwisePredicate:
141
+ Enabled: true
142
+ Style/CollectionCompact:
143
+ Enabled: true
144
+ Style/CollectionQuerying:
145
+ Enabled: true
146
+ Style/CombinableDefined:
147
+ Enabled: true
148
+ Style/ComparableBetween:
149
+ Enabled: true
150
+ Style/ComparableClamp:
151
+ Enabled: true
152
+ Style/ConcatArrayLiterals:
153
+ Enabled: true
154
+ Style/DataInheritance:
155
+ Enabled: true
156
+ Style/DigChain:
157
+ Enabled: true
158
+ Style/DirEmpty:
159
+ Enabled: true
160
+ Style/DocumentDynamicEvalDefinition:
161
+ Enabled: true
162
+ Style/EmptyHeredoc:
163
+ Enabled: true
164
+ Style/EmptyStringInsideInterpolation:
165
+ Enabled: true
166
+ Style/EndlessMethod:
167
+ Enabled: true
168
+ Style/EnvHome:
169
+ Enabled: true
170
+ Style/ExactRegexpMatch:
171
+ Enabled: true
172
+ Style/FetchEnvVar:
173
+ Enabled: true
174
+ Style/FileEmpty:
175
+ Enabled: true
176
+ Style/FileNull:
177
+ Enabled: true
178
+ Style/FileRead:
179
+ Enabled: true
180
+ Style/FileTouch:
181
+ Enabled: true
182
+ Style/FileWrite:
183
+ Enabled: true
184
+ Style/HashConversion:
185
+ Enabled: true
186
+ Style/HashExcept:
187
+ Enabled: true
188
+ Style/HashFetchChain:
189
+ Enabled: true
190
+ Style/HashSlice:
191
+ Enabled: true
192
+ Style/IfWithBooleanLiteralBranches:
193
+ Enabled: true
194
+ Style/InPatternThen:
195
+ Enabled: true
196
+ Style/ItAssignment:
197
+ Enabled: true
198
+ Style/ItBlockParameter:
199
+ Enabled: true
200
+ Style/KeywordArgumentsMerging:
201
+ Enabled: true
202
+ Style/MagicCommentFormat:
203
+ Enabled: true
204
+ Style/MapCompactWithConditionalBlock:
205
+ Enabled: true
206
+ Style/MapIntoArray:
207
+ Enabled: true
208
+ Style/MapToHash:
209
+ Enabled: true
210
+ Style/MapToSet:
211
+ Enabled: true
212
+ Style/MinMaxComparison:
213
+ Enabled: true
214
+ Style/MultilineInPatternThen:
215
+ Enabled: true
216
+ Style/NegatedIfElseCondition:
217
+ Enabled: true
218
+ Style/NestedFileDirname:
219
+ Enabled: true
220
+ Style/NilLambda:
221
+ Enabled: true
222
+ Style/NumberedParameters:
223
+ Enabled: true
224
+ Style/NumberedParametersLimit:
225
+ Enabled: true
226
+ Style/ObjectThen:
227
+ Enabled: true
228
+ Style/OpenStructUse:
229
+ Enabled: true
230
+ Style/OperatorMethodCall:
231
+ Enabled: true
232
+ Style/QuotedSymbols:
233
+ Enabled: true
234
+ Style/RedundantArgument:
235
+ Enabled: true
236
+ Style/RedundantArrayConstructor:
237
+ Enabled: true
238
+ Style/RedundantArrayFlatten:
239
+ Enabled: true
240
+ Style/RedundantConstantBase:
241
+ Enabled: true
242
+ Style/RedundantCurrentDirectoryInPath:
243
+ Enabled: true
244
+ Style/RedundantDoubleSplatHashBraces:
245
+ Enabled: true
246
+ Style/RedundantEach:
247
+ Enabled: true
248
+ Style/RedundantFilterChain:
249
+ Enabled: true
250
+ Style/RedundantFormat:
251
+ Enabled: true
252
+ Style/RedundantHeredocDelimiterQuotes:
253
+ Enabled: true
254
+ Style/RedundantInitialize:
255
+ Enabled: true
256
+ Style/RedundantInterpolationUnfreeze:
257
+ Enabled: true
258
+ Style/RedundantLineContinuation:
259
+ Enabled: true
260
+ Style/RedundantRegexpArgument:
261
+ Enabled: true
262
+ Style/RedundantRegexpConstructor:
263
+ Enabled: true
264
+ Style/RedundantSelfAssignmentBranch:
265
+ Enabled: true
266
+ Style/RedundantStringEscape:
267
+ Enabled: true
268
+ Style/ReturnNilInPredicateMethodDefinition:
269
+ Enabled: true
270
+ Style/SafeNavigationChainLength:
271
+ Enabled: true
272
+ Style/SelectByRegexp:
273
+ Enabled: true
274
+ Style/SendWithLiteralMethodName:
275
+ Enabled: true
276
+ Style/SingleLineDoEndBlock:
277
+ Enabled: true
278
+ Style/StringChars:
279
+ Enabled: true
280
+ Style/SuperArguments:
281
+ Enabled: true
282
+ Style/SuperWithArgsParentheses:
283
+ Enabled: true
284
+ Style/SwapValues:
285
+ Enabled: true
286
+ Style/YAMLFileRead:
287
+ Enabled: true
data/exe/air_test ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+
6
+ GREEN = "\e[32m"
7
+ YELLOW = "\e[33m"
8
+ RED = "\e[31m"
9
+ CYAN = "\e[36m"
10
+ RESET = "\e[0m"
11
+
12
+ puts "#{CYAN}🚀 Initializing AirTest for your Rails project...#{RESET}\n"
13
+
14
+ initializer_path = "config/initializers/air_test.rb"
15
+ if File.exist?(initializer_path)
16
+ puts "#{YELLOW}⚠️ #{initializer_path} already exists. Skipping.#{RESET}"
17
+ else
18
+ FileUtils.mkdir_p(File.dirname(initializer_path))
19
+ File.write(initializer_path, <<~RUBY)
20
+ AirTest.configure do |config|
21
+ config.notion_token = ENV['NOTION_TOKEN']
22
+ config.notion_database_id = ENV['NOTION_DATABASE_ID']
23
+ config.github_token = ENV['GITHUB_BOT_TOKEN']
24
+ config.repo = 'your-org/your-repo' # format: 'organization/repo_name'
25
+ end
26
+ RUBY
27
+ puts "#{GREEN}✅ Created #{initializer_path}#{RESET}"
28
+ end
29
+
30
+ ["spec/features", "spec/steps"].each do |dir|
31
+ if Dir.exist?(dir)
32
+ puts "#{YELLOW}⚠️ #{dir} already exists. Skipping.#{RESET}"
33
+ else
34
+ FileUtils.mkdir_p(dir)
35
+ puts "#{GREEN}✅ Created #{dir}/#{RESET}"
36
+ end
37
+ end
38
+
39
+ example_env = ".env.air_test.example"
40
+ if File.exist?(example_env)
41
+ puts "#{YELLOW}⚠️ #{example_env} already exists. Skipping.#{RESET}"
42
+ else
43
+ File.write(example_env, <<~ENV)
44
+ NOTION_TOKEN=your_notion_token
45
+ NOTION_DATABASE_ID=your_notion_database_id
46
+ GITHUB_BOT_TOKEN=your_github_token
47
+ ENV
48
+ puts "#{GREEN}✅ Created #{example_env}#{RESET}"
49
+ end
50
+
51
+ puts "\n🔎 Checking environment variables..."
52
+ missing = []
53
+ %w[NOTION_TOKEN NOTION_DATABASE_ID GITHUB_BOT_TOKEN].each do |var|
54
+ if ENV[var].nil? || ENV[var].empty?
55
+ puts "#{YELLOW}⚠️ #{var} is not set!#{RESET}"
56
+ missing << var
57
+ else
58
+ puts "#{GREEN}✅ #{var} is set#{RESET}"
59
+ end
60
+ end
61
+
62
+ puts "\n✨ All set! Next steps:"
63
+ puts " 1. Fill in your config/initializers/air_test.rb"
64
+ puts " 2. Add your tokens to .env or your environment"
65
+ puts " 3. Run: bundle exec rake air_test:generate_specs_from_notion"
66
+ puts "\nHappy testing! 🎉"
@@ -1,14 +1,16 @@
1
+ # Handles configuration for AirTest, including API tokens and environment variables.
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module AirTest
5
+ # Handles configuration for AirTest, including API tokens and environment variables.
4
6
  class Configuration
5
7
  attr_accessor :notion_token, :notion_database_id, :github_token, :repo
6
8
 
7
9
  def initialize
8
- @notion_token = ENV['NOTION_TOKEN']
9
- @notion_database_id = ENV['NOTION_DATABASE_ID']
10
- @github_token = ENV['GITHUB_BOT_TOKEN'] || ENV['GITHUB_TOKEN']
10
+ @notion_token = ENV.fetch("NOTION_TOKEN", nil)
11
+ @notion_database_id = ENV.fetch("NOTION_DATABASE_ID", nil)
12
+ @github_token = ENV["GITHUB_BOT_TOKEN"] || ENV.fetch("GITHUB_TOKEN", nil)
11
13
  @repo = nil
12
14
  end
13
15
  end
14
- end
16
+ end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
- require 'octokit'
2
+
3
+ require "octokit"
3
4
 
4
5
  module AirTest
6
+ # Handles GitHub API interactions for AirTest, such as commits and pull requests.
5
7
  class GithubClient
6
8
  def initialize(config = AirTest.configuration)
7
9
  @github_token = config.github_token
@@ -17,18 +19,18 @@ module AirTest
17
19
  end
18
20
  files.each { |f| system("git add -f #{f}") }
19
21
  has_changes = !system("git diff --cached --quiet")
20
- if has_changes
21
- system("git commit -m '#{commit_message}'")
22
- end
22
+ system("git commit -m '#{commit_message}'") if has_changes
23
23
  system("git push origin #{branch}")
24
24
  has_changes
25
25
  end
26
26
 
27
- def create_pull_request(branch, pr_title, pr_body, assignees: ['Notion'])
27
+ # rubocop:disable Metrics/MethodLength
28
+ def create_pull_request(branch, pr_title, pr_body, assignees: ["Notion"])
28
29
  return unless @client && @repo
30
+
29
31
  @client.create_pull_request(
30
32
  @repo,
31
- 'main',
33
+ "main",
32
34
  branch,
33
35
  pr_title,
34
36
  pr_body,
@@ -38,6 +40,7 @@ module AirTest
38
40
  warn "❌ Erreur lors de la création de la PR : #{e.message}"
39
41
  nil
40
42
  end
43
+ # rubocop:enable Metrics/MethodLength
41
44
 
42
45
  private
43
46
 
@@ -47,7 +50,7 @@ module AirTest
47
50
 
48
51
  def detect_repo_from_git
49
52
  remote_url = `git config --get remote.origin.url`.strip
50
- remote_url.split(/[:\/]/).last(2).join('/').gsub(/\.git$/, '')
53
+ remote_url.split(%r{[:/]}).last(2).join("/").gsub(/\.git$/, "")
51
54
  end
52
55
  end
53
- end
56
+ end
@@ -1,85 +1,95 @@
1
+ # Parses Notion tickets and extracts relevant information for spec generation in AirTest.
2
+ # rubocop:disable Metrics/ClassLength
1
3
  # frozen_string_literal: true
2
- require 'net/http'
3
- require 'json'
4
- require 'uri'
4
+
5
+ require "net/http"
6
+ require "json"
7
+ require "uri"
5
8
 
6
9
  module AirTest
10
+ # Parses Notion tickets and extracts relevant information for spec generation in AirTest.
7
11
  class NotionParser
8
12
  def initialize(config = AirTest.configuration)
9
13
  @database_id = config.notion_database_id
10
14
  @notion_token = config.notion_token
11
- @base_url = 'https://api.notion.com/v1'
15
+ @base_url = "https://api.notion.com/v1"
12
16
  end
13
17
 
14
18
  def fetch_tickets(limit: 5)
15
19
  uri = URI("#{@base_url}/databases/#{@database_id}/query")
16
20
  response = make_api_request(uri, { page_size: 100 })
17
- return [] unless response.code == '200'
21
+ return [] unless response.code == "200"
22
+
18
23
  data = JSON.parse(response.body)
19
- data['results'].first(limit)
24
+ data["results"].first(limit)
20
25
  end
21
26
 
22
27
  def parse_ticket_content(page_id)
23
28
  blocks = get_page_content(page_id)
24
29
  return nil unless blocks
30
+
25
31
  parse_content(blocks)
26
32
  end
27
33
 
28
34
  def extract_ticket_title(ticket)
29
- ticket.dig('properties', 'Projects', 'title', 0, 'plain_text') || 'No title'
35
+ ticket.dig("properties", "Projects", "title", 0, "plain_text") || "No title"
30
36
  end
31
37
 
32
38
  def extract_ticket_id(ticket)
33
- ticket.dig('properties', 'ID', 'unique_id', 'number') || 'No ID'
39
+ ticket.dig("properties", "ID", "unique_id", "number") || "No ID"
34
40
  end
35
41
 
36
42
  def extract_ticket_url(ticket)
37
- ticket['url'] || "https://www.notion.so/#{ticket['id'].gsub('-', '')}"
43
+ ticket["url"] || "https://www.notion.so/#{ticket["id"].gsub("-", "")}"
38
44
  end
39
45
 
40
46
  private
41
47
 
42
48
  def get_page_content(page_id)
43
49
  uri = URI("#{@base_url}/blocks/#{page_id}/children")
44
- response = make_api_request(uri, nil, 'GET')
45
- return nil unless response.code == '200'
50
+ response = make_api_request(uri, nil, "GET")
51
+ return nil unless response.code == "200"
52
+
46
53
  data = JSON.parse(response.body)
47
- data['results']
54
+ data["results"]
48
55
  end
49
56
 
50
- def make_api_request(uri, request_body = nil, method = 'POST')
57
+ # rubocop:disable Metrics/MethodLength
58
+ def make_api_request(uri, request_body = nil, method = "POST")
51
59
  http = Net::HTTP.new(uri.host, uri.port)
52
60
  http.use_ssl = true
53
- if method == 'GET'
61
+ if method == "GET"
54
62
  request = Net::HTTP::Get.new(uri)
55
63
  else
56
64
  request = Net::HTTP::Post.new(uri)
57
65
  request.body = request_body.to_json if request_body
58
66
  end
59
- request['Authorization'] = "Bearer #{@notion_token}"
60
- request['Notion-Version'] = '2022-06-28'
61
- request['Content-Type'] = 'application/json'
67
+ request["Authorization"] = "Bearer #{@notion_token}"
68
+ request["Notion-Version"] = "2022-06-28"
69
+ request["Content-Type"] = "application/json"
62
70
  http.request(request)
63
71
  end
72
+ # rubocop:enable Metrics/MethodLength
64
73
 
74
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
65
75
  def parse_content(blocks)
66
76
  parsed_data = {
67
- feature: '',
77
+ feature: "",
68
78
  scenarios: [],
69
- meta: { tags: [], priority: '', estimate: nil, assignee: '' }
79
+ meta: { tags: [], priority: "", estimate: nil, assignee: "" }
70
80
  }
71
81
  current_scenario = nil
72
82
  in_feature_block = false
73
83
  in_scenario_block = false
74
84
  blocks.each do |block|
75
- case block['type']
76
- when 'heading_1', 'heading_2', 'heading_3'
77
- heading_text = extract_text(block[block['type']]['rich_text'])
78
- if heading_text.downcase.include?('feature')
85
+ case block["type"]
86
+ when "heading_1", "heading_2", "heading_3"
87
+ heading_text = extract_text(block[block["type"]]["rich_text"])
88
+ if heading_text.downcase.include?("feature")
79
89
  in_feature_block = true
80
90
  in_scenario_block = false
81
91
  parsed_data[:feature] = heading_text
82
- elsif heading_text.downcase.include?('scenario')
92
+ elsif heading_text.downcase.include?("scenario")
83
93
  in_scenario_block = true
84
94
  in_feature_block = false
85
95
  current_scenario = { title: heading_text, steps: [] }
@@ -88,33 +98,36 @@ module AirTest
88
98
  in_feature_block = false
89
99
  in_scenario_block = false
90
100
  end
91
- when 'paragraph'
92
- text = extract_text(block['paragraph']['rich_text'])
101
+ when "paragraph"
102
+ text = extract_text(block["paragraph"]["rich_text"])
93
103
  next if text.empty?
104
+
94
105
  if in_feature_block
95
- parsed_data[:feature] += "\n" + text
106
+ parsed_data[:feature] += "\n#{text}"
96
107
  elsif in_scenario_block && current_scenario
97
108
  current_scenario[:steps] << text
98
109
  end
99
- when 'bulleted_list_item', 'numbered_list_item'
100
- text = extract_text(block[block['type']]['rich_text'])
110
+ when "bulleted_list_item", "numbered_list_item"
111
+ text = extract_text(block[block["type"]]["rich_text"])
101
112
  next if text.empty?
113
+
102
114
  if in_feature_block
103
- parsed_data[:feature] += "\n• " + text
115
+ parsed_data[:feature] += "\n• #{text}"
104
116
  elsif in_scenario_block && current_scenario
105
117
  current_scenario[:steps] << text
106
118
  end
107
- when 'callout'
108
- text = extract_text(block['callout']['rich_text'])
119
+ when "callout"
120
+ text = extract_text(block["callout"]["rich_text"])
109
121
  next if text.empty?
110
- if text.downcase.include?('tag')
122
+
123
+ if text.downcase.include?("tag")
111
124
  tags = extract_tags(text)
112
125
  parsed_data[:meta][:tags].concat(tags)
113
- elsif text.downcase.include?('priority')
126
+ elsif text.downcase.include?("priority")
114
127
  parsed_data[:meta][:priority] = extract_priority(text)
115
- elsif text.downcase.include?('estimate')
128
+ elsif text.downcase.include?("estimate")
116
129
  parsed_data[:meta][:estimate] = extract_estimate(text)
117
- elsif text.downcase.include?('assignee')
130
+ elsif text.downcase.include?("assignee")
118
131
  parsed_data[:meta][:assignee] = extract_assignee(text)
119
132
  end
120
133
  end
@@ -123,16 +136,18 @@ module AirTest
123
136
  parsed_data[:meta][:tags] = parsed_data[:meta][:tags].uniq
124
137
  parsed_data
125
138
  end
139
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
126
140
 
127
141
  def extract_text(rich_text_array)
128
- return '' unless rich_text_array
129
- rich_text_array.map { |text| text['plain_text'] }.join(' ')
142
+ return "" unless rich_text_array
143
+
144
+ rich_text_array.map { |text| text["plain_text"] }.join(" ")
130
145
  end
131
146
 
132
147
  def extract_tags(text)
133
148
  if text =~ /tags?:\s*(.+)/i
134
- tags_text = $1.strip
135
- tags_text.split(/[\,\s]+/).map(&:strip).reject(&:empty?)
149
+ tags_text = ::Regexp.last_match(1).strip
150
+ tags_text.split(/[,\s]+/).map(&:strip).reject(&:empty?)
136
151
  else
137
152
  []
138
153
  end
@@ -140,26 +155,26 @@ module AirTest
140
155
 
141
156
  def extract_priority(text)
142
157
  if text =~ /priority:\s*(.+)/i
143
- $1.strip
158
+ ::Regexp.last_match(1).strip
144
159
  else
145
- ''
160
+ ""
146
161
  end
147
162
  end
148
163
 
149
164
  def extract_estimate(text)
150
- if text =~ /estimate:\s*(\d+)/i
151
- $1.to_i
152
- else
153
- nil
154
- end
165
+ return unless text =~ /estimate:\s*(\d+)/i
166
+
167
+ ::Regexp.last_match(1).to_i
155
168
  end
156
169
 
157
170
  def extract_assignee(text)
158
171
  if text =~ /assignee:\s*(.+)/i
159
- $1.strip
172
+ ::Regexp.last_match(1).strip
160
173
  else
161
- ''
174
+ ""
162
175
  end
163
176
  end
164
177
  end
165
- end
178
+ end
179
+
180
+ # rubocop:enable Metrics/ClassLength
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Runs the main automation workflow for AirTest, orchestrating Notion parsing and GitHub actions.
4
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
3
5
  module AirTest
6
+ # Runs the main automation workflow for AirTest, orchestrating Notion parsing and GitHub actions.
4
7
  class Runner
5
8
  def initialize(config = AirTest.configuration)
6
9
  @notion = NotionParser.new(config)
@@ -16,26 +19,28 @@ module AirTest
16
19
  title = @notion.extract_ticket_title(ticket)
17
20
  url = @notion.extract_ticket_url(ticket)
18
21
  puts "\n📋 Processing: FDR#{ticket_id} - #{title}"
19
- parsed_data = @notion.parse_ticket_content(ticket['id'])
22
+ parsed_data = @notion.parse_ticket_content(ticket["id"])
20
23
  next unless parsed_data && parsed_data[:feature] && !parsed_data[:feature].empty?
21
- slug = parsed_data[:feature].downcase.gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '')
24
+
25
+ slug = parsed_data[:feature].downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
22
26
  branch = "air_test/#{ticket_id}-#{slug}"
23
27
  spec_path = @spec.generate_spec_from_parsed_data(ticket_id, parsed_data)
24
28
  step_path = @spec.generate_step_definitions_for_spec(spec_path)
25
29
  files_to_commit = [spec_path]
26
30
  files_to_commit << step_path if step_path
27
- has_changes = @github.commit_and_push_branch(branch, files_to_commit, "Add specs for Notion ticket #{ticket_id}")
31
+ has_changes = @github.commit_and_push_branch(branch, files_to_commit,
32
+ "Add specs for Notion ticket #{ticket_id}")
28
33
  if has_changes
29
34
  pr_title = title
30
- scenarios_md = parsed_data[:scenarios].map.with_index(1) do |sc, i|
31
- steps = sc[:steps]&.join(' ')
35
+ scenarios_md = parsed_data[:scenarios].map.with_index(1) do |sc, _i|
36
+ steps = sc[:steps]&.join(" ")
32
37
  " - [ ] #{sc[:title]} – #{steps}"
33
38
  end.join("\n")
34
39
  pr_body = <<~MD
35
- - **Story Notion :** #{url}
36
- - **Feature** : #{parsed_data[:feature]}
37
- - **Scénarios** :
38
- #{scenarios_md}
40
+ - **Story Notion :** #{url}
41
+ - **Feature** : #{parsed_data[:feature]}
42
+ - **Scénarios** :
43
+ #{scenarios_md}
39
44
  MD
40
45
  pr = @github.create_pull_request(branch, pr_title, pr_body)
41
46
  puts "✅ Pull Request créée : #{pr.html_url}" if pr
@@ -45,4 +50,5 @@ module AirTest
45
50
  end
46
51
  end
47
52
  end
48
- end
53
+ end
54
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
@@ -1,14 +1,18 @@
1
+ # Generates RSpec and Turnip spec files from parsed Notion ticket data for AirTest.
1
2
  # frozen_string_literal: true
2
- require 'fileutils'
3
+
4
+ require "fileutils"
3
5
 
4
6
  module AirTest
7
+ # Generates RSpec and Turnip spec files from parsed Notion ticket data for AirTest.
5
8
  class SpecGenerator
6
- def generate_spec_from_parsed_data(ticket_id, parsed_data, output_dir: 'spec/features')
7
- feature_slug = parsed_data[:feature].downcase.gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '')
9
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
10
+ def generate_spec_from_parsed_data(ticket_id, parsed_data, output_dir: "spec/features")
11
+ feature_slug = parsed_data[:feature].downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
8
12
  filename = "#{feature_slug}_fdr#{ticket_id}.rb"
9
13
  filepath = File.join(output_dir, filename)
10
14
  FileUtils.mkdir_p(output_dir)
11
- File.open(filepath, 'w') do |f|
15
+ File.open(filepath, "w") do |f|
12
16
  f.puts "# language: turnip"
13
17
  f.puts "# frozen_string_literal: true\n"
14
18
  f.puts "feature '#{parsed_data[:feature]}' do"
@@ -24,19 +28,18 @@ module AirTest
24
28
  filepath
25
29
  end
26
30
 
27
- def generate_step_definitions_for_spec(spec_filepath, output_dir: 'spec/steps')
31
+ def generate_step_definitions_for_spec(spec_filepath, output_dir: "spec/steps")
28
32
  step_texts = []
29
33
  File.readlines(spec_filepath).each do |line|
30
- if line =~ /pending ['\"](.*)['\"]/
31
- step_texts << $1.strip
32
- end
34
+ step_texts << ::Regexp.last_match(1).strip if line =~ /pending ['\"](.*)['\"]/
33
35
  end
34
36
  return if step_texts.empty?
35
- spec_filename = File.basename(spec_filepath, '.rb')
37
+
38
+ spec_filename = File.basename(spec_filepath, ".rb")
36
39
  step_file = File.join(output_dir, "#{spec_filename}_steps.rb")
37
40
  FileUtils.mkdir_p(output_dir)
38
- File.open(step_file, 'w') do |f|
39
- f.puts "# Auto-generated step definitions for #{spec_filename.gsub('-', ' ')}"
41
+ File.open(step_file, "w") do |f|
42
+ f.puts "# Auto-generated step definitions for #{spec_filename.gsub("-", " ")}"
40
43
  step_texts.uniq.each do |step|
41
44
  f.puts "\nstep '#{step}' do"
42
45
  f.puts " pending 'Implement: #{step.gsub("'", "\\'")}'"
@@ -45,5 +48,6 @@ module AirTest
45
48
  end
46
49
  step_file
47
50
  end
51
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
48
52
  end
49
- end
53
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AirTest
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/air_test.rb CHANGED
@@ -1,12 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "air_test/version"
4
- require_relative "air_test_automation/configuration"
5
- require_relative "air_test_automation/notion_parser"
6
- require_relative "air_test_automation/spec_generator"
7
- require_relative "air_test_automation/github_client"
8
- require_relative "air_test_automation/rake_tasks"
9
-
3
+ # Main namespace for the AirTest gem, which automates spec generation and GitHub integration.
10
4
  module AirTest
11
5
  class << self
12
6
  attr_accessor :configuration
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  namespace :air_test do
2
4
  desc "Generate specs and PR from Notion"
3
5
  task generate_specs_from_notion: :environment do
4
- require 'air_test/runner'
6
+ require "air_test/runner"
5
7
  AirTest::Runner.new.run(limit: 5)
6
8
  end
7
- end
9
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: air_test
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - julien bouland
@@ -10,10 +10,11 @@ cert_chain: []
10
10
  date: 2025-07-18 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: Automate the generation of Turnip/RSpec specs from Notion tickets, create
13
- branches, commits, pushes, and GitHub Pull Requests—all with a single Rake command.
13
+ branches, commits, pushes, and GitHub Pull Requests. All with a single Rake command.
14
14
  email:
15
15
  - bouland.julien@gmail.com
16
- executables: []
16
+ executables:
17
+ - air_test
17
18
  extensions: []
18
19
  extra_rdoc_files: []
19
20
  files:
@@ -24,6 +25,7 @@ files:
24
25
  - LICENSE.txt
25
26
  - README.md
26
27
  - Rakefile
28
+ - exe/air_test
27
29
  - lib/air_test.rb
28
30
  - lib/air_test/configuration.rb
29
31
  - lib/air_test/github_client.rb
@@ -40,6 +42,7 @@ metadata:
40
42
  homepage_uri: https://github.com/airtest-dev/airtest
41
43
  source_code_uri: https://github.com/airtest-dev/airtest
42
44
  changelog_uri: https://github.com/airtest-io/air_test/blob/main/CHANGELOG.md
45
+ rubygems_mfa_required: 'true'
43
46
  rdoc_options: []
44
47
  require_paths:
45
48
  - lib