kaloko 0.5.0 → 0.5.2

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.
@@ -227,6 +227,88 @@ export async function applySuggestions (file, report) {
227
227
  return { applied, skipped }
228
228
  }
229
229
 
230
+ const sameEntry = (a, b) => JSON.stringify({ rect: a.rect, viewport: a.viewport, selector: a.selector }) === JSON.stringify({ rect: b.rect, viewport: b.viewport, selector: b.selector })
231
+
232
+ /**
233
+ * Adds ignore entries to steps of the scenario file, appending to an existing block-style `ignore:` list or creating
234
+ * one (comments and formatting stay; flow-style lists are left alone). `byNode`: Map node → entries.
235
+ * Returns { applied: [{ node, entries }], skipped: [reason] }; entries already in the file are not added twice.
236
+ */
237
+ export async function mergeIgnore (file, byNode) {
238
+ let text = await readFile(file, 'utf8')
239
+ const applied = []; const skipped = []
240
+ for (const [nodeId, entries] of byNode) {
241
+ const parsed = yaml.load(text)
242
+ const node = (parsed?.nodes || []).find((n) => n.id === nodeId)
243
+ if (!node) { skipped.push(`${nodeId}: not in ${file}`); continue }
244
+ const fresh = entries.filter((e) => !(Array.isArray(node.ignore) ? node.ignore : []).some((x) => sameEntry(x, e)))
245
+ if (!fresh.length) { applied.push({ node: nodeId, entries: [] }); continue }
246
+ const lines = text.split('\n')
247
+ const at = lines.findIndex((l) => new RegExp(`^(\\s*)- id: ['"]?${nodeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]?\\s*(#.*)?$`).test(l))
248
+ if (at < 0) { skipped.push(`${nodeId}: node is not in block style; add the regions by hand`); continue }
249
+ const dash = lines[at].match(/^(\s*)-/)[1].length
250
+ const indent = ' '.repeat(dash + 2)
251
+ // the node's block: lines indented deeper than its dash (blank lines included)
252
+ let end = at + 1
253
+ while (end < lines.length && (!lines[end].trim() || lines[end].match(/^(\s*)/)[1].length > dash)) end++
254
+ while (end > at + 1 && !lines[end - 1].trim()) end--
255
+ const key = lines.slice(at + 1, end).findIndex((l) => l.startsWith(`${indent}ignore:`))
256
+ const item = (e, pad) => `${pad}- ${yaml.dump(e, { flowLevel: 0, lineWidth: 400 }).trim()}`
257
+ let block
258
+ if (key < 0) {
259
+ block = { at: end, lines: [`${indent}ignore:`, ...fresh.map((e) => item(e, indent + ' '))] }
260
+ } else {
261
+ const k = at + 1 + key
262
+ if (lines[k].replace(/#.*$/, '').trim() !== 'ignore:') { skipped.push(`${nodeId}: ignore is written inline; add the regions by hand`); continue }
263
+ // list items follow at the key's indentation or deeper; the list ends at the next line that is neither
264
+ let j = k + 1; let pad = null
265
+ while (j < end) {
266
+ const l = lines[j]
267
+ if (!l.trim()) { j++; continue }
268
+ const ind = l.match(/^(\s*)/)[1].length
269
+ if (pad == null) { if (!l.trimStart().startsWith('- ') || ind < indent.length) break; pad = ' '.repeat(ind) }
270
+ if (ind < pad.length || (ind === pad.length && !l.trimStart().startsWith('- '))) break
271
+ j++
272
+ }
273
+ while (j > k + 1 && !lines[j - 1].trim()) j--
274
+ block = { at: j, lines: fresh.map((e) => item(e, pad ?? indent + ' ')) }
275
+ }
276
+ const next = [...lines.slice(0, block.at), ...block.lines, ...lines.slice(block.at)].join('\n')
277
+ let ok = false
278
+ try { const n2 = (yaml.load(next)?.nodes || []).find((n) => n.id === nodeId); ok = Array.isArray(n2?.ignore) && n2.ignore.length === (Array.isArray(node.ignore) ? node.ignore.length : 0) + fresh.length } catch {}
279
+ if (!ok) { skipped.push(`${nodeId}: could not add to its ignore list safely; add the regions by hand`); continue }
280
+ text = next
281
+ applied.push({ node: nodeId, entries: fresh })
282
+ }
283
+ if (applied.some((a) => a.entries.length)) await writeFile(file, text)
284
+ return { applied, skipped }
285
+ }
286
+
287
+ /**
288
+ * `kaloko stability --pull`: regions people drew on the canvas (Developer / Tester and up) become ignore: entries of
289
+ * their steps in the scenario file; the service then stops showing them as suggestions.
290
+ */
291
+ export async function pullDrawn ({ root, config, run, args, say, client }) {
292
+ const scenarioId = typeof args.scenario === 'string' ? args.scenario : run.meta.scenario.id
293
+ const data = await client.request('GET', `/orgs/${config.org}/scenarios/${encodeURIComponent(scenarioId)}/ignore-regions`)
294
+ const regions = data.regions || []
295
+ if (!regions.length) { say(`No drawn regions wait for ${scenarioId}. Draw them on a screenshot on kaloko.app (step detail → Page → Draw a region).`); return { applied: [], skipped: [], regions: [] } }
296
+ const byNode = new Map()
297
+ for (const r of regions) (byNode.get(r.node_id) || byNode.set(r.node_id, []).get(r.node_id)).push(r.scenario_entry)
298
+ const file = resolve(root, run.meta.scenario_file)
299
+ const out = await mergeIgnore(file, byNode)
300
+ const done = new Set(out.applied.map((a) => a.node))
301
+ const ids = regions.filter((r) => done.has(r.node_id)).map((r) => r.id)
302
+ if (ids.length) await client.request('POST', `/orgs/${config.org}/scenarios/${encodeURIComponent(scenarioId)}/ignore-regions/applied`, { ids })
303
+ if (args.json) { say(JSON.stringify({ ...out, regions }, null, 2)); return out }
304
+ const added = out.applied.reduce((n, a) => n + a.entries.length, 0)
305
+ say(`Pulled ${regions.length} drawn region(s) of ${scenarioId}: ${added} added to ${run.meta.scenario_file}${added < ids.length ? `, ${ids.length - added} already there` : ''}.`)
306
+ for (const a of out.applied.filter((x) => x.entries.length)) say(` ${a.node}: ${a.entries.map((e) => `[${e.rect.join(', ')}] on ${e.viewport}`).join('; ')}`)
307
+ for (const x of out.skipped) say(` skipped ${x}`)
308
+ if (added) say('Walk those steps again so the canvas and pixel diffs use them.')
309
+ return out
310
+ }
311
+
230
312
  /** `kaloko stability`: detect, write stability.json and stability.yml in the run, print, optionally apply. */
231
313
  export async function stabilityCmd ({ root, config, run, scenario, args, say }) {
232
314
  const report = await detectUnstable({ root, config, run, scenario, last: Number(args.last) || 4, runs: typeof args.runs === 'string' ? args.runs.split(',').map((s) => s.trim()).filter(Boolean) : null, sameCommit: !!args['same-commit'] })