air_test 0.1.5.7 → 0.1.6.1

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,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+ require_relative "ticket_parser"
7
+
8
+ module AirTest
9
+ class MondayTicketParser
10
+ include TicketParser
11
+ def initialize(config = AirTest.configuration)
12
+ @api_token = config.monday[:token]
13
+ @board_id = config.monday[:board_id]
14
+ @domain = config.monday[:domain]
15
+ @base_url = "https://api.monday.com/v2"
16
+ end
17
+
18
+ def fetch_tickets(limit: 5)
19
+ # First, get all items from the board
20
+ query = <<~GRAPHQL
21
+ query {
22
+ boards(ids: "#{@board_id}") {
23
+ items_page {
24
+ items {
25
+ id
26
+ name
27
+ column_values {
28
+ id
29
+ text
30
+ value
31
+ }
32
+ }
33
+ }
34
+ }
35
+ }
36
+ GRAPHQL
37
+
38
+ response = make_graphql_request(query)
39
+ return [] unless response["data"]
40
+
41
+ items = response.dig("data", "boards", 0, "items_page", "items") || []
42
+
43
+ # Filter for items with "Not Started" status
44
+ not_started_items = items.select do |item|
45
+ status_column = item["column_values"].find { |cv| cv["id"] == "project_status" }
46
+ status_column && status_column["text"] == "Not Started"
47
+ end
48
+
49
+ not_started_items.first(limit)
50
+ end
51
+
52
+ def parse_ticket_content(item_id)
53
+ # For Monday, we'll use the item name as feature and create a simple scenario
54
+ # In the future, you could add a description column to Monday and parse it like Notion
55
+ {
56
+ feature: "Feature: #{extract_ticket_title({ "id" => item_id, "name" => "Loading..." })}",
57
+ scenarios: [
58
+ {
59
+ title: "Scenario",
60
+ steps: ["Implement the feature"]
61
+ }
62
+ ],
63
+ meta: { tags: [], priority: "", estimate: nil, assignee: "" }
64
+ }
65
+ end
66
+
67
+ def extract_ticket_title(ticket)
68
+ ticket["name"] || "No title"
69
+ end
70
+
71
+ def extract_ticket_id(ticket)
72
+ ticket["id"] || "No ID"
73
+ end
74
+
75
+ def extract_ticket_url(ticket)
76
+ "https://#{@domain}/boards/#{@board_id}/pulses/#{ticket["id"]}"
77
+ end
78
+
79
+ private
80
+
81
+ def make_graphql_request(query)
82
+ uri = URI(@base_url)
83
+ request = Net::HTTP::Post.new(uri)
84
+ request["Authorization"] = @api_token
85
+ request["Content-Type"] = "application/json"
86
+ request.body = { query: query }.to_json
87
+
88
+ http = Net::HTTP.new(uri.host, uri.port)
89
+ http.use_ssl = true
90
+ response = http.request(request)
91
+
92
+ return {} unless response.code == "200"
93
+
94
+ JSON.parse(response.body)
95
+ rescue JSON::ParserError
96
+ {}
97
+ end
98
+ end
99
+ end
@@ -1,23 +1,33 @@
1
- # Parses Notion tickets and extracts relevant information for spec generation in AirTest.
2
- # rubocop:disable Metrics/ClassLength
3
1
  # frozen_string_literal: true
4
2
 
5
3
  require "net/http"
6
4
  require "json"
7
5
  require "uri"
6
+ require_relative "ticket_parser"
8
7
 
9
8
  module AirTest
10
- # Parses Notion tickets and extracts relevant information for spec generation in AirTest.
11
- class NotionParser
9
+ # Implements TicketParser for Notion integration.
10
+ class NotionTicketParser
11
+ include TicketParser
12
12
  def initialize(config = AirTest.configuration)
13
- @database_id = config.notion_database_id
14
- @notion_token = config.notion_token
13
+ @database_id = config.notion[:database_id]
14
+ @notion_token = config.notion[:token]
15
15
  @base_url = "https://api.notion.com/v1"
16
16
  end
17
17
 
18
18
  def fetch_tickets(limit: 5)
19
19
  uri = URI("#{@base_url}/databases/#{@database_id}/query")
20
- response = make_api_request(uri, { page_size: 100 })
20
+ # Add filter for 'Not started' status
21
+ request_body = {
22
+ page_size: 100,
23
+ filter: {
24
+ property: "Status",
25
+ select: {
26
+ equals: "Not started"
27
+ }
28
+ }
29
+ }
30
+ response = make_api_request(uri, request_body)
21
31
  return [] unless response.code == "200"
22
32
 
23
33
  data = JSON.parse(response.body)
@@ -29,13 +39,13 @@ module AirTest
29
39
  # puts "\n===== RAW NOTION BLOCKS ====="
30
40
  # puts JSON.pretty_generate(blocks)
31
41
  return nil unless blocks
42
+
32
43
  normalized_blocks = normalize_blocks(blocks)
33
44
  # puts "\n===== NORMALIZED BLOCKS ====="
34
45
  # puts JSON.pretty_generate(normalized_blocks)
35
- parsed_data = parse_content(normalized_blocks)
46
+ parse_content(normalized_blocks)
36
47
  # puts "\n===== PARSED DATA ====="
37
48
  # puts JSON.pretty_generate(parsed_data)
38
- parsed_data
39
49
  end
40
50
 
41
51
  def extract_ticket_title(ticket)
@@ -93,7 +103,11 @@ module AirTest
93
103
 
94
104
  blocks.each do |block|
95
105
  block_type = block["type"]
96
- text = extract_text(block[block_type]["rich_text"]) rescue ""
106
+ text = begin
107
+ extract_text(block[block_type]["rich_text"])
108
+ rescue StandardError
109
+ ""
110
+ end
97
111
 
98
112
  if %w[heading_1 heading_2 heading_3].include?(block_type)
99
113
  heading_text = text.strip
@@ -127,19 +141,19 @@ module AirTest
127
141
  parsed_data[:scenarios] << current_scenario
128
142
  elsif %w[paragraph bulleted_list_item numbered_list_item].include?(block_type)
129
143
  next if text.empty?
144
+
130
145
  if in_feature_block
131
146
  parsed_data[:feature] += "\n#{text}"
132
147
  elsif in_background_block
133
148
  background_steps << text
134
149
  elsif in_scenario_block && current_scenario
135
150
  # Only add as step if not a scenario heading
136
- unless text.strip.downcase.start_with?("scenario:")
137
- current_scenario[:steps] << text
138
- end
151
+ current_scenario[:steps] << text unless text.strip.downcase.start_with?("scenario:")
139
152
  end
140
153
  elsif block_type == "callout"
141
154
  text = extract_text(block["callout"]["rich_text"])
142
155
  next if text.empty?
156
+
143
157
  if text.downcase.include?("tag")
144
158
  tags = extract_tags(text)
145
159
  parsed_data[:meta][:tags].concat(tags)
@@ -166,23 +180,19 @@ module AirTest
166
180
  in_steps = false
167
181
  blocks.each do |block|
168
182
  block_type = block["type"]
169
- text = extract_text(block[block_type]["rich_text"] || block["paragraph"]["rich_text"]) rescue ""
183
+ text = begin
184
+ extract_text(block[block_type]["rich_text"] || block["paragraph"]["rich_text"])
185
+ rescue StandardError
186
+ ""
187
+ end
170
188
  if %w[heading_1 heading_2 heading_3].include?(block_type)
171
189
  heading_text = text.strip
172
- if heading_text.downcase.include?("feature")
173
- in_steps = true
174
- elsif heading_text.downcase.include?("scenario")
175
- in_steps = true
176
- else
177
- in_steps = false
178
- end
190
+ in_steps = heading_text.downcase.include?("feature") || heading_text.downcase.include?("scenario")
179
191
  elsif %w[paragraph bulleted_list_item numbered_list_item].include?(block_type)
180
192
  steps << text if in_steps && !text.empty? && !text.strip.downcase.start_with?("scenario:")
181
193
  end
182
194
  end
183
- if steps.any?
184
- parsed_data[:scenarios] << { title: "Scenario", steps: steps }
185
- end
195
+ parsed_data[:scenarios] << { title: "Scenario", steps: steps } if steps.any?
186
196
  end
187
197
 
188
198
  parsed_data[:feature] = parsed_data[:feature].strip
@@ -234,10 +244,10 @@ module AirTest
234
244
  blocks.each do |block|
235
245
  block_type = block["type"]
236
246
  text = if block[block_type] && block[block_type]["rich_text"]
237
- block[block_type]["rich_text"].map { |rt| rt["plain_text"] }.join("")
238
- else
239
- ""
240
- end
247
+ block[block_type]["rich_text"].map { |rt| rt["plain_text"] }.join
248
+ else
249
+ ""
250
+ end
241
251
  lines = text.split("\n").map(&:strip).reject(&:empty?)
242
252
  if lines.size > 1
243
253
  lines.each do |line|
@@ -254,5 +264,3 @@ module AirTest
254
264
  end
255
265
  end
256
266
  end
257
-
258
- # rubocop:enable Metrics/ClassLength
@@ -6,20 +6,32 @@ module AirTest
6
6
  # Runs the main automation workflow for AirTest, orchestrating Notion parsing and GitHub actions.
7
7
  class Runner
8
8
  def initialize(config = AirTest.configuration)
9
- @notion = NotionParser.new(config)
9
+ @config = config
10
+ @parser = case config.tool.to_s.downcase
11
+ when "notion"
12
+ NotionTicketParser.new(config)
13
+ when "jira"
14
+ JiraTicketParser.new(config)
15
+ when "monday"
16
+ MondayTicketParser.new(config)
17
+ else
18
+ raise "Unknown tool: #{config.tool}"
19
+ end
10
20
  @spec = SpecGenerator.new
11
21
  @github = GithubClient.new(config)
12
22
  end
13
23
 
14
24
  def run(limit: 5)
15
- tickets = @notion.fetch_tickets(limit: limit)
25
+ @config.validate!
26
+ tickets = @parser.fetch_tickets(limit: limit)
27
+ # Filter for 'Not started' tickets (assuming each parser returns only those, or filter here if needed)
16
28
  puts "🔍 Found #{tickets.length} tickets"
17
29
  tickets.each do |ticket|
18
- ticket_id = @notion.extract_ticket_id(ticket)
19
- title = @notion.extract_ticket_title(ticket)
20
- url = @notion.extract_ticket_url(ticket)
30
+ ticket_id = @parser.extract_ticket_id(ticket)
31
+ title = @parser.extract_ticket_title(ticket)
32
+ url = @parser.extract_ticket_url(ticket)
21
33
  puts "\n📋 Processing: FDR#{ticket_id} - #{title}"
22
- parsed_data = @notion.parse_ticket_content(ticket["id"])
34
+ parsed_data = @parser.parse_ticket_content(ticket["id"])
23
35
  unless parsed_data && parsed_data[:feature] && !parsed_data[:feature].empty?
24
36
  puts "⚠️ Skipping ticket FDR#{ticket_id} due to missing or empty feature."
25
37
  next
@@ -43,6 +55,9 @@ module AirTest
43
55
  - **Feature** : #{parsed_data[:feature]}
44
56
  - **Scénarios** :
45
57
  #{scenarios_md}
58
+ - **Want to help us improve airtest?**
59
+ Leave feedback [here](http://bit.ly/4o5rinU)
60
+ or [join the community](https://discord.gg/ggnBvhtw7E)
46
61
  MD
47
62
  pr = @github.create_pull_request(branch, pr_title, pr_body)
48
63
  puts "✅ Pull Request créée : #{pr.html_url}" if pr
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AirTest
4
+ # Interface for ticket parsers (Notion, Jira, Monday, etc.)
5
+ module TicketParser
6
+ def fetch_tickets(limit: 5)
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def parse_ticket_content(page_id)
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def extract_ticket_title(ticket)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def extract_ticket_id(ticket)
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def extract_ticket_url(ticket)
23
+ raise NotImplementedError
24
+ end
25
+ end
26
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AirTest
4
- VERSION = "0.1.5.7"
4
+ VERSION = "0.1.6.1"
5
5
  end
data/lib/air_test.rb CHANGED
@@ -14,7 +14,10 @@ end
14
14
 
15
15
  require_relative "air_test/version"
16
16
  require_relative "air_test/configuration"
17
- require_relative "air_test/notion_parser"
17
+ require_relative "air_test/ticket_parser"
18
+ require_relative "air_test/notion_ticket_parser"
19
+ require_relative "air_test/jira_ticket_parser"
20
+ require_relative "air_test/monday_ticket_parser"
18
21
  require_relative "air_test/spec_generator"
19
22
  require_relative "air_test/github_client"
20
23
  require_relative "air_test/runner"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: air_test
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5.7
4
+ version: 0.1.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - julien bouland
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-07-23 00:00:00.000000000 Z
10
+ date: 2025-07-29 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '7.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: tty-prompt
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.23'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.23'
54
68
  description: Automate the generation of Turnip/RSpec specs from Notion tickets, create
55
69
  branches, commits, pushes, and GitHub Pull Requests. All with a single Rake command.
56
70
  email:
@@ -69,13 +83,17 @@ files:
69
83
  - Rakefile
70
84
  - exe/air_test
71
85
  - lib/air_test.rb
86
+ - lib/air_test/cli.rb
72
87
  - lib/air_test/configuration.rb
73
88
  - lib/air_test/engine.rb
74
89
  - lib/air_test/github_client.rb
75
- - lib/air_test/notion_parser.rb
90
+ - lib/air_test/jira_ticket_parser.rb
91
+ - lib/air_test/monday_ticket_parser.rb
92
+ - lib/air_test/notion_ticket_parser.rb
76
93
  - lib/air_test/runner.rb
77
94
  - lib/air_test/spec_generator.rb
78
95
  - lib/air_test/tasks/air_test.rake
96
+ - lib/air_test/ticket_parser.rb
79
97
  - lib/air_test/version.rb
80
98
  - sig/air_test.rbs
81
99
  homepage: https://github.com/airtest-dev/airtest