assuremind 1.2.0 → 1.2.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.
package/dist/cli/index.js CHANGED
@@ -353,8 +353,8 @@ async function runInit(options = {}) {
353
353
  printSuccess(` ${jsonFile}`);
354
354
  }
355
355
  if (!await import_fs_extra.default.pathExists("autotest.config.json")) {
356
- const { DEFAULT_CONFIG: DEFAULT_CONFIG2 } = await Promise.resolve().then(() => (init_config(), config_exports));
357
- await import_fs_extra.default.writeJson("autotest.config.json", DEFAULT_CONFIG2, { spaces: 2 });
356
+ const { DEFAULT_CONFIG: DEFAULT_CONFIG3 } = await Promise.resolve().then(() => (init_config(), config_exports));
357
+ await import_fs_extra.default.writeJson("autotest.config.json", DEFAULT_CONFIG3, { spaces: 2 });
358
358
  printSuccess(" autotest.config.json");
359
359
  } else {
360
360
  printSkipped(" autotest.config.json (already exists)");
@@ -468,10 +468,10 @@ var init_errors = __esm({
468
468
  };
469
469
  StorageError = class extends AssuremindError {
470
470
  path;
471
- constructor(message, path37, code = "STORAGE_ERROR") {
471
+ constructor(message, path41, code = "STORAGE_ERROR") {
472
472
  super(message, code);
473
473
  this.name = "StorageError";
474
- this.path = path37;
474
+ this.path = path41;
475
475
  }
476
476
  };
477
477
  }
@@ -1162,8 +1162,10 @@ var init_suite = __esm({
1162
1162
  mockStatus: import_zod3.z.number().int().optional(),
1163
1163
  runAudit: import_zod3.z.boolean().optional(),
1164
1164
  // Mark this step as a Lighthouse audit checkpoint
1165
- soft: import_zod3.z.boolean().optional()
1165
+ soft: import_zod3.z.boolean().optional(),
1166
1166
  // Use expect.soft() — test continues on assertion failure
1167
+ visualCheck: import_zod3.z.boolean().optional()
1168
+ // Enable visual regression comparison for this step
1167
1169
  });
1168
1170
  DataSourceSchema = import_zod3.z.object({
1169
1171
  type: import_zod3.z.enum(["inline", "json-file", "csv-file"]),
@@ -3651,7 +3653,86 @@ When the instruction starts with "Soft verify" or "Soft assert" or "Soft check",
3651
3653
  await expect.soft(page.getByText('Welcome')).toBeVisible()
3652
3654
  await expect.soft(page.getByLabel('Email')).toHaveValue('user@example.com')
3653
3655
 
3654
- For "verify user is on X page" \u2192 use toHaveURL() or check for a heading/element specific to that page.`;
3656
+ For "verify user is on X page" \u2192 use toHaveURL() or check for a heading/element specific to that page.
3657
+
3658
+ \u2501\u2501\u2501 JAVASCRIPT ALERTS / DIALOGS \u2501\u2501\u2501
3659
+ When the step mentions "accept alert", "dismiss alert", "handle popup", "click OK on dialog", "confirm dialog", or "enter text in prompt":
3660
+ // Accept (OK) an alert or confirm dialog
3661
+ page.once('dialog', dialog => dialog.accept());
3662
+ await page.getByRole('button', { name: 'Trigger' }).click();
3663
+
3664
+ // Dismiss (Cancel) a confirm dialog
3665
+ page.once('dialog', dialog => dialog.dismiss());
3666
+ await page.getByRole('button', { name: 'Delete' }).click();
3667
+
3668
+ // Accept a prompt and provide text
3669
+ page.once('dialog', dialog => dialog.accept('my input text'));
3670
+ await page.getByRole('button', { name: 'Ask' }).click();
3671
+
3672
+ // Assert dialog message
3673
+ page.once('dialog', async dialog => {
3674
+ expect(dialog.message()).toContain('Are you sure?');
3675
+ await dialog.accept();
3676
+ });
3677
+
3678
+ IMPORTANT: Register the dialog handler with page.once() BEFORE the action that triggers it.
3679
+
3680
+ \u2501\u2501\u2501 SHADOW DOM \u2501\u2501\u2501
3681
+ For elements inside a Shadow Root, use Playwright's CSS pierce combinator (>>):
3682
+ // Click a button inside shadow root
3683
+ await page.locator('my-component >> button').click();
3684
+ await page.locator('my-component >> [data-testid="submit"]').click();
3685
+
3686
+ // Fill an input inside shadow root
3687
+ await page.locator('custom-input >> input').fill('value');
3688
+
3689
+ // Deep shadow DOM (multiple levels)
3690
+ await page.locator('outer-element >> inner-element >> button').click();
3691
+
3692
+ // Using :host selector
3693
+ await page.locator(':shadow(my-component) >> input').fill('text');
3694
+
3695
+ When elements are inside a shadow root, use locator() with >> combinator instead of getBy methods.
3696
+
3697
+ \u2501\u2501\u2501 IFRAMES \u2501\u2501\u2501
3698
+ For elements inside an <iframe>, use frameLocator() to enter the frame context:
3699
+ // Click inside iframe
3700
+ await page.frameLocator('iframe[name="myFrame"]').getByRole('button', { name: 'Submit' }).click();
3701
+ await page.frameLocator('#iframe-id').getByLabel('Username').fill('admin');
3702
+ await page.frameLocator('iframe[src*="payment"]').getByPlaceholder('Card number').fill('4111111111111111');
3703
+
3704
+ // Multiple iframes \u2014 chain frameLocator
3705
+ await page.frameLocator('iframe.outer').frameLocator('iframe.inner').getByRole('button').click();
3706
+
3707
+ Use frameLocator() for ALL interactions inside iframes \u2014 both click AND fill operations work.
3708
+
3709
+ \u2501\u2501\u2501 KEYBOARD ACTIONS \u2501\u2501\u2501
3710
+ For keyboard-specific steps (press key, keyboard shortcut, type text):
3711
+ // Press a single key
3712
+ await page.keyboard.press('Enter');
3713
+ await page.keyboard.press('Escape');
3714
+ await page.keyboard.press('Tab');
3715
+ await page.keyboard.press('ArrowDown');
3716
+ await page.keyboard.press('Space');
3717
+ await page.keyboard.press('Backspace');
3718
+ await page.keyboard.press('Delete');
3719
+
3720
+ // Keyboard shortcuts (modifier + key)
3721
+ await page.keyboard.press('Control+A'); // Select all
3722
+ await page.keyboard.press('Control+C'); // Copy
3723
+ await page.keyboard.press('Control+V'); // Paste
3724
+ await page.keyboard.press('Control+Z'); // Undo
3725
+ await page.keyboard.press('Shift+Tab'); // Shift+Tab (go back)
3726
+ await page.keyboard.press('Alt+F4');
3727
+
3728
+ // Type text character by character (simulates real keystrokes)
3729
+ await page.keyboard.type('Hello World');
3730
+
3731
+ // Press key on a specific element (focus first)
3732
+ await page.getByLabel('Search').press('Enter');
3733
+ await page.getByRole('textbox').press('Control+A');
3734
+
3735
+ Use page.keyboard.press() for single keys or shortcuts, page.keyboard.type() when the step says "type using keyboard".`;
3655
3736
  }
3656
3737
  });
3657
3738
 
@@ -5598,6 +5679,7 @@ async function stepRoutes(fastify) {
5598
5679
  }
5599
5680
  }
5600
5681
  }
5682
+ if (parsed.data.visualCheck !== void 0) patch.visualCheck = parsed.data.visualCheck;
5601
5683
  return { ...s, ...patch };
5602
5684
  });
5603
5685
  const updated = await updateCase(caseDir, { steps: updatedSteps });
@@ -5643,7 +5725,8 @@ var init_steps = __esm({
5643
5725
  auditUrl: import_zod8.z.string().optional(),
5644
5726
  lighthouseCategories: import_zod8.z.array(import_zod8.z.enum(["performance", "accessibility", "seo"])).optional(),
5645
5727
  runAudit: import_zod8.z.boolean().optional(),
5646
- soft: import_zod8.z.boolean().optional()
5728
+ soft: import_zod8.z.boolean().optional(),
5729
+ visualCheck: import_zod8.z.boolean().optional()
5647
5730
  });
5648
5731
  }
5649
5732
  });
@@ -6354,8 +6437,8 @@ async function healingRoutes(fastify) {
6354
6437
  return reply.status(400).send({ error: "Invalid request", details: parsed.error.issues });
6355
6438
  }
6356
6439
  try {
6357
- const { default: fs30 } = await import("fs-extra");
6358
- const report = await fs30.readJson(parsed.data.reportPath);
6440
+ const { default: fs34 } = await import("fs-extra");
6441
+ const report = await fs34.readJson(parsed.data.reportPath);
6359
6442
  if (!Array.isArray(report.events)) {
6360
6443
  return reply.status(400).send({ error: "Invalid healing report format" });
6361
6444
  }
@@ -6390,7 +6473,7 @@ async function applyHealToCase(testsDir, caseId, stepId, healedCode) {
6390
6473
  await updateCase(casePath, { steps: updatedSteps });
6391
6474
  }
6392
6475
  async function findCasePath(testsDir, caseId) {
6393
- const { default: fs30 } = await import("fs-extra");
6476
+ const { default: fs34 } = await import("fs-extra");
6394
6477
  const { listSuiteDirs: listSuiteDirs2 } = await Promise.resolve().then(() => (init_suite_store(), suite_store_exports));
6395
6478
  const suiteDirs = await listSuiteDirs2(testsDir);
6396
6479
  for (const suiteDir of suiteDirs) {
@@ -6971,8 +7054,8 @@ function normalizeUrl2(url) {
6971
7054
  if (!url) return "";
6972
7055
  try {
6973
7056
  const u = new URL(url);
6974
- const path37 = u.pathname.replace(/\/\d+/g, "/*");
6975
- return `${u.hostname}${path37}`;
7057
+ const path41 = u.pathname.replace(/\/\d+/g, "/*");
7058
+ return `${u.hostname}${path41}`;
6976
7059
  } catch {
6977
7060
  return url;
6978
7061
  }
@@ -8279,139 +8362,256 @@ var init_allure_generator = __esm({
8279
8362
  }
8280
8363
  });
8281
8364
 
8282
- // src/utils/faker.ts
8283
- function pick(arr) {
8284
- return arr[Math.floor(Math.random() * arr.length)];
8285
- }
8286
- function fakeFirstName() {
8287
- return pick(FIRST_NAMES);
8288
- }
8289
- function fakeLastName() {
8290
- return pick(LAST_NAMES);
8291
- }
8292
- function fakeAge() {
8293
- return String(Math.floor(Math.random() * 63) + 18);
8294
- }
8295
- function fakeEmail() {
8296
- const first = fakeFirstName().toLowerCase();
8297
- const last = fakeLastName().toLowerCase();
8298
- const rand = Math.floor(Math.random() * 900) + 100;
8299
- return `${first}.${last}${rand}@${pick(EMAIL_DOMAINS)}`;
8300
- }
8301
- function fakePhone() {
8302
- const area = Math.floor(Math.random() * 800) + 200;
8303
- const prefix = Math.floor(Math.random() * 800) + 200;
8304
- const line = Math.floor(Math.random() * 9e3) + 1e3;
8305
- return `${area}-${prefix}-${line}`;
8306
- }
8307
- function fakePassword() {
8308
- const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
8309
- const lower = "abcdefghjkmnpqrstuvwxyz";
8310
- const digits = "23456789";
8311
- const special = "!@#$%^&*";
8312
- const all = upper + lower + digits + special;
8313
- const len = Math.floor(Math.random() * 5) + 10;
8314
- let pwd = pick([...upper]) + pick([...lower]) + pick([...digits]) + pick([...special]);
8315
- for (let i = pwd.length; i < len; i++) pwd += pick([...all]);
8316
- return pwd.split("").sort(() => Math.random() - 0.5).join("");
8317
- }
8318
- function fakeNumber(digits) {
8319
- const d = Math.max(1, digits);
8320
- const min = d === 1 ? 0 : Math.pow(10, d - 1);
8321
- const max = Math.pow(10, d) - 1;
8322
- return String(Math.floor(Math.random() * (max - min + 1)) + min);
8365
+ // src/engine/data-generator.ts
8366
+ function getAvailableGenerators() {
8367
+ return [
8368
+ {
8369
+ category: "person",
8370
+ methods: [
8371
+ { name: "fullName", example: import_faker.faker.person.fullName() },
8372
+ { name: "firstName", example: import_faker.faker.person.firstName() },
8373
+ { name: "lastName", example: import_faker.faker.person.lastName() },
8374
+ { name: "middleName", example: import_faker.faker.person.middleName() },
8375
+ { name: "sex", example: import_faker.faker.person.sex() },
8376
+ { name: "prefix", example: import_faker.faker.person.prefix() },
8377
+ { name: "suffix", example: import_faker.faker.person.suffix() },
8378
+ { name: "jobTitle", example: import_faker.faker.person.jobTitle() },
8379
+ { name: "jobArea", example: import_faker.faker.person.jobArea() },
8380
+ { name: "jobType", example: import_faker.faker.person.jobType() },
8381
+ { name: "bio", example: import_faker.faker.person.bio() }
8382
+ ]
8383
+ },
8384
+ {
8385
+ category: "internet",
8386
+ methods: [
8387
+ { name: "email", example: import_faker.faker.internet.email() },
8388
+ { name: "userName", example: import_faker.faker.internet.username() },
8389
+ { name: "password", example: import_faker.faker.internet.password() },
8390
+ { name: "url", example: import_faker.faker.internet.url() },
8391
+ { name: "domainName", example: import_faker.faker.internet.domainName() },
8392
+ { name: "domainWord", example: import_faker.faker.internet.domainWord() },
8393
+ { name: "ip", example: import_faker.faker.internet.ip() },
8394
+ { name: "ipv6", example: import_faker.faker.internet.ipv6() },
8395
+ { name: "mac", example: import_faker.faker.internet.mac() },
8396
+ { name: "userAgent", example: import_faker.faker.internet.userAgent() },
8397
+ { name: "emoji", example: import_faker.faker.internet.emoji() }
8398
+ ]
8399
+ },
8400
+ {
8401
+ category: "phone",
8402
+ methods: [
8403
+ { name: "number", example: import_faker.faker.phone.number() },
8404
+ { name: "imei", example: import_faker.faker.phone.imei() }
8405
+ ]
8406
+ },
8407
+ {
8408
+ category: "location",
8409
+ methods: [
8410
+ { name: "streetAddress", example: import_faker.faker.location.streetAddress() },
8411
+ { name: "street", example: import_faker.faker.location.street() },
8412
+ { name: "city", example: import_faker.faker.location.city() },
8413
+ { name: "state", example: import_faker.faker.location.state() },
8414
+ { name: "zipCode", example: import_faker.faker.location.zipCode() },
8415
+ { name: "country", example: import_faker.faker.location.country() },
8416
+ { name: "countryCode", example: import_faker.faker.location.countryCode() },
8417
+ { name: "county", example: import_faker.faker.location.county() },
8418
+ { name: "latitude", example: String(import_faker.faker.location.latitude()) },
8419
+ { name: "longitude", example: String(import_faker.faker.location.longitude()) },
8420
+ { name: "timeZone", example: import_faker.faker.location.timeZone() },
8421
+ { name: "buildingNumber", example: import_faker.faker.location.buildingNumber() }
8422
+ ]
8423
+ },
8424
+ {
8425
+ category: "company",
8426
+ methods: [
8427
+ { name: "name", example: import_faker.faker.company.name() },
8428
+ { name: "catchPhrase", example: import_faker.faker.company.catchPhrase() },
8429
+ { name: "buzzPhrase", example: import_faker.faker.company.buzzPhrase() },
8430
+ { name: "buzzNoun", example: import_faker.faker.company.buzzNoun() },
8431
+ { name: "buzzVerb", example: import_faker.faker.company.buzzVerb() },
8432
+ { name: "buzzAdjective", example: import_faker.faker.company.buzzAdjective() }
8433
+ ]
8434
+ },
8435
+ {
8436
+ category: "finance",
8437
+ methods: [
8438
+ { name: "accountNumber", example: import_faker.faker.finance.accountNumber() },
8439
+ { name: "accountName", example: import_faker.faker.finance.accountName() },
8440
+ { name: "creditCardNumber", example: import_faker.faker.finance.creditCardNumber() },
8441
+ { name: "creditCardCVV", example: import_faker.faker.finance.creditCardCVV() },
8442
+ { name: "currencyCode", example: import_faker.faker.finance.currencyCode() },
8443
+ { name: "currencyName", example: import_faker.faker.finance.currencyName() },
8444
+ { name: "amount", example: import_faker.faker.finance.amount() },
8445
+ { name: "iban", example: import_faker.faker.finance.iban() },
8446
+ { name: "bic", example: import_faker.faker.finance.bic() },
8447
+ { name: "bitcoinAddress", example: import_faker.faker.finance.bitcoinAddress() },
8448
+ { name: "transactionType", example: import_faker.faker.finance.transactionType() }
8449
+ ]
8450
+ },
8451
+ {
8452
+ category: "date",
8453
+ methods: [
8454
+ { name: "past", example: import_faker.faker.date.past().toISOString() },
8455
+ { name: "future", example: import_faker.faker.date.future().toISOString() },
8456
+ { name: "recent", example: import_faker.faker.date.recent().toISOString() },
8457
+ { name: "soon", example: import_faker.faker.date.soon().toISOString() },
8458
+ { name: "birthdate", example: import_faker.faker.date.birthdate().toISOString() },
8459
+ { name: "month", example: import_faker.faker.date.month() },
8460
+ { name: "weekday", example: import_faker.faker.date.weekday() }
8461
+ ]
8462
+ },
8463
+ {
8464
+ category: "lorem",
8465
+ methods: [
8466
+ { name: "word", example: import_faker.faker.lorem.word() },
8467
+ { name: "words", example: import_faker.faker.lorem.words() },
8468
+ { name: "sentence", example: import_faker.faker.lorem.sentence() },
8469
+ { name: "sentences", example: import_faker.faker.lorem.sentences() },
8470
+ { name: "paragraph", example: import_faker.faker.lorem.paragraph() },
8471
+ { name: "slug", example: import_faker.faker.lorem.slug() },
8472
+ { name: "lines", example: import_faker.faker.lorem.lines(1) }
8473
+ ]
8474
+ },
8475
+ {
8476
+ category: "string",
8477
+ methods: [
8478
+ { name: "uuid", example: import_faker.faker.string.uuid() },
8479
+ { name: "alphanumeric", example: import_faker.faker.string.alphanumeric(10) },
8480
+ { name: "numeric", example: import_faker.faker.string.numeric(6) },
8481
+ { name: "alpha", example: import_faker.faker.string.alpha(8) },
8482
+ { name: "nanoid", example: import_faker.faker.string.nanoid() },
8483
+ { name: "hexadecimal", example: import_faker.faker.string.hexadecimal({ length: 8 }) },
8484
+ { name: "sample", example: import_faker.faker.string.sample(12) }
8485
+ ]
8486
+ },
8487
+ {
8488
+ category: "number",
8489
+ methods: [
8490
+ { name: "int", example: String(import_faker.faker.number.int({ max: 9999 })) },
8491
+ { name: "float", example: String(import_faker.faker.number.float({ max: 100, fractionDigits: 2 })) }
8492
+ ]
8493
+ },
8494
+ {
8495
+ category: "datatype",
8496
+ methods: [
8497
+ { name: "boolean", example: String(import_faker.faker.datatype.boolean()) }
8498
+ ]
8499
+ },
8500
+ {
8501
+ category: "image",
8502
+ methods: [
8503
+ { name: "avatar", example: import_faker.faker.image.avatar() },
8504
+ { name: "url", example: import_faker.faker.image.url() }
8505
+ ]
8506
+ },
8507
+ {
8508
+ category: "color",
8509
+ methods: [
8510
+ { name: "human", example: import_faker.faker.color.human() },
8511
+ { name: "rgb", example: import_faker.faker.color.rgb() },
8512
+ { name: "hex", example: import_faker.faker.color.rgb({ format: "hex" }) }
8513
+ ]
8514
+ },
8515
+ {
8516
+ category: "commerce",
8517
+ methods: [
8518
+ { name: "productName", example: import_faker.faker.commerce.productName() },
8519
+ { name: "price", example: import_faker.faker.commerce.price() },
8520
+ { name: "department", example: import_faker.faker.commerce.department() },
8521
+ { name: "productDescription", example: import_faker.faker.commerce.productDescription().slice(0, 60) + "..." },
8522
+ { name: "isbn", example: import_faker.faker.commerce.isbn() }
8523
+ ]
8524
+ },
8525
+ {
8526
+ category: "vehicle",
8527
+ methods: [
8528
+ { name: "vehicle", example: import_faker.faker.vehicle.vehicle() },
8529
+ { name: "manufacturer", example: import_faker.faker.vehicle.manufacturer() },
8530
+ { name: "model", example: import_faker.faker.vehicle.model() },
8531
+ { name: "vin", example: import_faker.faker.vehicle.vin() },
8532
+ { name: "vrm", example: import_faker.faker.vehicle.vrm() }
8533
+ ]
8534
+ },
8535
+ {
8536
+ category: "system",
8537
+ methods: [
8538
+ { name: "fileName", example: import_faker.faker.system.fileName() },
8539
+ { name: "mimeType", example: import_faker.faker.system.mimeType() },
8540
+ { name: "fileExt", example: import_faker.faker.system.fileExt() },
8541
+ { name: "filePath", example: import_faker.faker.system.filePath() },
8542
+ { name: "semver", example: import_faker.faker.system.semver() }
8543
+ ]
8544
+ }
8545
+ ];
8323
8546
  }
8547
+ function callFaker(category, method) {
8548
+ const fakerModule = import_faker.faker[category];
8549
+ const resolvedMethod = METHOD_ALIASES[`${category}.${method}`] ?? method;
8550
+ if (!fakerModule || typeof fakerModule[resolvedMethod] !== "function") {
8551
+ return `[unknown: ${category}.${method}]`;
8552
+ }
8553
+ const result = fakerModule[resolvedMethod]();
8554
+ if (result instanceof Date) return result.toISOString();
8555
+ return String(result);
8556
+ }
8557
+ var import_faker, METHOD_ALIASES;
8558
+ var init_data_generator = __esm({
8559
+ "src/engine/data-generator.ts"() {
8560
+ "use strict";
8561
+ init_cjs_shims();
8562
+ import_faker = require("@faker-js/faker");
8563
+ METHOD_ALIASES = {
8564
+ "internet.userName": "username"
8565
+ };
8566
+ }
8567
+ });
8568
+
8569
+ // src/utils/faker.ts
8324
8570
  function resolveFakeToken(tokenName) {
8325
8571
  switch (tokenName) {
8326
8572
  case "FAKE_FIRST_NAME":
8327
- return fakeFirstName();
8573
+ return callFaker("person", "firstName");
8328
8574
  case "FAKE_LAST_NAME":
8329
- return fakeLastName();
8575
+ return callFaker("person", "lastName");
8330
8576
  case "FAKE_AGE":
8331
- return fakeAge();
8577
+ return String(Math.floor(Math.random() * 63) + 18);
8578
+ // 18–80
8332
8579
  case "FAKE_EMAIL":
8333
- return fakeEmail();
8580
+ return callFaker("internet", "email");
8334
8581
  case "FAKE_PHONE":
8335
- return fakePhone();
8582
+ return callFaker("phone", "number");
8336
8583
  case "FAKE_PASSWORD":
8337
- return fakePassword();
8338
- default: {
8339
- const m = tokenName.match(/^FAKE_NUMBER_(\d+)$/);
8340
- if (m && m[1]) return fakeNumber(parseInt(m[1], 10));
8341
- return null;
8584
+ return callFaker("internet", "password");
8585
+ }
8586
+ const numberMatch = tokenName.match(/^FAKE_NUMBER_(\d+)$/);
8587
+ if (numberMatch && numberMatch[1]) {
8588
+ const d = Math.max(1, parseInt(numberMatch[1], 10));
8589
+ const min = d === 1 ? 0 : Math.pow(10, d - 1);
8590
+ const max = Math.pow(10, d) - 1;
8591
+ return String(Math.floor(Math.random() * (max - min + 1)) + min);
8592
+ }
8593
+ if (tokenName.startsWith("FAKE_")) {
8594
+ const key = tokenName.slice(5);
8595
+ const entry = GENERATOR_MAP[key];
8596
+ if (entry) {
8597
+ return callFaker(entry[0], entry[1]);
8342
8598
  }
8343
8599
  }
8600
+ return null;
8344
8601
  }
8345
- var FIRST_NAMES, LAST_NAMES, EMAIL_DOMAINS;
8602
+ var GENERATOR_MAP;
8346
8603
  var init_faker = __esm({
8347
8604
  "src/utils/faker.ts"() {
8348
8605
  "use strict";
8349
8606
  init_cjs_shims();
8350
- FIRST_NAMES = [
8351
- "Alice",
8352
- "Bob",
8353
- "Charlie",
8354
- "Diana",
8355
- "Eve",
8356
- "Frank",
8357
- "Grace",
8358
- "Henry",
8359
- "Iris",
8360
- "Jack",
8361
- "Kate",
8362
- "Liam",
8363
- "Mia",
8364
- "Noah",
8365
- "Olivia",
8366
- "Paul",
8367
- "Quinn",
8368
- "Rachel",
8369
- "Sam",
8370
- "Tara",
8371
- "Uma",
8372
- "Victor",
8373
- "Wendy",
8374
- "Xander",
8375
- "Yara",
8376
- "Zoe",
8377
- "Aaron",
8378
- "Bella",
8379
- "Carlos",
8380
- "Daisy",
8381
- "Ethan",
8382
- "Fiona"
8383
- ];
8384
- LAST_NAMES = [
8385
- "Smith",
8386
- "Johnson",
8387
- "Williams",
8388
- "Brown",
8389
- "Jones",
8390
- "Miller",
8391
- "Davis",
8392
- "Garcia",
8393
- "Wilson",
8394
- "Martinez",
8395
- "Anderson",
8396
- "Taylor",
8397
- "Thomas",
8398
- "Jackson",
8399
- "White",
8400
- "Harris",
8401
- "Martin",
8402
- "Thompson",
8403
- "Robinson",
8404
- "Clark",
8405
- "Lewis",
8406
- "Lee",
8407
- "Walker",
8408
- "Hall",
8409
- "Allen",
8410
- "Young",
8411
- "King",
8412
- "Wright"
8413
- ];
8414
- EMAIL_DOMAINS = ["example.com", "test.io", "mail.net", "demo.org", "qa.dev"];
8607
+ init_data_generator();
8608
+ GENERATOR_MAP = {};
8609
+ for (const cat of getAvailableGenerators()) {
8610
+ for (const m of cat.methods) {
8611
+ const key = `${cat.category}_${m.name}`.toUpperCase();
8612
+ GENERATOR_MAP[key] = [cat.category, m.name];
8613
+ }
8614
+ }
8415
8615
  }
8416
8616
  });
8417
8617
 
@@ -9571,6 +9771,259 @@ var init_data_loader = __esm({
9571
9771
  }
9572
9772
  });
9573
9773
 
9774
+ // src/storage/visual-regression-store.ts
9775
+ function configPath(rootDir) {
9776
+ return import_path24.default.join(rootDir, ".assuremind", "visual-regression.json");
9777
+ }
9778
+ function diffSummaryPath(rootDir, runId) {
9779
+ return import_path24.default.join(rootDir, "results", "visual-diffs", runId, "summary.json");
9780
+ }
9781
+ function diffRunsDir(rootDir) {
9782
+ return import_path24.default.join(rootDir, "results", "visual-diffs");
9783
+ }
9784
+ function baselinesDir(rootDir) {
9785
+ return import_path24.default.join(rootDir, "baselines");
9786
+ }
9787
+ async function readVisualConfig(rootDir) {
9788
+ const fp = configPath(rootDir);
9789
+ if (!await import_fs_extra20.default.pathExists(fp)) {
9790
+ return { ...DEFAULT_CONFIG2 };
9791
+ }
9792
+ try {
9793
+ const data = await readJson(fp);
9794
+ return { ...DEFAULT_CONFIG2, ...data };
9795
+ } catch {
9796
+ return { ...DEFAULT_CONFIG2 };
9797
+ }
9798
+ }
9799
+ async function writeVisualConfig(rootDir, config) {
9800
+ await atomicWriteJson(configPath(rootDir), config);
9801
+ }
9802
+ async function saveDiffSummary(rootDir, summary) {
9803
+ await atomicWriteJson(diffSummaryPath(rootDir, summary.runId), summary);
9804
+ }
9805
+ async function readDiffSummary(rootDir, runId) {
9806
+ const data = await readJson(diffSummaryPath(rootDir, runId));
9807
+ return data;
9808
+ }
9809
+ async function listDiffRuns(rootDir) {
9810
+ const dir = diffRunsDir(rootDir);
9811
+ if (!await import_fs_extra20.default.pathExists(dir)) return [];
9812
+ const entries = await import_fs_extra20.default.readdir(dir, { withFileTypes: true });
9813
+ const results = [];
9814
+ for (const entry of entries) {
9815
+ if (!entry.isDirectory()) continue;
9816
+ const summaryFile = import_path24.default.join(dir, entry.name, "summary.json");
9817
+ if (!await import_fs_extra20.default.pathExists(summaryFile)) continue;
9818
+ try {
9819
+ const data = await readJson(summaryFile);
9820
+ const diffs = data.diffs ?? [];
9821
+ results.push({
9822
+ runId: data.runId,
9823
+ timestamp: data.timestamp,
9824
+ totalDiffs: diffs.length,
9825
+ passed: diffs.filter((d) => d.status === "passed").length,
9826
+ failed: diffs.filter((d) => d.status === "failed").length,
9827
+ newCount: diffs.filter((d) => d.status === "new").length,
9828
+ approved: diffs.filter((d) => d.status === "approved").length,
9829
+ rejected: diffs.filter((d) => d.status === "rejected").length
9830
+ });
9831
+ } catch {
9832
+ }
9833
+ }
9834
+ results.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
9835
+ return results;
9836
+ }
9837
+ async function updateDiffStatus(rootDir, runId, stepId, status) {
9838
+ const summary = await readDiffSummary(rootDir, runId);
9839
+ const entry = summary.diffs.find((d) => d.stepId === stepId);
9840
+ if (!entry) {
9841
+ throw new Error(`Diff entry not found for stepId "${stepId}" in run "${runId}"`);
9842
+ }
9843
+ entry.status = status;
9844
+ await saveDiffSummary(rootDir, summary);
9845
+ return summary;
9846
+ }
9847
+ async function approveAllDiffs(rootDir, runId) {
9848
+ const summary = await readDiffSummary(rootDir, runId);
9849
+ for (const entry of summary.diffs) {
9850
+ if (entry.status === "failed" || entry.status === "new") {
9851
+ entry.status = "approved";
9852
+ }
9853
+ }
9854
+ await saveDiffSummary(rootDir, summary);
9855
+ return summary;
9856
+ }
9857
+ async function listBaselines(rootDir) {
9858
+ const dir = baselinesDir(rootDir);
9859
+ if (!await import_fs_extra20.default.pathExists(dir)) return [];
9860
+ const results = [];
9861
+ async function walk(currentDir) {
9862
+ const entries = await import_fs_extra20.default.readdir(currentDir, { withFileTypes: true });
9863
+ for (const entry of entries) {
9864
+ const fullPath = import_path24.default.join(currentDir, entry.name);
9865
+ if (entry.isDirectory()) {
9866
+ await walk(fullPath);
9867
+ } else if (entry.name.endsWith(".png")) {
9868
+ const relativePath = import_path24.default.relative(dir, fullPath).replace(/\\/g, "/");
9869
+ const parts = relativePath.split("/");
9870
+ const stat = await import_fs_extra20.default.stat(fullPath);
9871
+ results.push({
9872
+ path: fullPath,
9873
+ relativePath,
9874
+ suite: parts.length > 1 ? parts[0] : "",
9875
+ caseName: parts.length > 2 ? parts[1] : "",
9876
+ fileName: entry.name,
9877
+ sizeBytes: stat.size
9878
+ });
9879
+ }
9880
+ }
9881
+ }
9882
+ await walk(dir);
9883
+ return results;
9884
+ }
9885
+ async function deleteBaseline(rootDir, baselinePath2) {
9886
+ const dir = baselinesDir(rootDir);
9887
+ const resolved = import_path24.default.resolve(baselinePath2);
9888
+ if (!resolved.startsWith(import_path24.default.resolve(dir))) {
9889
+ throw new Error("Path is outside baselines directory");
9890
+ }
9891
+ await import_fs_extra20.default.remove(resolved);
9892
+ }
9893
+ var import_path24, import_fs_extra20, DEFAULT_CONFIG2;
9894
+ var init_visual_regression_store = __esm({
9895
+ "src/storage/visual-regression-store.ts"() {
9896
+ "use strict";
9897
+ init_cjs_shims();
9898
+ import_path24 = __toESM(require("path"));
9899
+ import_fs_extra20 = __toESM(require("fs-extra"));
9900
+ init_utils();
9901
+ DEFAULT_CONFIG2 = {
9902
+ enabled: false,
9903
+ defaultThreshold: 0.5,
9904
+ fullPage: true,
9905
+ maskSelectors: []
9906
+ };
9907
+ }
9908
+ });
9909
+
9910
+ // src/engine/visual-comparator.ts
9911
+ async function readPng(filePath) {
9912
+ const buffer = await import_fs_extra21.default.readFile(filePath);
9913
+ return new Promise((resolve, reject) => {
9914
+ const png = new import_pngjs.PNG();
9915
+ png.parse(buffer, (err, data) => {
9916
+ if (err) reject(err);
9917
+ else resolve(data);
9918
+ });
9919
+ });
9920
+ }
9921
+ function encodePng(png) {
9922
+ return import_pngjs.PNG.sync.write(png);
9923
+ }
9924
+ async function compareScreenshots(baselinePath2, actualPath, diffOutputPath, options = {}) {
9925
+ const { threshold = 0.1, diffThreshold = 0.5 } = options;
9926
+ const baselineExists = await import_fs_extra21.default.pathExists(baselinePath2);
9927
+ if (!baselineExists) {
9928
+ await import_fs_extra21.default.ensureDir(import_path25.default.dirname(baselinePath2));
9929
+ await import_fs_extra21.default.copy(actualPath, baselinePath2);
9930
+ return {
9931
+ match: true,
9932
+ diffPercentage: 0,
9933
+ diffPixels: 0,
9934
+ totalPixels: 0,
9935
+ baselinePath: baselinePath2,
9936
+ actualPath
9937
+ };
9938
+ }
9939
+ const [baselinePng, actualPng] = await Promise.all([
9940
+ readPng(baselinePath2),
9941
+ readPng(actualPath)
9942
+ ]);
9943
+ if (baselinePng.width !== actualPng.width || baselinePng.height !== actualPng.height) {
9944
+ await import_fs_extra21.default.ensureDir(import_path25.default.dirname(diffOutputPath));
9945
+ const baseCopyPath2 = diffOutputPath.replace(/\.png$/, "-baseline.png");
9946
+ const actualCopyPath2 = diffOutputPath.replace(/\.png$/, "-actual.png");
9947
+ await Promise.all([
9948
+ import_fs_extra21.default.copy(baselinePath2, baseCopyPath2),
9949
+ import_fs_extra21.default.copy(actualPath, actualCopyPath2)
9950
+ ]);
9951
+ const totalPixels2 = Math.max(
9952
+ baselinePng.width * baselinePng.height,
9953
+ actualPng.width * actualPng.height
9954
+ );
9955
+ return {
9956
+ match: false,
9957
+ diffPercentage: 100,
9958
+ diffPixels: totalPixels2,
9959
+ totalPixels: totalPixels2,
9960
+ baselinePath: baselinePath2,
9961
+ actualPath,
9962
+ error: `Size mismatch: baseline ${baselinePng.width}x${baselinePng.height} vs actual ${actualPng.width}x${actualPng.height}`
9963
+ };
9964
+ }
9965
+ const { width, height } = baselinePng;
9966
+ const diff = new import_pngjs.PNG({ width, height });
9967
+ const diffPixels = (0, import_pixelmatch.default)(
9968
+ baselinePng.data,
9969
+ actualPng.data,
9970
+ diff.data,
9971
+ width,
9972
+ height,
9973
+ { threshold }
9974
+ );
9975
+ const totalPixels = width * height;
9976
+ const diffPercentage = totalPixels > 0 ? diffPixels / totalPixels * 100 : 0;
9977
+ const match = diffPercentage <= diffThreshold;
9978
+ await import_fs_extra21.default.ensureDir(import_path25.default.dirname(diffOutputPath));
9979
+ const diffBuffer = encodePng(diff);
9980
+ const baseCopyPath = diffOutputPath.replace(/\.png$/, "-baseline.png");
9981
+ const actualCopyPath = diffOutputPath.replace(/\.png$/, "-actual.png");
9982
+ await Promise.all([
9983
+ import_fs_extra21.default.writeFile(diffOutputPath, diffBuffer),
9984
+ import_fs_extra21.default.copy(baselinePath2, baseCopyPath),
9985
+ import_fs_extra21.default.copy(actualPath, actualCopyPath)
9986
+ ]);
9987
+ return {
9988
+ match,
9989
+ diffPercentage: Math.round(diffPercentage * 100) / 100,
9990
+ diffPixels,
9991
+ totalPixels,
9992
+ diffImagePath: diffOutputPath,
9993
+ baselinePath: baselinePath2,
9994
+ actualPath
9995
+ };
9996
+ }
9997
+ function getBaselinePath(rootDir, suiteSlug, caseSlug, stepId, browser, viewport) {
9998
+ return import_path25.default.join(
9999
+ rootDir,
10000
+ "baselines",
10001
+ suiteSlug,
10002
+ caseSlug,
10003
+ `${stepId}-${browser}-${viewport.width}x${viewport.height}.png`
10004
+ );
10005
+ }
10006
+ function getDiffPath(rootDir, runId, stepId, browser, viewport) {
10007
+ return import_path25.default.join(
10008
+ rootDir,
10009
+ "results",
10010
+ "visual-diffs",
10011
+ runId,
10012
+ `${stepId}-${browser}-${viewport.width}x${viewport.height}-diff.png`
10013
+ );
10014
+ }
10015
+ var import_pixelmatch, import_pngjs, import_fs_extra21, import_path25;
10016
+ var init_visual_comparator = __esm({
10017
+ "src/engine/visual-comparator.ts"() {
10018
+ "use strict";
10019
+ init_cjs_shims();
10020
+ import_pixelmatch = __toESM(require("pixelmatch"));
10021
+ import_pngjs = require("pngjs");
10022
+ import_fs_extra21 = __toESM(require("fs-extra"));
10023
+ import_path25 = __toESM(require("path"));
10024
+ }
10025
+ });
10026
+
9574
10027
  // node_modules/escape-string-regexp/index.js
9575
10028
  var require_escape_string_regexp = __commonJS({
9576
10029
  "node_modules/escape-string-regexp/index.js"(exports2, module2) {
@@ -10705,11 +11158,11 @@ var require_is_docker = __commonJS({
10705
11158
  "node_modules/chrome-launcher/node_modules/is-docker/index.js"(exports2, module2) {
10706
11159
  "use strict";
10707
11160
  init_cjs_shims();
10708
- var fs30 = require("fs");
11161
+ var fs34 = require("fs");
10709
11162
  var isDocker;
10710
11163
  function hasDockerEnv() {
10711
11164
  try {
10712
- fs30.statSync("/.dockerenv");
11165
+ fs34.statSync("/.dockerenv");
10713
11166
  return true;
10714
11167
  } catch (_) {
10715
11168
  return false;
@@ -10717,7 +11170,7 @@ var require_is_docker = __commonJS({
10717
11170
  }
10718
11171
  function hasDockerCGroup() {
10719
11172
  try {
10720
- return fs30.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
11173
+ return fs34.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
10721
11174
  } catch (_) {
10722
11175
  return false;
10723
11176
  }
@@ -10737,7 +11190,7 @@ var require_is_wsl = __commonJS({
10737
11190
  "use strict";
10738
11191
  init_cjs_shims();
10739
11192
  var os = require("os");
10740
- var fs30 = require("fs");
11193
+ var fs34 = require("fs");
10741
11194
  var isDocker = require_is_docker();
10742
11195
  var isWsl3 = () => {
10743
11196
  if (process.platform !== "linux") {
@@ -10750,7 +11203,7 @@ var require_is_wsl = __commonJS({
10750
11203
  return true;
10751
11204
  }
10752
11205
  try {
10753
- return fs30.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft") ? !isDocker() : false;
11206
+ return fs34.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft") ? !isDocker() : false;
10754
11207
  } catch (_) {
10755
11208
  return false;
10756
11209
  }
@@ -10811,15 +11264,15 @@ function toWSLPath(dir, fallback) {
10811
11264
  return fallback;
10812
11265
  }
10813
11266
  }
10814
- function getLocalAppDataPath(path37) {
11267
+ function getLocalAppDataPath(path41) {
10815
11268
  const userRegExp = /\/mnt\/([a-z])\/Users\/([^\/:]+)\/AppData\//;
10816
- const results = userRegExp.exec(path37) || [];
11269
+ const results = userRegExp.exec(path41) || [];
10817
11270
  return `/mnt/${results[1]}/Users/${results[2]}/AppData/Local`;
10818
11271
  }
10819
- function getWSLLocalAppDataPath(path37) {
11272
+ function getWSLLocalAppDataPath(path41) {
10820
11273
  const userRegExp = /\/([a-z])\/Users\/([^\/:]+)\/AppData\//;
10821
- const results = userRegExp.exec(path37) || [];
10822
- return toWSLPath(`${results[1]}:\\Users\\${results[2]}\\AppData\\Local`, getLocalAppDataPath(path37));
11274
+ const results = userRegExp.exec(path41) || [];
11275
+ return toWSLPath(`${results[1]}:\\Users\\${results[2]}\\AppData\\Local`, getLocalAppDataPath(path41));
10823
11276
  }
10824
11277
  function makeUnixTmpDir() {
10825
11278
  return import_child_process3.default.execSync("mktemp -d -t lighthouse.XXXXXXX").toString().trim();
@@ -10827,16 +11280,16 @@ function makeUnixTmpDir() {
10827
11280
  function makeWin32TmpDir() {
10828
11281
  const winTmpPath = process.env.TEMP || process.env.TMP || (process.env.SystemRoot || process.env.windir) + "\\temp";
10829
11282
  const randomNumber = Math.floor(Math.random() * 9e7 + 1e7);
10830
- const tmpdir = (0, import_path24.join)(winTmpPath, "lighthouse." + randomNumber);
11283
+ const tmpdir = (0, import_path26.join)(winTmpPath, "lighthouse." + randomNumber);
10831
11284
  (0, import_fs.mkdirSync)(tmpdir, { recursive: true });
10832
11285
  return tmpdir;
10833
11286
  }
10834
- var import_path24, import_child_process3, import_fs, import_is_wsl, LauncherError, ChromePathNotSetError, InvalidUserDataDirectoryError, UnsupportedPlatformError, ChromeNotInstalledError;
11287
+ var import_path26, import_child_process3, import_fs, import_is_wsl, LauncherError, ChromePathNotSetError, InvalidUserDataDirectoryError, UnsupportedPlatformError, ChromeNotInstalledError;
10835
11288
  var init_utils3 = __esm({
10836
11289
  "node_modules/chrome-launcher/dist/utils.js"() {
10837
11290
  "use strict";
10838
11291
  init_cjs_shims();
10839
- import_path24 = require("path");
11292
+ import_path26 = require("path");
10840
11293
  import_child_process3 = __toESM(require("child_process"), 1);
10841
11294
  import_fs = require("fs");
10842
11295
  import_is_wsl = __toESM(require_is_wsl(), 1);
@@ -10912,7 +11365,7 @@ function darwin() {
10912
11365
  }
10913
11366
  (0, import_child_process4.execSync)(`${LSREGISTER} -dump | grep -i 'google chrome\\( canary\\)\\?\\.app' | awk '{$1=""; print $0}'`).toString().split(newLineRegex).forEach((inst) => {
10914
11367
  suffixes.forEach((suffix) => {
10915
- const execPath = import_path25.default.join(inst.substring(0, inst.indexOf(".app") + 4).trim(), suffix);
11368
+ const execPath = import_path27.default.join(inst.substring(0, inst.indexOf(".app") + 4).trim(), suffix);
10916
11369
  if (canAccess(execPath) && installations.indexOf(execPath) === -1) {
10917
11370
  installations.push(execPath);
10918
11371
  }
@@ -10952,7 +11405,7 @@ function linux() {
10952
11405
  installations.push(customChromePath);
10953
11406
  }
10954
11407
  const desktopInstallationFolders = [
10955
- import_path25.default.join((0, import_os.homedir)(), ".local/share/applications/"),
11408
+ import_path27.default.join((0, import_os.homedir)(), ".local/share/applications/"),
10956
11409
  "/usr/share/applications/"
10957
11410
  ];
10958
11411
  desktopInstallationFolders.forEach((folder) => {
@@ -11000,8 +11453,8 @@ function wsl() {
11000
11453
  function win32() {
11001
11454
  const installations = [];
11002
11455
  const suffixes = [
11003
- `${import_path25.default.sep}Google${import_path25.default.sep}Chrome SxS${import_path25.default.sep}Application${import_path25.default.sep}chrome.exe`,
11004
- `${import_path25.default.sep}Google${import_path25.default.sep}Chrome${import_path25.default.sep}Application${import_path25.default.sep}chrome.exe`
11456
+ `${import_path27.default.sep}Google${import_path27.default.sep}Chrome SxS${import_path27.default.sep}Application${import_path27.default.sep}chrome.exe`,
11457
+ `${import_path27.default.sep}Google${import_path27.default.sep}Chrome${import_path27.default.sep}Application${import_path27.default.sep}chrome.exe`
11005
11458
  ];
11006
11459
  const prefixes = [
11007
11460
  process.env.LOCALAPPDATA,
@@ -11013,7 +11466,7 @@ function win32() {
11013
11466
  installations.push(customChromePath);
11014
11467
  }
11015
11468
  prefixes.forEach((prefix) => suffixes.forEach((suffix) => {
11016
- const chromePath = import_path25.default.join(prefix, suffix);
11469
+ const chromePath = import_path27.default.join(prefix, suffix);
11017
11470
  if (canAccess(chromePath)) {
11018
11471
  installations.push(chromePath);
11019
11472
  }
@@ -11061,13 +11514,13 @@ function findChromeExecutables(folder) {
11061
11514
  }
11062
11515
  return installations;
11063
11516
  }
11064
- var import_fs2, import_path25, import_os, import_child_process4, import_escape_string_regexp, newLineRegex;
11517
+ var import_fs2, import_path27, import_os, import_child_process4, import_escape_string_regexp, newLineRegex;
11065
11518
  var init_chrome_finder = __esm({
11066
11519
  "node_modules/chrome-launcher/dist/chrome-finder.js"() {
11067
11520
  "use strict";
11068
11521
  init_cjs_shims();
11069
11522
  import_fs2 = __toESM(require("fs"), 1);
11070
- import_path25 = __toESM(require("path"), 1);
11523
+ import_path27 = __toESM(require("path"), 1);
11071
11524
  import_os = require("os");
11072
11525
  import_child_process4 = require("child_process");
11073
11526
  import_escape_string_regexp = __toESM(require_escape_string_regexp(), 1);
@@ -11219,12 +11672,12 @@ function killAll() {
11219
11672
  }
11220
11673
  return errors;
11221
11674
  }
11222
- var fs18, net, import_child_process5, isWsl2, isWindows3, _SIGINT, _SIGINT_EXIT_CODE, _SUPPORTED_PLATFORMS, instances, sigintListener, Launcher;
11675
+ var fs20, net, import_child_process5, isWsl2, isWindows3, _SIGINT, _SIGINT_EXIT_CODE, _SUPPORTED_PLATFORMS, instances, sigintListener, Launcher;
11223
11676
  var init_chrome_launcher = __esm({
11224
11677
  "node_modules/chrome-launcher/dist/chrome-launcher.js"() {
11225
11678
  "use strict";
11226
11679
  init_cjs_shims();
11227
- fs18 = __toESM(require("fs"), 1);
11680
+ fs20 = __toESM(require("fs"), 1);
11228
11681
  net = __toESM(require("net"), 1);
11229
11682
  init_chrome_finder();
11230
11683
  init_random_port();
@@ -11247,7 +11700,7 @@ var init_chrome_launcher = __esm({
11247
11700
  this.opts = opts;
11248
11701
  this.tmpDirandPidFileReady = false;
11249
11702
  this.remoteDebuggingPipes = null;
11250
- this.fs = moduleOverrides.fs || fs18;
11703
+ this.fs = moduleOverrides.fs || fs20;
11251
11704
  this.spawn = moduleOverrides.spawn || import_child_process5.spawn;
11252
11705
  lighthouse_logger_default.setLevel(defaults(this.opts.logLevel, "silent"));
11253
11706
  this.startingUrl = defaults(this.opts.startingUrl, "about:blank");
@@ -11525,7 +11978,9 @@ var init_dist = __esm({
11525
11978
  // src/engine/runner.ts
11526
11979
  var runner_exports = {};
11527
11980
  __export(runner_exports, {
11528
- runTests: () => runTests
11981
+ getActiveRunId: () => getActiveRunId,
11982
+ runTests: () => runTests,
11983
+ stopActiveRun: () => stopActiveRun
11529
11984
  });
11530
11985
  function isCaseApiOnly(tc) {
11531
11986
  if (tc.steps.length === 0) return false;
@@ -11537,10 +11992,24 @@ function isSuiteApiOnly(suite) {
11537
11992
  function isSuiteAudit(suite) {
11538
11993
  return suite.type === "audit" || suite.type === "performance";
11539
11994
  }
11995
+ function stopActiveRun() {
11996
+ if (_activeRunAbort) {
11997
+ _activeRunAbort();
11998
+ }
11999
+ }
12000
+ function getActiveRunId() {
12001
+ return _activeRunId;
12002
+ }
11540
12003
  async function runTests(rootDir, runConfig, autotestConfig, ws) {
11541
12004
  const runId = (0, import_crypto10.randomUUID)();
11542
12005
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
11543
- const testsDir = import_path26.default.join(rootDir, "tests");
12006
+ const testsDir = import_path28.default.join(rootDir, "tests");
12007
+ let _runStopped = false;
12008
+ _activeRunId = runId;
12009
+ _activeRunAbort = () => {
12010
+ _runStopped = true;
12011
+ logger31.info({ runId }, "Run stop requested");
12012
+ };
11544
12013
  const activeEnv = autotestConfig.environment ?? "dev";
11545
12014
  const envUrl = autotestConfig.environmentUrls?.[activeEnv];
11546
12015
  if (envUrl) {
@@ -11557,6 +12026,14 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
11557
12026
  const browserManager = new BrowserManager();
11558
12027
  const healingReporter = new HealingReporter();
11559
12028
  const allureReporter = new AllureReporter(rootDir, runId, activeEnv);
12029
+ const visualConfig = await readVisualConfig(rootDir);
12030
+ const visualDiffEntries = [];
12031
+ if (visualConfig.enabled) {
12032
+ if (autotestConfig.screenshot !== "on") {
12033
+ autotestConfig = { ...autotestConfig, screenshot: "on" };
12034
+ }
12035
+ logToFile("info", "Visual regression enabled \u2014 screenshots forced ON", { threshold: visualConfig.defaultThreshold, fullPage: visualConfig.fullPage });
12036
+ }
11560
12037
  try {
11561
12038
  const pairs = await collectWork(testsDir, runConfig);
11562
12039
  if (pairs.length === 0) {
@@ -11663,6 +12140,9 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
11663
12140
  allPairs,
11664
12141
  autotestConfig.parallel,
11665
12142
  async ({ browserName, browser, suite, testCase, dataRow, dataRowIndex }) => {
12143
+ if (_runStopped) {
12144
+ return null;
12145
+ }
11666
12146
  const caseVariables = dataRow ? { ...variables, ...dataRow } : variables;
11667
12147
  const caseName = dataRow ? `${testCase.name} [row ${(dataRowIndex ?? 0) + 1}]` : testCase.name;
11668
12148
  ws?.broadcast("run:case", {
@@ -11692,7 +12172,9 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
11692
12172
  ws,
11693
12173
  logToFile,
11694
12174
  compositeCaseId,
11695
- ragManager
12175
+ ragManager,
12176
+ visualConfig,
12177
+ visualDiffEntries
11696
12178
  );
11697
12179
  result.suiteType = suite.type;
11698
12180
  if (dataRow !== void 0) {
@@ -11715,14 +12197,15 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
11715
12197
  return { ...result, browserName };
11716
12198
  }
11717
12199
  );
12200
+ const validCaseResults = allCaseResults.filter(Boolean);
11718
12201
  if (browsers.length > 1) {
11719
12202
  for (const browserName of browsers) {
11720
- const browserResults = allCaseResults.filter((r) => r.browserName === browserName);
12203
+ const browserResults = validCaseResults.filter((r) => r.browserName === browserName);
11721
12204
  const grouped = groupBySuite(browserResults, browserName);
11722
12205
  allSuiteResults.push(...grouped);
11723
12206
  }
11724
12207
  } else {
11725
- const grouped = groupBySuite(allCaseResults);
12208
+ const grouped = groupBySuite(validCaseResults);
11726
12209
  allSuiteResults.push(...grouped);
11727
12210
  }
11728
12211
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -11742,9 +12225,21 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
11742
12225
  generateAllureReport(rootDir, runId).catch(() => void 0);
11743
12226
  }
11744
12227
  if (autotestConfig.reporting.json) {
11745
- const jsonPath = import_path26.default.join(rootDir, "results", `run-${runId}.json`);
11746
- await import_fs_extra20.default.ensureDir(import_path26.default.dirname(jsonPath));
11747
- await import_fs_extra20.default.writeJson(jsonPath, runResult, { spaces: 2 });
12228
+ const jsonPath = import_path28.default.join(rootDir, "results", `run-${runId}.json`);
12229
+ await import_fs_extra22.default.ensureDir(import_path28.default.dirname(jsonPath));
12230
+ await import_fs_extra22.default.writeJson(jsonPath, runResult, { spaces: 2 });
12231
+ }
12232
+ if (visualConfig.enabled && visualDiffEntries.length > 0) {
12233
+ await saveDiffSummary(rootDir, {
12234
+ runId,
12235
+ timestamp: finishedAt,
12236
+ diffs: visualDiffEntries
12237
+ });
12238
+ logToFile("info", `Visual regression: ${visualDiffEntries.length} comparisons saved`, {
12239
+ passed: visualDiffEntries.filter((d) => d.status === "passed").length,
12240
+ failed: visualDiffEntries.filter((d) => d.status === "failed").length,
12241
+ new: visualDiffEntries.filter((d) => d.status === "new").length
12242
+ });
11748
12243
  }
11749
12244
  await healingReporter.flush(rootDir, runId);
11750
12245
  await smartRouter.getCache().persist();
@@ -11768,17 +12263,22 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
11768
12263
  );
11769
12264
  return runResult;
11770
12265
  } finally {
12266
+ _activeRunAbort = null;
12267
+ _activeRunId = null;
11771
12268
  await browserManager.closeAll();
12269
+ if (_runStopped) {
12270
+ ws?.broadcast("run:stopped", { runId });
12271
+ }
11772
12272
  }
11773
12273
  }
11774
- async function runCase(rootDir, runId, suite, testCase, browserName, browser, config, variables, sharedVariables, provider, budget, healingReporter, browserManager, ws, logToFile, wsCaseId, ragManager) {
12274
+ async function runCase(rootDir, runId, suite, testCase, browserName, browser, config, variables, sharedVariables, provider, budget, healingReporter, browserManager, ws, logToFile, wsCaseId, ragManager, visualConfig, visualDiffEntries) {
11775
12275
  const broadcastCaseId = wsCaseId ?? testCase.id;
11776
12276
  const caseStart = (/* @__PURE__ */ new Date()).toISOString();
11777
12277
  const stepResults = [];
11778
12278
  const previousSteps = [];
11779
12279
  const apiOnly = isSuiteApiOnly(suite) || isCaseApiOnly(testCase);
11780
12280
  const caseConfig = isSuiteAudit(suite) ? { ...config, screenshot: "off", video: "off", trace: "off" } : config;
11781
- const tracePath = !apiOnly && caseConfig.trace !== "off" ? import_path26.default.join(rootDir, "results", "traces", `${runId}_${testCase.id}_${browserName}.zip`) : void 0;
12281
+ const tracePath = !apiOnly && caseConfig.trace !== "off" ? import_path28.default.join(rootDir, "results", "traces", `${runId}_${testCase.id}_${browserName}.zip`) : void 0;
11782
12282
  let context = null;
11783
12283
  let page;
11784
12284
  if (apiOnly) {
@@ -11786,7 +12286,7 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
11786
12286
  logger31.debug({ caseName: testCase.name }, "API-only case \u2014 zero browser, using standalone HTTP client");
11787
12287
  } else {
11788
12288
  if (!browser) throw new Error("Browser required for UI test cases");
11789
- context = await browserManager.newContext(browser, caseConfig, import_path26.default.join(rootDir, "results", "videos"));
12289
+ context = await browserManager.newContext(browser, caseConfig, import_path28.default.join(rootDir, "results", "videos"));
11790
12290
  page = await browserManager.newPage(context);
11791
12291
  await installHighlighter(page);
11792
12292
  }
@@ -11895,6 +12395,40 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
11895
12395
  ...result.healed ? { healed: true } : {}
11896
12396
  });
11897
12397
  stepResults.push(result);
12398
+ if (visualConfig?.enabled && visualDiffEntries && !apiOnly && step.visualCheck && result.status === "passed" && result.screenshotPath) {
12399
+ try {
12400
+ const viewport = config.viewport ?? { width: 1280, height: 720 };
12401
+ const suiteSlug = toSlug(suite.name);
12402
+ const caseSlug = toSlug(testCase.name);
12403
+ const bPath = getBaselinePath(rootDir, suiteSlug, caseSlug, step.id, browserName, viewport);
12404
+ const dPath = getDiffPath(rootDir, runId, step.id, browserName, viewport);
12405
+ const cmpResult = await compareScreenshots(bPath, result.screenshotPath, dPath, {
12406
+ threshold: 0.1,
12407
+ diffThreshold: visualConfig.defaultThreshold
12408
+ });
12409
+ const entryStatus = cmpResult.totalPixels === 0 ? "new" : cmpResult.match ? "passed" : "failed";
12410
+ visualDiffEntries.push({
12411
+ stepId: step.id,
12412
+ caseId: testCase.id,
12413
+ caseName: testCase.name,
12414
+ suiteName: suite.name,
12415
+ suiteId: suite.id,
12416
+ browser: browserName,
12417
+ viewport: `${viewport.width}x${viewport.height}`,
12418
+ diffPercentage: cmpResult.diffPercentage,
12419
+ threshold: visualConfig.defaultThreshold,
12420
+ status: entryStatus,
12421
+ baselinePath: cmpResult.baselinePath,
12422
+ actualPath: cmpResult.actualPath,
12423
+ diffPath: cmpResult.diffImagePath ?? dPath
12424
+ });
12425
+ logToFile?.("info", ` Visual: ${step.id} \u2014 ${entryStatus} (diff: ${cmpResult.diffPercentage}%)`, {
12426
+ baseline: bPath
12427
+ });
12428
+ } catch (vrErr) {
12429
+ logger31.debug({ err: vrErr, stepId: step.id }, "Visual regression comparison failed");
12430
+ }
12431
+ }
11898
12432
  if (result.status === "passed") {
11899
12433
  previousSteps.push({ instruction: step.instruction, code: result.code });
11900
12434
  } else if (result.status === "failed") {
@@ -11921,16 +12455,16 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
11921
12455
  const launch2 = chromeLauncher.launch ?? chromeLauncher.default?.launch;
11922
12456
  const { chromium: pwChromium } = await import("playwright");
11923
12457
  const chromePath = pwChromium.executablePath();
11924
- const lhBaseDir = import_path26.default.join(rootDir, "results", ".lh-profiles");
11925
- await import_fs_extra20.default.ensureDir(lhBaseDir);
12458
+ const lhBaseDir = import_path28.default.join(rootDir, "results", ".lh-profiles");
12459
+ await import_fs_extra22.default.ensureDir(lhBaseDir);
11926
12460
  for (let auditIdx = 0; auditIdx < navigatedSteps.length; auditIdx++) {
11927
12461
  const { s, idx } = navigatedSteps[auditIdx];
11928
12462
  const url = s.auditUrl ?? s.navigatedToUrl;
11929
12463
  logToFile?.("info", ` Auditing: ${url}`);
11930
12464
  let chromeInstance = null;
11931
- const userDataDir = import_path26.default.join(lhBaseDir, `audit-${Date.now()}-${auditIdx}`);
12465
+ const userDataDir = import_path28.default.join(lhBaseDir, `audit-${Date.now()}-${auditIdx}`);
11932
12466
  try {
11933
- await import_fs_extra20.default.ensureDir(userDataDir);
12467
+ await import_fs_extra22.default.ensureDir(userDataDir);
11934
12468
  chromeInstance = await launch2({
11935
12469
  chromePath,
11936
12470
  chromeFlags: ["--headless", "--no-sandbox", "--disable-gpu"],
@@ -12020,7 +12554,7 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
12020
12554
  } catch {
12021
12555
  }
12022
12556
  try {
12023
- await import_fs_extra20.default.remove(userDataDir);
12557
+ await import_fs_extra22.default.remove(userDataDir);
12024
12558
  } catch {
12025
12559
  }
12026
12560
  }
@@ -12073,11 +12607,11 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
12073
12607
  await browserManager.closeContext(context, caseConfig, tracePath);
12074
12608
  const videoDeleteOnPass = caseConfig.video === "retain-on-failure" || caseConfig.video === "on-first-retry";
12075
12609
  if (videoDeleteOnPass && caseStatus === "passed" && pendingVideoPath) {
12076
- await import_fs_extra20.default.remove(pendingVideoPath).catch(() => void 0);
12610
+ await import_fs_extra22.default.remove(pendingVideoPath).catch(() => void 0);
12077
12611
  }
12078
12612
  const traceDeleteOnPass = caseConfig.trace === "retain-on-failure" || caseConfig.trace === "on-first-retry";
12079
12613
  if (traceDeleteOnPass && caseStatus === "passed" && tracePath) {
12080
- await import_fs_extra20.default.remove(tracePath).catch(() => void 0);
12614
+ await import_fs_extra22.default.remove(tracePath).catch(() => void 0);
12081
12615
  }
12082
12616
  }
12083
12617
  }
@@ -12219,14 +12753,14 @@ function buildRunResult(runId, startedAt, finishedAt, suites, environment) {
12219
12753
  skipped
12220
12754
  };
12221
12755
  }
12222
- var import_path26, import_crypto10, import_fs_extra20, logger31;
12756
+ var import_path28, import_crypto10, import_fs_extra22, logger31, _activeRunAbort, _activeRunId;
12223
12757
  var init_runner = __esm({
12224
12758
  "src/engine/runner.ts"() {
12225
12759
  "use strict";
12226
12760
  init_cjs_shims();
12227
- import_path26 = __toESM(require("path"));
12761
+ import_path28 = __toESM(require("path"));
12228
12762
  import_crypto10 = require("crypto");
12229
- import_fs_extra20 = __toESM(require("fs-extra"));
12763
+ import_fs_extra22 = __toESM(require("fs-extra"));
12230
12764
  init_suite_store();
12231
12765
  init_case_store();
12232
12766
  init_variable_store();
@@ -12244,8 +12778,13 @@ var init_runner = __esm({
12244
12778
  init_highlighter();
12245
12779
  init_data_loader();
12246
12780
  init_step_type_detector();
12781
+ init_visual_regression_store();
12782
+ init_visual_comparator();
12783
+ init_sanitize();
12247
12784
  init_logger();
12248
12785
  logger31 = createChildLogger("runner");
12786
+ _activeRunAbort = null;
12787
+ _activeRunId = null;
12249
12788
  }
12250
12789
  });
12251
12790
 
@@ -12300,6 +12839,10 @@ async function runRoutes(fastify) {
12300
12839
  return sendError(reply, 500, "Failed to start run", err);
12301
12840
  }
12302
12841
  });
12842
+ fastify.post("/run/stop", async (_req, reply) => {
12843
+ stopActiveRun();
12844
+ return reply.status(202).send({ status: "stopping" });
12845
+ });
12303
12846
  fastify.get("/run", async (req, reply) => {
12304
12847
  const parsed = ListRunsQuery.safeParse(req.query);
12305
12848
  if (!parsed.success) {
@@ -12739,7 +13282,7 @@ async function storyRoutes(fastify) {
12739
13282
  return reply.status(400).send({ error: "Invalid request", details: parsed.error.issues });
12740
13283
  }
12741
13284
  try {
12742
- const testsDir = import_path27.default.join(rootDir, "tests");
13285
+ const testsDir = import_path29.default.join(rootDir, "tests");
12743
13286
  let storyText = parsed.data.story;
12744
13287
  if (parsed.data.jiraUrl) {
12745
13288
  const issueKey = extractIssueKey(parsed.data.jiraUrl);
@@ -12785,7 +13328,7 @@ async function storyRoutes(fastify) {
12785
13328
  return reply.status(400).send({ error: "Invalid request", details: parsed.error.issues });
12786
13329
  }
12787
13330
  const { stories, concurrency } = parsed.data;
12788
- const testsDir = import_path27.default.join(rootDir, "tests");
13331
+ const testsDir = import_path29.default.join(rootDir, "tests");
12789
13332
  const wsManager = fastify.wsManager;
12790
13333
  logger34.info({ total: stories.length, concurrency }, "Bulk story generation requested");
12791
13334
  const enrichedStories = await Promise.all(
@@ -12897,7 +13440,7 @@ async function storyRoutes(fastify) {
12897
13440
  }
12898
13441
  const storyText = storyLines.join("\n");
12899
13442
  const suiteName = parsed.data.suiteName ?? `${spec.title} API Tests`;
12900
- const testsDir = import_path27.default.join(rootDir, "tests");
13443
+ const testsDir = import_path29.default.join(rootDir, "tests");
12901
13444
  const { suiteId } = await generateSuiteFromStory(storyText, {
12902
13445
  rootDir,
12903
13446
  outputDir: testsDir,
@@ -12917,12 +13460,12 @@ async function storyRoutes(fastify) {
12917
13460
  }
12918
13461
  });
12919
13462
  }
12920
- var import_path27, import_zod15, logger34, GenerateStoryBody, FetchJiraBody;
13463
+ var import_path29, import_zod15, logger34, GenerateStoryBody, FetchJiraBody;
12921
13464
  var init_story = __esm({
12922
13465
  "src/server/routes/story.ts"() {
12923
13466
  "use strict";
12924
13467
  init_cjs_shims();
12925
- import_path27 = __toESM(require("path"));
13468
+ import_path29 = __toESM(require("path"));
12926
13469
  import_zod15 = require("zod");
12927
13470
  init_router();
12928
13471
  init_swagger_parser();
@@ -12993,27 +13536,27 @@ var init_report = __esm({
12993
13536
  });
12994
13537
 
12995
13538
  // src/storage/baseline-store.ts
12996
- function baselinesDir(rootDir) {
12997
- return import_path28.default.join(rootDir, "results", "baselines");
13539
+ function baselinesDir2(rootDir) {
13540
+ return import_path30.default.join(rootDir, "results", "baselines");
12998
13541
  }
12999
13542
  function baselinePath(rootDir, stepId) {
13000
- return import_path28.default.join(baselinesDir(rootDir), `${stepId}.png`);
13543
+ return import_path30.default.join(baselinesDir2(rootDir), `${stepId}.png`);
13001
13544
  }
13002
13545
  async function saveBaseline(rootDir, stepId, sourcePath) {
13003
- const dir = baselinesDir(rootDir);
13004
- await import_fs_extra21.default.ensureDir(dir);
13005
- await import_fs_extra21.default.copy(sourcePath, baselinePath(rootDir, stepId), { overwrite: true });
13546
+ const dir = baselinesDir2(rootDir);
13547
+ await import_fs_extra23.default.ensureDir(dir);
13548
+ await import_fs_extra23.default.copy(sourcePath, baselinePath(rootDir, stepId), { overwrite: true });
13006
13549
  }
13007
13550
  async function hasBaseline(rootDir, stepId) {
13008
- return import_fs_extra21.default.pathExists(baselinePath(rootDir, stepId));
13551
+ return import_fs_extra23.default.pathExists(baselinePath(rootDir, stepId));
13009
13552
  }
13010
- var import_path28, import_fs_extra21;
13553
+ var import_path30, import_fs_extra23;
13011
13554
  var init_baseline_store = __esm({
13012
13555
  "src/storage/baseline-store.ts"() {
13013
13556
  "use strict";
13014
13557
  init_cjs_shims();
13015
- import_path28 = __toESM(require("path"));
13016
- import_fs_extra21 = __toESM(require("fs-extra"));
13558
+ import_path30 = __toESM(require("path"));
13559
+ import_fs_extra23 = __toESM(require("fs-extra"));
13017
13560
  }
13018
13561
  });
13019
13562
 
@@ -13024,26 +13567,26 @@ async function mediaRoutes(fastify) {
13024
13567
  if (!["screenshots", "videos", "traces", "logs", "baselines"].includes(category)) {
13025
13568
  return reply.status(404).send({ error: "Unknown media category" });
13026
13569
  }
13027
- const safeName = import_path29.default.basename(filename);
13028
- const filePath = import_path29.default.join(fastify.rootDir, "results", category, safeName);
13029
- if (!await import_fs_extra22.default.pathExists(filePath)) {
13570
+ const safeName = import_path31.default.basename(filename);
13571
+ const filePath = import_path31.default.join(fastify.rootDir, "results", category, safeName);
13572
+ if (!await import_fs_extra24.default.pathExists(filePath)) {
13030
13573
  return reply.status(404).send({ error: `File not found: ${safeName}` });
13031
13574
  }
13032
- const ext = import_path29.default.extname(safeName).toLowerCase();
13575
+ const ext = import_path31.default.extname(safeName).toLowerCase();
13033
13576
  const mimeType = MIME[ext] ?? "application/octet-stream";
13034
13577
  if (ext === ".zip") {
13035
13578
  void reply.header("Content-Disposition", `attachment; filename="${safeName}"`);
13036
13579
  }
13037
- const stat = await import_fs_extra22.default.stat(filePath);
13580
+ const stat = await import_fs_extra24.default.stat(filePath);
13038
13581
  void reply.header("Content-Length", stat.size);
13039
- return reply.type(mimeType).send(import_fs_extra22.default.createReadStream(filePath));
13582
+ return reply.type(mimeType).send(import_fs_extra24.default.createReadStream(filePath));
13040
13583
  });
13041
13584
  fastify.post("/baselines/:stepId/capture", async (req, reply) => {
13042
13585
  try {
13043
13586
  const body = req.body;
13044
13587
  if (!body?.screenshotPath) return reply.status(400).send({ error: "screenshotPath required" });
13045
- const fullPath = import_path29.default.isAbsolute(body.screenshotPath) ? body.screenshotPath : import_path29.default.join(fastify.rootDir, body.screenshotPath);
13046
- if (!await import_fs_extra22.default.pathExists(fullPath)) return reply.status(404).send({ error: "Screenshot not found" });
13588
+ const fullPath = import_path31.default.isAbsolute(body.screenshotPath) ? body.screenshotPath : import_path31.default.join(fastify.rootDir, body.screenshotPath);
13589
+ if (!await import_fs_extra24.default.pathExists(fullPath)) return reply.status(404).send({ error: "Screenshot not found" });
13047
13590
  await saveBaseline(fastify.rootDir, req.params.stepId, fullPath);
13048
13591
  return reply.send({ saved: true });
13049
13592
  } catch (err) {
@@ -13054,21 +13597,21 @@ async function mediaRoutes(fastify) {
13054
13597
  try {
13055
13598
  const exists = await hasBaseline(fastify.rootDir, req.params.stepId);
13056
13599
  if (!exists) return reply.send({ hasBaseline: false });
13057
- const filePath = import_path29.default.join(fastify.rootDir, "results", "baselines", `${req.params.stepId}.png`);
13058
- const stat = await import_fs_extra22.default.stat(filePath).catch(() => null);
13600
+ const filePath = import_path31.default.join(fastify.rootDir, "results", "baselines", `${req.params.stepId}.png`);
13601
+ const stat = await import_fs_extra24.default.stat(filePath).catch(() => null);
13059
13602
  return reply.send({ hasBaseline: true, capturedAt: stat?.mtimeMs ?? Date.now() });
13060
13603
  } catch (err) {
13061
13604
  return sendError(reply, 500, "Failed to check baseline", err);
13062
13605
  }
13063
13606
  });
13064
13607
  }
13065
- var import_path29, import_fs_extra22, MIME;
13608
+ var import_path31, import_fs_extra24, MIME;
13066
13609
  var init_media = __esm({
13067
13610
  "src/server/routes/media.ts"() {
13068
13611
  "use strict";
13069
13612
  init_cjs_shims();
13070
- import_path29 = __toESM(require("path"));
13071
- import_fs_extra22 = __toESM(require("fs-extra"));
13613
+ import_path31 = __toESM(require("path"));
13614
+ import_fs_extra24 = __toESM(require("fs-extra"));
13072
13615
  init_baseline_store();
13073
13616
  init_utils2();
13074
13617
  MIME = {
@@ -13112,8 +13655,8 @@ var init_client = __esm({
13112
13655
  this.authHeader = "Basic " + Buffer.from(`${config.jiraEmail}:${config.jiraApiToken}`).toString("base64");
13113
13656
  }
13114
13657
  // ─── Jira REST API helpers ──────────────────────────────────────────────────
13115
- async jiraRequest(method, path37, body) {
13116
- const url = `${this.config.jiraBaseUrl}/rest/api/3${path37}`;
13658
+ async jiraRequest(method, path41, body) {
13659
+ const url = `${this.config.jiraBaseUrl}/rest/api/3${path41}`;
13117
13660
  logger36.debug({ method, url }, "Jira API request");
13118
13661
  const headers = {
13119
13662
  "Authorization": this.authHeader,
@@ -13130,7 +13673,7 @@ var init_client = __esm({
13130
13673
  if (!res.ok) {
13131
13674
  const text = await res.text();
13132
13675
  logger36.error({ status: res.status, url, body: text }, "Jira API error");
13133
- throw new Error(`Jira API ${method} ${path37} \u2192 ${res.status}: ${text}`);
13676
+ throw new Error(`Jira API ${method} ${path41} \u2192 ${res.status}: ${text}`);
13134
13677
  }
13135
13678
  if (res.status === 204) return void 0;
13136
13679
  return res.json();
@@ -13157,10 +13700,10 @@ var init_client = __esm({
13157
13700
  return this.xrayToken;
13158
13701
  }
13159
13702
  // ─── Xray API helpers ─────────────────────────────────────────────────────
13160
- async xrayRequest(method, path37, body) {
13703
+ async xrayRequest(method, path41, body) {
13161
13704
  const token = await this.getXrayToken();
13162
13705
  const baseUrl = token ? "https://xray.cloud.getxray.app/api/v2" : `${this.config.jiraBaseUrl}/rest/raven/2.0/api`;
13163
- const url = `${baseUrl}${path37}`;
13706
+ const url = `${baseUrl}${path41}`;
13164
13707
  logger36.debug({ method, url }, "Xray API request");
13165
13708
  const headers = {
13166
13709
  "Accept": "application/json"
@@ -13181,7 +13724,7 @@ var init_client = __esm({
13181
13724
  if (!res.ok) {
13182
13725
  const text = await res.text();
13183
13726
  logger36.error({ status: res.status, url, body: text }, "Xray API error");
13184
- throw new Error(`Xray API ${method} ${path37} \u2192 ${res.status}: ${text}`);
13727
+ throw new Error(`Xray API ${method} ${path41} \u2192 ${res.status}: ${text}`);
13185
13728
  }
13186
13729
  if (res.status === 204) return void 0;
13187
13730
  return res.json();
@@ -13768,11 +14311,11 @@ var init_xray = __esm({
13768
14311
 
13769
14312
  // src/storage/xray-store.ts
13770
14313
  function mappingPath(rootDir) {
13771
- return import_path30.default.join(rootDir, "assuremind-data", MAPPING_FILE);
14314
+ return import_path32.default.join(rootDir, "assuremind-data", MAPPING_FILE);
13772
14315
  }
13773
14316
  async function readXrayMapping(rootDir) {
13774
14317
  const filePath = mappingPath(rootDir);
13775
- if (!await import_fs_extra23.default.pathExists(filePath)) {
14318
+ if (!await import_fs_extra25.default.pathExists(filePath)) {
13776
14319
  return {
13777
14320
  projectKey: "",
13778
14321
  suites: [],
@@ -13789,7 +14332,7 @@ async function readXrayMapping(rootDir) {
13789
14332
  }
13790
14333
  async function writeXrayMapping(rootDir, mapping) {
13791
14334
  const filePath = mappingPath(rootDir);
13792
- await import_fs_extra23.default.ensureDir(import_path30.default.dirname(filePath));
14335
+ await import_fs_extra25.default.ensureDir(import_path32.default.dirname(filePath));
13793
14336
  const data = { ...mapping, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
13794
14337
  await atomicWriteJson(filePath, data);
13795
14338
  logger37.debug({ path: filePath }, "Xray mapping written");
@@ -13824,13 +14367,13 @@ function upsertCaseMapping(suiteMapping, caseMapping) {
13824
14367
  }
13825
14368
  return { ...suiteMapping, cases };
13826
14369
  }
13827
- var import_path30, import_fs_extra23, logger37, MAPPING_FILE;
14370
+ var import_path32, import_fs_extra25, logger37, MAPPING_FILE;
13828
14371
  var init_xray_store = __esm({
13829
14372
  "src/storage/xray-store.ts"() {
13830
14373
  "use strict";
13831
14374
  init_cjs_shims();
13832
- import_path30 = __toESM(require("path"));
13833
- import_fs_extra23 = __toESM(require("fs-extra"));
14375
+ import_path32 = __toESM(require("path"));
14376
+ import_fs_extra25 = __toESM(require("fs-extra"));
13834
14377
  init_xray();
13835
14378
  init_utils();
13836
14379
  init_logger();
@@ -14565,14 +15108,14 @@ var init_xray2 = __esm({
14565
15108
  });
14566
15109
 
14567
15110
  // src/git/service.ts
14568
- var import_simple_git, import_fs_extra24, import_path31, logger40, GitService;
15111
+ var import_simple_git, import_fs_extra26, import_path33, logger40, GitService;
14569
15112
  var init_service2 = __esm({
14570
15113
  "src/git/service.ts"() {
14571
15114
  "use strict";
14572
15115
  init_cjs_shims();
14573
15116
  import_simple_git = __toESM(require("simple-git"));
14574
- import_fs_extra24 = __toESM(require("fs-extra"));
14575
- import_path31 = __toESM(require("path"));
15117
+ import_fs_extra26 = __toESM(require("fs-extra"));
15118
+ import_path33 = __toESM(require("path"));
14576
15119
  init_logger();
14577
15120
  logger40 = createChildLogger("git-service");
14578
15121
  GitService = class {
@@ -14772,8 +15315,8 @@ var init_service2 = __esm({
14772
15315
  return status.conflicted;
14773
15316
  }
14774
15317
  async getFileConflictContent(filePath) {
14775
- const fullPath = import_path31.default.join(this.rootDir, filePath);
14776
- const content = await import_fs_extra24.default.readFile(fullPath, "utf-8");
15318
+ const fullPath = import_path33.default.join(this.rootDir, filePath);
15319
+ const content = await import_fs_extra26.default.readFile(fullPath, "utf-8");
14777
15320
  let ours = "";
14778
15321
  let theirs = "";
14779
15322
  let section = "none";
@@ -14799,8 +15342,8 @@ var init_service2 = __esm({
14799
15342
  return { ours: ours.trimEnd(), theirs: theirs.trimEnd() };
14800
15343
  }
14801
15344
  async resolveConflict(filePath, resolvedContent) {
14802
- const fullPath = import_path31.default.join(this.rootDir, filePath);
14803
- await import_fs_extra24.default.writeFile(fullPath, resolvedContent, "utf-8");
15345
+ const fullPath = import_path33.default.join(this.rootDir, filePath);
15346
+ await import_fs_extra26.default.writeFile(fullPath, resolvedContent, "utf-8");
14804
15347
  await this.git.add(filePath);
14805
15348
  logger40.info({ filePath }, "Conflict resolved");
14806
15349
  }
@@ -15114,7 +15657,7 @@ async function gitRoutes(fastify) {
15114
15657
  const changedFiles = stdout.split("\n").filter(Boolean);
15115
15658
  const { listSuiteDirs: listSuiteDirs2, readSuite: readSuite2 } = await Promise.resolve().then(() => (init_suite_store(), suite_store_exports));
15116
15659
  const { listCases: listCases2 } = await Promise.resolve().then(() => (init_case_store(), case_store_exports));
15117
- const testsDir = import_path32.default.join(rootDir, "tests");
15660
+ const testsDir = import_path34.default.join(rootDir, "tests");
15118
15661
  const suiteDirs = await listSuiteDirs2(testsDir);
15119
15662
  const impacted = [];
15120
15663
  for (const suiteDir of suiteDirs) {
@@ -15124,8 +15667,8 @@ async function gitRoutes(fastify) {
15124
15667
  for (const tc of cases) {
15125
15668
  const allText = [tc.name, tc.description ?? "", ...tc.steps.map((s) => s.instruction)].join(" ").toLowerCase();
15126
15669
  for (const file of changedFiles) {
15127
- const basename = import_path32.default.basename(file).toLowerCase();
15128
- const dirname = import_path32.default.dirname(file).toLowerCase();
15670
+ const basename = import_path34.default.basename(file).toLowerCase();
15671
+ const dirname = import_path34.default.dirname(file).toLowerCase();
15129
15672
  if (allText.includes(basename) || allText.includes(dirname)) {
15130
15673
  impacted.push({ caseId: tc.id, caseName: tc.name, suiteId: suite.id, suiteName: suite.name, reason: `Mentions "${file}"` });
15131
15674
  break;
@@ -15141,12 +15684,12 @@ async function gitRoutes(fastify) {
15141
15684
  }
15142
15685
  });
15143
15686
  }
15144
- var import_path32, import_zod18, logger42, CheckoutBody, CommitBody, PullBody, FullSyncBody, ResolveConflictBody, ResolveConflictAIBody;
15687
+ var import_path34, import_zod18, logger42, CheckoutBody, CommitBody, PullBody, FullSyncBody, ResolveConflictBody, ResolveConflictAIBody;
15145
15688
  var init_git = __esm({
15146
15689
  "src/server/routes/git.ts"() {
15147
15690
  "use strict";
15148
15691
  init_cjs_shims();
15149
- import_path32 = __toESM(require("path"));
15692
+ import_path34 = __toESM(require("path"));
15150
15693
  import_zod18 = require("zod");
15151
15694
  init_service2();
15152
15695
  init_ai();
@@ -15197,12 +15740,12 @@ function parseCSV(content) {
15197
15740
  }
15198
15741
  async function dataFileRoutes(fastify) {
15199
15742
  const rootDir = fastify.rootDir;
15200
- const dataDir = import_path33.default.join(rootDir, "tests", "data");
15201
- await import_fs_extra25.default.ensureDir(dataDir);
15743
+ const dataDir = import_path35.default.join(rootDir, "tests", "data");
15744
+ await import_fs_extra27.default.ensureDir(dataDir);
15202
15745
  fastify.get("/data-files", async (_req, reply) => {
15203
15746
  try {
15204
- await import_fs_extra25.default.ensureDir(dataDir);
15205
- const entries = await import_fs_extra25.default.readdir(dataDir, { withFileTypes: true });
15747
+ await import_fs_extra27.default.ensureDir(dataDir);
15748
+ const entries = await import_fs_extra27.default.readdir(dataDir, { withFileTypes: true });
15206
15749
  const files = entries.filter((e) => e.isFile() && ALLOWED_EXTENSIONS.some((ext) => e.name.endsWith(ext))).map((e) => ({
15207
15750
  name: e.name,
15208
15751
  type: e.name.endsWith(".csv") ? "csv" : "json",
@@ -15215,12 +15758,12 @@ async function dataFileRoutes(fastify) {
15215
15758
  });
15216
15759
  fastify.get("/data-files/:filename", async (req, reply) => {
15217
15760
  try {
15218
- const filename = import_path33.default.basename(req.params.filename);
15219
- const filePath = import_path33.default.join(dataDir, filename);
15220
- if (!await import_fs_extra25.default.pathExists(filePath)) {
15761
+ const filename = import_path35.default.basename(req.params.filename);
15762
+ const filePath = import_path35.default.join(dataDir, filename);
15763
+ if (!await import_fs_extra27.default.pathExists(filePath)) {
15221
15764
  return reply.status(404).send({ error: `Data file "${filename}" not found` });
15222
15765
  }
15223
- const raw = await import_fs_extra25.default.readFile(filePath, "utf8");
15766
+ const raw = await import_fs_extra27.default.readFile(filePath, "utf8");
15224
15767
  let rows;
15225
15768
  if (filename.endsWith(".csv")) {
15226
15769
  rows = parseCSV(raw);
@@ -15241,14 +15784,14 @@ async function dataFileRoutes(fastify) {
15241
15784
  if (!filename) {
15242
15785
  return reply.status(400).send({ error: "filename is required" });
15243
15786
  }
15244
- const ext = import_path33.default.extname(filename).toLowerCase();
15787
+ const ext = import_path35.default.extname(filename).toLowerCase();
15245
15788
  if (!ALLOWED_EXTENSIONS.includes(ext)) {
15246
15789
  return reply.status(400).send({ error: `Only ${ALLOWED_EXTENSIONS.join(", ")} files are allowed` });
15247
15790
  }
15248
- const safeName = import_path33.default.basename(filename);
15249
- const filePath = import_path33.default.join(dataDir, safeName);
15250
- await import_fs_extra25.default.ensureDir(dataDir);
15251
- await import_fs_extra25.default.writeFile(filePath, content, "utf8");
15791
+ const safeName = import_path35.default.basename(filename);
15792
+ const filePath = import_path35.default.join(dataDir, safeName);
15793
+ await import_fs_extra27.default.ensureDir(dataDir);
15794
+ await import_fs_extra27.default.writeFile(filePath, content, "utf8");
15252
15795
  let rowCount = 0;
15253
15796
  try {
15254
15797
  if (ext === ".csv") {
@@ -15271,12 +15814,12 @@ async function dataFileRoutes(fastify) {
15271
15814
  });
15272
15815
  fastify.put("/data-files/:filename", async (req, reply) => {
15273
15816
  try {
15274
- const filename = import_path33.default.basename(req.params.filename);
15275
- const filePath = import_path33.default.join(dataDir, filename);
15817
+ const filename = import_path35.default.basename(req.params.filename);
15818
+ const filePath = import_path35.default.join(dataDir, filename);
15276
15819
  const body = req.body;
15277
15820
  const content = String(body.content ?? "");
15278
- await import_fs_extra25.default.ensureDir(dataDir);
15279
- await import_fs_extra25.default.writeFile(filePath, content, "utf8");
15821
+ await import_fs_extra27.default.ensureDir(dataDir);
15822
+ await import_fs_extra27.default.writeFile(filePath, content, "utf8");
15280
15823
  logger43.info({ filename }, "Data file updated");
15281
15824
  return reply.send({ filename, path: `data/${filename}` });
15282
15825
  } catch (err) {
@@ -15285,12 +15828,12 @@ async function dataFileRoutes(fastify) {
15285
15828
  });
15286
15829
  fastify.delete("/data-files/:filename", async (req, reply) => {
15287
15830
  try {
15288
- const filename = import_path33.default.basename(req.params.filename);
15289
- const filePath = import_path33.default.join(dataDir, filename);
15290
- if (!await import_fs_extra25.default.pathExists(filePath)) {
15831
+ const filename = import_path35.default.basename(req.params.filename);
15832
+ const filePath = import_path35.default.join(dataDir, filename);
15833
+ if (!await import_fs_extra27.default.pathExists(filePath)) {
15291
15834
  return reply.status(404).send({ error: `Data file "${filename}" not found` });
15292
15835
  }
15293
- await import_fs_extra25.default.remove(filePath);
15836
+ await import_fs_extra27.default.remove(filePath);
15294
15837
  logger43.info({ filename }, "Data file deleted");
15295
15838
  return reply.status(204).send();
15296
15839
  } catch (err) {
@@ -15298,13 +15841,13 @@ async function dataFileRoutes(fastify) {
15298
15841
  }
15299
15842
  });
15300
15843
  }
15301
- var import_path33, import_fs_extra25, logger43, ALLOWED_EXTENSIONS;
15844
+ var import_path35, import_fs_extra27, logger43, ALLOWED_EXTENSIONS;
15302
15845
  var init_data_files = __esm({
15303
15846
  "src/server/routes/data-files.ts"() {
15304
15847
  "use strict";
15305
15848
  init_cjs_shims();
15306
- import_path33 = __toESM(require("path"));
15307
- import_fs_extra25 = __toESM(require("fs-extra"));
15849
+ import_path35 = __toESM(require("path"));
15850
+ import_fs_extra27 = __toESM(require("fs-extra"));
15308
15851
  init_utils2();
15309
15852
  init_logger();
15310
15853
  logger43 = createChildLogger("routes/data-files");
@@ -15326,7 +15869,7 @@ function matchWeight(matchedOn) {
15326
15869
  }
15327
15870
  }
15328
15871
  async function searchRoutes(fastify) {
15329
- const testsDir = import_path34.default.join(fastify.rootDir, "tests");
15872
+ const testsDir = import_path36.default.join(fastify.rootDir, "tests");
15330
15873
  fastify.get("/search", async (req, reply) => {
15331
15874
  const q = (req.query.q ?? "").trim();
15332
15875
  if (q.length < 2) {
@@ -15388,12 +15931,12 @@ async function searchRoutes(fastify) {
15388
15931
  }
15389
15932
  });
15390
15933
  }
15391
- var import_path34;
15934
+ var import_path36;
15392
15935
  var init_search = __esm({
15393
15936
  "src/server/routes/search.ts"() {
15394
15937
  "use strict";
15395
15938
  init_cjs_shims();
15396
- import_path34 = __toESM(require("path"));
15939
+ import_path36 = __toESM(require("path"));
15397
15940
  init_suite_store();
15398
15941
  init_case_store();
15399
15942
  init_utils2();
@@ -15674,13 +16217,13 @@ var init_flakiness = __esm({
15674
16217
 
15675
16218
  // src/storage/step-library-store.ts
15676
16219
  function libraryPath(rootDir) {
15677
- return import_path35.default.join(rootDir, "tests", ".step-library.json");
16220
+ return import_path37.default.join(rootDir, "tests", ".step-library.json");
15678
16221
  }
15679
16222
  async function readLibrary(rootDir) {
15680
16223
  const filePath = libraryPath(rootDir);
15681
- if (!await import_fs_extra26.default.pathExists(filePath)) return [];
16224
+ if (!await import_fs_extra28.default.pathExists(filePath)) return [];
15682
16225
  try {
15683
- const raw = await import_fs_extra26.default.readJson(filePath);
16226
+ const raw = await import_fs_extra28.default.readJson(filePath);
15684
16227
  if (!Array.isArray(raw)) return [];
15685
16228
  return raw.map((item) => LibraryStepSchema.parse(item));
15686
16229
  } catch {
@@ -15689,7 +16232,7 @@ async function readLibrary(rootDir) {
15689
16232
  }
15690
16233
  async function writeLibrary(rootDir, steps) {
15691
16234
  const filePath = libraryPath(rootDir);
15692
- await import_fs_extra26.default.ensureDir(import_path35.default.dirname(filePath));
16235
+ await import_fs_extra28.default.ensureDir(import_path37.default.dirname(filePath));
15693
16236
  await atomicWriteJson(filePath, steps);
15694
16237
  }
15695
16238
  async function addStep(rootDir, data) {
@@ -15721,13 +16264,13 @@ async function deleteStep(rootDir, id) {
15721
16264
  const filtered = steps.filter((s) => s.id !== id);
15722
16265
  await writeLibrary(rootDir, filtered);
15723
16266
  }
15724
- var import_path35, import_fs_extra26, import_uuid3, import_zod20, LibraryStepSchema;
16267
+ var import_path37, import_fs_extra28, import_uuid3, import_zod20, LibraryStepSchema;
15725
16268
  var init_step_library_store = __esm({
15726
16269
  "src/storage/step-library-store.ts"() {
15727
16270
  "use strict";
15728
16271
  init_cjs_shims();
15729
- import_path35 = __toESM(require("path"));
15730
- import_fs_extra26 = __toESM(require("fs-extra"));
16272
+ import_path37 = __toESM(require("path"));
16273
+ import_fs_extra28 = __toESM(require("fs-extra"));
15731
16274
  import_uuid3 = require("uuid");
15732
16275
  import_zod20 = require("zod");
15733
16276
  init_utils();
@@ -15867,26 +16410,44 @@ function buildCaptureScript() {
15867
16410
  }, 200);
15868
16411
 
15869
16412
  // \u2500\u2500 Detect iframe context \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
15870
- // When running inside an iframe, compute a selector for the iframe element
15871
- // as seen from the parent frame. This lets Node.js use page.frameLocator().
15872
16413
  var _frameSelector = '';
15873
16414
  try {
15874
16415
  if (window !== window.top && window.frameElement) {
15875
16416
  var iframe = window.frameElement;
15876
- if (iframe.id) { _frameSelector = '#' + iframe.id; }
15877
- else if (iframe.getAttribute('name')) { _frameSelector = 'iframe[name="' + iframe.getAttribute('name') + '"]'; }
15878
- else if (iframe.getAttribute('data-testid')) { _frameSelector = 'iframe[data-testid="' + iframe.getAttribute('data-testid') + '"]'; }
15879
- else if (iframe.getAttribute('src')) {
15880
- // Use src but strip query params for stability
16417
+ if (iframe.id) {
16418
+ _frameSelector = '#' + CSS.escape(iframe.id);
16419
+ } else if (iframe.getAttribute('name')) {
16420
+ _frameSelector = 'iframe[name="' + iframe.getAttribute('name') + '"]';
16421
+ } else if (iframe.getAttribute('data-testid')) {
16422
+ _frameSelector = 'iframe[data-testid="' + iframe.getAttribute('data-testid') + '"]';
16423
+ } else if (iframe.getAttribute('src')) {
15881
16424
  var src = iframe.getAttribute('src').split('?')[0];
15882
16425
  _frameSelector = 'iframe[src*="' + src + '"]';
16426
+ } else {
16427
+ // Positional fallback \u2014 find this iframe's index among all iframes
16428
+ var allIframes = document.querySelectorAll ? [] : [];
16429
+ try {
16430
+ var parent = iframe.ownerDocument;
16431
+ if (parent) {
16432
+ var iframeList = parent.querySelectorAll('iframe');
16433
+ var idx = Array.from(iframeList).indexOf(iframe);
16434
+ _frameSelector = idx >= 0 ? 'iframe:nth-of-type(' + (idx + 1) + ')' : 'iframe';
16435
+ }
16436
+ } catch(pe) { _frameSelector = 'iframe'; }
15883
16437
  }
15884
- else { _frameSelector = 'iframe'; }
15885
16438
  }
15886
- } catch(e) {
15887
- // Cross-origin iframe \u2014 frameElement is null, try parent postMessage approach
15888
- // For same-origin iframes this works; cross-origin will use fallback
15889
- _frameSelector = '';
16439
+ } catch(e) { _frameSelector = ''; }
16440
+
16441
+ // \u2500\u2500 Shadow DOM detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
16442
+ // Returns "host-selector >> inner-selector" when element is inside a shadow root.
16443
+ function getShadowPath(el) {
16444
+ var root = el.getRootNode();
16445
+ if (!root || !(root instanceof ShadowRoot)) return null;
16446
+ var host = root.host;
16447
+ if (!host) return null;
16448
+ var hostSel = cssPath(host);
16449
+ var innerSel = cssPathLocal(el, root);
16450
+ return hostSel + ' >> ' + innerSel;
15890
16451
  }
15891
16452
 
15892
16453
  // \u2500\u2500 Collect element metadata \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -15910,23 +16471,28 @@ function buildCaptureScript() {
15910
16471
  cssPath: cssPath(el),
15911
16472
  classes: Array.from(el.classList || []).join(' '),
15912
16473
  };
15913
- if (_frameSelector) { meta.frameSelector = _frameSelector; }
16474
+ if (_frameSelector) meta.frameSelector = _frameSelector;
16475
+ // Shadow DOM: record the pierce path
16476
+ var sp = getShadowPath(el);
16477
+ if (sp) meta.shadowPath = sp;
15914
16478
  return meta;
15915
16479
  }
15916
16480
 
15917
16481
  function getLabel(el) {
15918
- if (!el.id) {
15919
- var wrap = el.closest('label');
15920
- if (wrap) {
15921
- var clone = wrap.cloneNode(true);
15922
- clone.querySelectorAll('input,select,textarea').forEach(function(i) { i.remove(); });
15923
- var t = clone.textContent.trim();
15924
- if (t) return t;
15925
- }
15926
- return null;
16482
+ // Try in current root (works inside shadow roots too)
16483
+ var root = el.getRootNode() || document;
16484
+ if (el.id) {
16485
+ var lbl = root.querySelector('label[for="' + CSS.escape(el.id) + '"]');
16486
+ if (lbl) return lbl.textContent.trim();
16487
+ }
16488
+ var wrap = el.closest('label');
16489
+ if (wrap) {
16490
+ var clone = wrap.cloneNode(true);
16491
+ clone.querySelectorAll('input,select,textarea').forEach(function(i) { i.remove(); });
16492
+ var t = clone.textContent.trim();
16493
+ if (t) return t;
15927
16494
  }
15928
- var lbl = document.querySelector('label[for="' + CSS.escape(el.id) + '"]');
15929
- return lbl ? lbl.textContent.trim() : null;
16495
+ return null;
15930
16496
  }
15931
16497
 
15932
16498
  function visText(el) {
@@ -15934,13 +16500,33 @@ function buildCaptureScript() {
15934
16500
  return t.trim().replace(/\\s+/g, ' ').slice(0, 80);
15935
16501
  }
15936
16502
 
16503
+ // CSS path that stops at the nearest shadow root boundary (for shadow-dom inner selector)
16504
+ function cssPathLocal(el, root) {
16505
+ if (el.id) return '#' + CSS.escape(el.id);
16506
+ var parts = [], cur = el;
16507
+ for (var i = 0; i < 4 && cur && cur !== root && cur !== document.body; i++) {
16508
+ var sel = cur.tagName.toLowerCase();
16509
+ if (cur.id) { parts.unshift('#' + CSS.escape(cur.id)); break; }
16510
+ var cls = Array.from(cur.classList || []).filter(function(c) { return !/^(ng-|_|css-|sc-|jsx-|emotion)/.test(c); }).slice(0, 2);
16511
+ if (cls.length) sel += '.' + cls.map(function(c) { return CSS.escape(c); }).join('.');
16512
+ var p = cur.parentElement || (cur.parentNode !== root ? cur.parentNode : null);
16513
+ if (p && p.children) {
16514
+ var sibs = Array.from(p.children).filter(function(s) { return s.tagName === cur.tagName; });
16515
+ if (sibs.length > 1) sel += ':nth-child(' + (sibs.indexOf(cur) + 1) + ')';
16516
+ }
16517
+ parts.unshift(sel);
16518
+ cur = cur.parentElement;
16519
+ }
16520
+ return parts.join(' > ') || el.tagName.toLowerCase();
16521
+ }
16522
+
15937
16523
  function cssPath(el) {
15938
16524
  if (el.id) return '#' + CSS.escape(el.id);
15939
16525
  var parts = [], cur = el;
15940
16526
  for (var i = 0; i < 4 && cur && cur !== document.body; i++) {
15941
16527
  var sel = cur.tagName.toLowerCase();
15942
16528
  if (cur.id) { parts.unshift('#' + CSS.escape(cur.id)); break; }
15943
- var cls = Array.from(cur.classList).filter(function(c) { return !/^(ng-|_|css-|sc-|jsx-|emotion)/.test(c); }).slice(0, 2);
16529
+ var cls = Array.from(cur.classList || []).filter(function(c) { return !/^(ng-|_|css-|sc-|jsx-|emotion)/.test(c); }).slice(0, 2);
15944
16530
  if (cls.length) sel += '.' + cls.map(function(c) { return CSS.escape(c); }).join('.');
15945
16531
  var p = cur.parentElement;
15946
16532
  if (p) {
@@ -15953,7 +16539,7 @@ function buildCaptureScript() {
15953
16539
  return parts.join(' > ');
15954
16540
  }
15955
16541
 
15956
- // Human-readable element description for instructions
16542
+ // Human-readable element description
15957
16543
  function descEl(meta) {
15958
16544
  var t = meta.tag, ty = meta.type;
15959
16545
  var nm = meta.ariaLabel || meta.labelText || meta.innerText || meta.placeholder || meta.title || meta.testId;
@@ -15972,12 +16558,11 @@ function buildCaptureScript() {
15972
16558
  return 'the "' + (nm || t) + '" element';
15973
16559
  }
15974
16560
 
15975
- // Track the currently active input \u2014 only record its value when the user LEAVES the field
15976
- var activeInput = null; // the DOM element being typed into
15977
- var pendingInput = null; // the action payload to send
16561
+ // Track the currently active input \u2014 only record value when user LEAVES the field
16562
+ var activeInput = null;
16563
+ var pendingInput = null;
15978
16564
  function flushInput() {
15979
16565
  if (pendingInput && activeInput) {
15980
- // Grab the FINAL value at flush time, not the value when input event fired
15981
16566
  var meta = getMeta(activeInput);
15982
16567
  var desc = descEl(meta);
15983
16568
  pendingInput.instruction = 'Enter "' + activeInput.value + '" in ' + desc;
@@ -15989,7 +16574,7 @@ function buildCaptureScript() {
15989
16574
  activeInput = null;
15990
16575
  }
15991
16576
 
15992
- // Banner + styles (only in main frame \u2014 iframes don't need the banner)
16577
+ // Banner (main frame only)
15993
16578
  function ensureBanner() {
15994
16579
  if (window !== window.top) return;
15995
16580
  if (document.getElementById('__am-banner')) return;
@@ -15999,7 +16584,7 @@ function buildCaptureScript() {
15999
16584
  var b = document.createElement('div');
16000
16585
  b.id = '__am-banner';
16001
16586
  b.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:2147483647;background:linear-gradient(90deg,#1e1e2e,#2d1b3d);color:#fff;font:13px system-ui,sans-serif;padding:6px 16px;display:flex;align-items:center;box-shadow:0 2px 8px rgba(0,0,0,0.3);gap:12px;';
16002
- b.innerHTML = '<span style="display:inline-flex;align-items:center;gap:6px"><span style="width:8px;height:8px;border-radius:50%;background:#ef4444;animation:__amp 1.5s infinite"></span><b>Recording</b></span><span style="opacity:.65;font-size:12px">Shift+Click = assert &nbsp;|&nbsp; Ctrl+Shift+Click = soft assert &nbsp;|&nbsp; Ctrl+Shift+U = URL &nbsp;|&nbsp; Ctrl+Shift+T = title</span>';
16587
+ b.innerHTML = '<span style="display:inline-flex;align-items:center;gap:6px"><span style="width:8px;height:8px;border-radius:50%;background:#ef4444;animation:__amp 1.5s infinite"></span><b>Recording</b></span><span style="opacity:.65;font-size:12px">Shift+Click = assert &nbsp;|&nbsp; Ctrl+Shift+U = URL &nbsp;|&nbsp; Ctrl+Shift+T = title</span>';
16003
16588
  document.body.appendChild(b);
16004
16589
  }
16005
16590
  if (document.body) ensureBanner();
@@ -16008,27 +16593,31 @@ function buildCaptureScript() {
16008
16593
  // Hover highlight
16009
16594
  var lastH = null;
16010
16595
  document.addEventListener('mouseover', function(e) {
16011
- var el = e.target;
16596
+ var el = (e.composedPath && e.composedPath()[0]) || e.target;
16012
16597
  if (!el || !el.tagName || !el.classList) return;
16013
- if (el.id === '__am-banner' || el.closest('#__am-banner')) return;
16598
+ if (el.id === '__am-banner' || el.closest && el.closest('#__am-banner')) return;
16014
16599
  if (lastH && lastH !== el && lastH.classList) lastH.classList.remove('__am-hh');
16015
16600
  el.classList.add('__am-hh');
16016
16601
  lastH = el;
16017
16602
  }, true);
16018
16603
  document.addEventListener('mouseout', function(e) {
16019
- if (e.target && e.target.classList) e.target.classList.remove('__am-hh');
16604
+ var el = (e.composedPath && e.composedPath()[0]) || e.target;
16605
+ if (el && el.classList) el.classList.remove('__am-hh');
16020
16606
  }, true);
16021
16607
 
16022
16608
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
16023
- // EVENT LISTENERS \u2014 send raw metadata, Node.js resolves locators
16609
+ // EVENT LISTENERS
16610
+ // composedPath()[0] gives the real target inside shadow roots.
16611
+ // Regular e.target is re-targeted to the shadow host at the boundary.
16024
16612
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
16025
16613
 
16026
- // Click (normal = action, Shift = assert)
16614
+ // Click
16027
16615
  document.addEventListener('click', function(e) {
16028
16616
  flushInput();
16029
- var el = e.target;
16617
+ // Use composedPath to pierce shadow DOM \u2014 get the real target element
16618
+ var el = (e.composedPath && e.composedPath()[0]) || e.target;
16030
16619
  if (!el || !el.tagName) return;
16031
- if (el.id === '__am-banner' || el.closest('#__am-banner')) return;
16620
+ if (el.id === '__am-banner' || (el.closest && el.closest('#__am-banner'))) return;
16032
16621
 
16033
16622
  var meta = getMeta(el);
16034
16623
  var desc = descEl(meta);
@@ -16039,7 +16628,6 @@ function buildCaptureScript() {
16039
16628
  var isSoft = e.ctrlKey || e.metaKey;
16040
16629
  var prefix = isSoft ? 'Soft verify ' : 'Verify ';
16041
16630
  if (el.classList) { el.classList.add('__am-ah'); setTimeout(function() { el.classList.remove('__am-ah'); }, 1200); }
16042
-
16043
16631
  var inst = '';
16044
16632
  if (meta.tag === 'input' || meta.tag === 'textarea') {
16045
16633
  inst = meta.value ? prefix + desc + ' has value "' + meta.value + '"' : prefix + desc + ' is visible';
@@ -16052,7 +16640,7 @@ function buildCaptureScript() {
16052
16640
  return;
16053
16641
  }
16054
16642
 
16055
- // Normal click
16643
+ // Normal click \u2014 skip plain text inputs (handled by fill)
16056
16644
  if (meta.tag === 'input' && ['submit','button','checkbox','radio','file'].indexOf(meta.type) < 0) return;
16057
16645
  if (meta.tag === 'textarea' || meta.tag === 'option') return;
16058
16646
 
@@ -16062,28 +16650,28 @@ function buildCaptureScript() {
16062
16650
  safeSend(JSON.stringify({ action: action, instruction: instruction, meta: meta, url: location.href }));
16063
16651
  }, true);
16064
16652
 
16065
- // Input \u2014 just mark this element as actively being typed into (NO timer, NO intermediate captures)
16653
+ // Input \u2014 mark element as active; capture final value on blur/Enter
16066
16654
  document.addEventListener('input', function(e) {
16067
- var el = e.target; if (!el || !el.tagName) return;
16068
- if (el.tagName.toLowerCase() !== 'input' && el.tagName.toLowerCase() !== 'textarea') return;
16655
+ var el = (e.composedPath && e.composedPath()[0]) || e.target;
16656
+ if (!el || !el.tagName) return;
16657
+ var t = el.tagName.toLowerCase();
16658
+ if (t !== 'input' && t !== 'textarea') return;
16069
16659
  var ty = (el.getAttribute('type') || '').toLowerCase();
16070
16660
  if (['checkbox','radio','submit','button','file'].indexOf(ty) >= 0) return;
16071
- // Just mark as active \u2014 we'll capture the FINAL value on blur/click/Enter
16072
16661
  activeInput = el;
16073
16662
  pendingInput = { action: 'fill', instruction: '', meta: null, value: '', url: location.href };
16074
16663
  }, true);
16075
16664
 
16076
- // Blur \u2014 user left the field \u2192 capture the FINAL value
16665
+ // Blur \u2014 user left the field \u2192 capture final value
16077
16666
  document.addEventListener('blur', function(e) {
16078
- var el = e.target;
16079
- if (el && el === activeInput && pendingInput) {
16080
- flushInput();
16081
- }
16667
+ var el = (e.composedPath && e.composedPath()[0]) || e.target;
16668
+ if (el && el === activeInput && pendingInput) flushInput();
16082
16669
  }, true);
16083
16670
 
16084
16671
  // Select change
16085
16672
  document.addEventListener('change', function(e) {
16086
- var el = e.target; if (!el || el.tagName.toLowerCase() !== 'select') return;
16673
+ var el = (e.composedPath && e.composedPath()[0]) || e.target;
16674
+ if (!el || el.tagName.toLowerCase() !== 'select') return;
16087
16675
  flushInput();
16088
16676
  var meta = getMeta(el), desc = descEl(meta);
16089
16677
  var selText = el.options[el.selectedIndex] ? el.options[el.selectedIndex].text : el.value;
@@ -16092,6 +16680,7 @@ function buildCaptureScript() {
16092
16680
 
16093
16681
  // Keyboard
16094
16682
  document.addEventListener('keydown', function(e) {
16683
+ // \u2500\u2500 Assertion shortcuts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
16095
16684
  if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'u') {
16096
16685
  e.preventDefault(); e.stopPropagation(); flushInput();
16097
16686
  var b = document.getElementById('__am-banner');
@@ -16107,21 +16696,67 @@ function buildCaptureScript() {
16107
16696
  safeSend(JSON.stringify({ action: 'assert', instruction: 'Verify the page title is "' + document.title + '"', meta: null, value: document.title, url: location.href }));
16108
16697
  return;
16109
16698
  }
16699
+
16700
+ var el = (e.composedPath && e.composedPath()[0]) || e.target;
16701
+ var tag = el && el.tagName ? el.tagName.toLowerCase() : '';
16702
+ var isInput = tag === 'input' || tag === 'textarea';
16703
+
16704
+ // \u2500\u2500 Enter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
16110
16705
  if (e.key === 'Enter') {
16111
16706
  flushInput();
16112
- var el = e.target;
16113
- if (el && el.tagName) {
16114
- var t = el.tagName.toLowerCase();
16115
- if (t === 'input' || t === 'textarea') {
16116
- var meta = getMeta(el), desc = descEl(meta);
16117
- safeSend(JSON.stringify({ action: 'press', instruction: 'Press Enter on ' + desc, meta: meta, value: 'Enter', url: location.href }));
16118
- }
16707
+ if (isInput) {
16708
+ var meta = getMeta(el), desc = descEl(meta);
16709
+ safeSend(JSON.stringify({ action: 'press', instruction: 'Press Enter on ' + desc, meta: meta, value: 'Enter', url: location.href }));
16119
16710
  }
16120
- } else if (e.key === 'Escape') {
16711
+ return;
16712
+ }
16713
+
16714
+ // \u2500\u2500 Escape \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
16715
+ if (e.key === 'Escape') {
16121
16716
  flushInput();
16122
16717
  safeSend(JSON.stringify({ action: 'press', instruction: 'Press Escape', meta: null, value: 'Escape', url: location.href }));
16123
- } else if (e.key === 'Tab') {
16718
+ return;
16719
+ }
16720
+
16721
+ // \u2500\u2500 Tab \u2014 flush active input, then record the Tab key press \u2500\u2500\u2500\u2500\u2500\u2500
16722
+ if (e.key === 'Tab') {
16124
16723
  flushInput();
16724
+ var tabKey = e.shiftKey ? 'Shift+Tab' : 'Tab';
16725
+ if (isInput) {
16726
+ var meta2 = getMeta(el), desc2 = descEl(meta2);
16727
+ safeSend(JSON.stringify({ action: 'press', instruction: 'Press ' + tabKey + ' on ' + desc2, meta: meta2, value: tabKey, url: location.href }));
16728
+ }
16729
+ return;
16730
+ }
16731
+
16732
+ // \u2500\u2500 Keyboard shortcuts (Ctrl/Meta + key) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
16733
+ if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
16734
+ var shortcutMap = { a: 'Control+A', c: 'Control+C', v: 'Control+V', x: 'Control+X', z: 'Control+Z', y: 'Control+Y' };
16735
+ var k = e.key.toLowerCase();
16736
+ if (shortcutMap[k]) {
16737
+ // Only record Ctrl+A (select all) explicitly \u2014 copy/paste/undo are usually not test steps
16738
+ if (k === 'a') {
16739
+ flushInput();
16740
+ safeSend(JSON.stringify({ action: 'press', instruction: 'Press Ctrl+A to select all', meta: isInput ? getMeta(el) : null, value: 'Control+A', url: location.href }));
16741
+ }
16742
+ return; // don't treat as a character key
16743
+ }
16744
+ }
16745
+
16746
+ // \u2500\u2500 Arrow keys (only when NOT inside an input \u2014 avoids noise) \u2500\u2500\u2500
16747
+ if (['ArrowDown','ArrowUp','ArrowLeft','ArrowRight'].indexOf(e.key) >= 0 && !isInput) {
16748
+ safeSend(JSON.stringify({ action: 'press', instruction: 'Press ' + e.key, meta: null, value: e.key, url: location.href }));
16749
+ return;
16750
+ }
16751
+
16752
+ // \u2500\u2500 Space on buttons / interactive non-inputs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
16753
+ if (e.key === ' ' && !isInput && el && ['button','a','[role=button]'].some(function(s) {
16754
+ return tag === s.replace('[role=button]','') || (el.getAttribute && el.getAttribute('role') === 'button');
16755
+ })) {
16756
+ flushInput();
16757
+ var meta3 = getMeta(el), desc3 = descEl(meta3);
16758
+ safeSend(JSON.stringify({ action: 'press', instruction: 'Press Space on ' + desc3, meta: meta3, value: ' ', url: location.href }));
16759
+ return;
16125
16760
  }
16126
16761
  }, true);
16127
16762
  })();
@@ -16129,6 +16764,10 @@ function buildCaptureScript() {
16129
16764
  }
16130
16765
  async function resolveLocator(page, meta) {
16131
16766
  const candidates = [];
16767
+ if (meta.shadowPath) {
16768
+ const framePrefix2 = meta.frameSelector ? `frameLocator('${esc(meta.frameSelector)}').` : "";
16769
+ return `${framePrefix2}locator('${esc(meta.shadowPath)}')`;
16770
+ }
16132
16771
  const inIframe = !!meta.frameSelector;
16133
16772
  const framePrefix = inIframe ? `frameLocator('${esc(meta.frameSelector)}').` : "";
16134
16773
  const base = inIframe ? page.frameLocator(meta.frameSelector) : page;
@@ -16353,6 +16992,21 @@ var init_recorder = __esm({
16353
16992
  this._setLastUserActionTime = (t) => {
16354
16993
  lastUserActionTime = t;
16355
16994
  };
16995
+ this.page.on("dialog", async (dialog) => {
16996
+ if (!this.running) return;
16997
+ const dialogType = dialog.type();
16998
+ const message = dialog.message();
16999
+ await dialog.accept().catch(() => {
17000
+ });
17001
+ for (let i = this.actions.length - 1; i >= 0; i--) {
17002
+ const a = this.actions[i];
17003
+ if (["click", "fill", "check", "uncheck", "select", "press"].includes(a.action)) {
17004
+ a.dialogHandler = { type: "accept", dialogType, message };
17005
+ break;
17006
+ }
17007
+ }
17008
+ logger44.debug({ dialogType, message }, "Dialog auto-accepted and tagged on triggering action");
17009
+ });
16356
17010
  this.page.on("close", () => {
16357
17011
  if (this.running) {
16358
17012
  logger44.info("Browser closed by user \u2014 stopping recorder");
@@ -16410,7 +17064,8 @@ var init_recorder = __esm({
16410
17064
  url: parsed.url,
16411
17065
  timestamp: Date.now(),
16412
17066
  ...parsed.soft ? { soft: true } : {},
16413
- ...meta?.frameSelector ? { frameSelector: meta.frameSelector } : {}
17067
+ ...meta?.frameSelector ? { frameSelector: meta.frameSelector } : {},
17068
+ ...meta?.shadowPath ? { shadowPath: meta.shadowPath } : {}
16414
17069
  };
16415
17070
  this.actions.push(action);
16416
17071
  this.options.onAction(action);
@@ -16519,6 +17174,564 @@ var init_recorder2 = __esm({
16519
17174
  }
16520
17175
  });
16521
17176
 
17177
+ // src/server/routes/faker.ts
17178
+ var fakerRoutes;
17179
+ var init_faker2 = __esm({
17180
+ "src/server/routes/faker.ts"() {
17181
+ "use strict";
17182
+ init_cjs_shims();
17183
+ init_data_generator();
17184
+ fakerRoutes = async (app) => {
17185
+ app.get("/faker/generators", async (_req, reply) => {
17186
+ const generators = getAvailableGenerators();
17187
+ return reply.send(generators);
17188
+ });
17189
+ };
17190
+ }
17191
+ });
17192
+
17193
+ // src/server/routes/visual-regression.ts
17194
+ async function visualRegressionRoutes(fastify) {
17195
+ const rootDir = fastify.rootDir;
17196
+ fastify.get("/visual-regression/config", async (_req, reply) => {
17197
+ try {
17198
+ const config = await readVisualConfig(rootDir);
17199
+ return reply.send(config);
17200
+ } catch (err) {
17201
+ return sendError(reply, 500, "Failed to read visual-regression config", err);
17202
+ }
17203
+ });
17204
+ fastify.put("/visual-regression/config", async (req, reply) => {
17205
+ try {
17206
+ const body = req.body;
17207
+ const current = await readVisualConfig(rootDir);
17208
+ const updated = {
17209
+ enabled: typeof body.enabled === "boolean" ? body.enabled : current.enabled,
17210
+ defaultThreshold: typeof body.defaultThreshold === "number" ? body.defaultThreshold : current.defaultThreshold,
17211
+ fullPage: typeof body.fullPage === "boolean" ? body.fullPage : current.fullPage,
17212
+ maskSelectors: Array.isArray(body.maskSelectors) ? body.maskSelectors : current.maskSelectors
17213
+ };
17214
+ await writeVisualConfig(rootDir, updated);
17215
+ return reply.send(updated);
17216
+ } catch (err) {
17217
+ return sendError(reply, 500, "Failed to save visual-regression config", err);
17218
+ }
17219
+ });
17220
+ fastify.get("/visual-regression/baselines", async (_req, reply) => {
17221
+ try {
17222
+ const baselines = await listBaselines(rootDir);
17223
+ return reply.send(baselines);
17224
+ } catch (err) {
17225
+ return sendError(reply, 500, "Failed to list baselines", err);
17226
+ }
17227
+ });
17228
+ fastify.delete("/visual-regression/baselines", async (req, reply) => {
17229
+ try {
17230
+ const { path: baselinePath2 } = req.body;
17231
+ if (!baselinePath2) {
17232
+ return reply.status(400).send({ error: 'Missing "path" in request body' });
17233
+ }
17234
+ await deleteBaseline(rootDir, baselinePath2);
17235
+ return reply.send({ ok: true });
17236
+ } catch (err) {
17237
+ return sendError(reply, 500, "Failed to delete baseline", err);
17238
+ }
17239
+ });
17240
+ fastify.post("/visual-regression/baselines/update", async (req, reply) => {
17241
+ try {
17242
+ const { actualPath, baselinePath: baselinePath2 } = req.body;
17243
+ if (!actualPath || !baselinePath2) {
17244
+ return reply.status(400).send({ error: "Missing actualPath or baselinePath" });
17245
+ }
17246
+ if (!await import_fs_extra29.default.pathExists(actualPath)) {
17247
+ return reply.status(404).send({ error: "Actual screenshot not found" });
17248
+ }
17249
+ await import_fs_extra29.default.ensureDir(import_path38.default.dirname(baselinePath2));
17250
+ await import_fs_extra29.default.copy(actualPath, baselinePath2, { overwrite: true });
17251
+ return reply.send({ ok: true });
17252
+ } catch (err) {
17253
+ return sendError(reply, 500, "Failed to update baseline", err);
17254
+ }
17255
+ });
17256
+ fastify.get("/visual-regression/diffs", async (_req, reply) => {
17257
+ try {
17258
+ const runs = await listDiffRuns(rootDir);
17259
+ return reply.send(runs);
17260
+ } catch (err) {
17261
+ return sendError(reply, 500, "Failed to list diff runs", err);
17262
+ }
17263
+ });
17264
+ fastify.get("/visual-regression/diffs/:runId", async (req, reply) => {
17265
+ try {
17266
+ const { runId } = req.params;
17267
+ const summary = await readDiffSummary(rootDir, runId);
17268
+ return reply.send(summary);
17269
+ } catch (err) {
17270
+ return sendError(reply, 500, "Failed to read diff summary", err);
17271
+ }
17272
+ });
17273
+ fastify.patch("/visual-regression/diffs/:runId/:stepId", async (req, reply) => {
17274
+ try {
17275
+ const { runId, stepId } = req.params;
17276
+ const { status } = req.body;
17277
+ if (!["approved", "rejected"].includes(status)) {
17278
+ return reply.status(400).send({ error: 'Status must be "approved" or "rejected"' });
17279
+ }
17280
+ const updated = await updateDiffStatus(rootDir, runId, stepId, status);
17281
+ return reply.send(updated);
17282
+ } catch (err) {
17283
+ return sendError(reply, 500, "Failed to update diff status", err);
17284
+ }
17285
+ });
17286
+ fastify.post("/visual-regression/diffs/:runId/approve-all", async (req, reply) => {
17287
+ try {
17288
+ const { runId } = req.params;
17289
+ const updated = await approveAllDiffs(rootDir, runId);
17290
+ return reply.send(updated);
17291
+ } catch (err) {
17292
+ return sendError(reply, 500, "Failed to approve all diffs", err);
17293
+ }
17294
+ });
17295
+ fastify.get("/visual-regression/image/*", async (req, reply) => {
17296
+ try {
17297
+ const rawPath = req.params["*"];
17298
+ if (!rawPath) {
17299
+ return reply.status(400).send({ error: "Missing image path" });
17300
+ }
17301
+ const imagePath = import_path38.default.resolve(rootDir, rawPath);
17302
+ if (!imagePath.startsWith(import_path38.default.resolve(rootDir))) {
17303
+ return reply.status(403).send({ error: "Forbidden" });
17304
+ }
17305
+ if (!await import_fs_extra29.default.pathExists(imagePath)) {
17306
+ return reply.status(404).send({ error: "Image not found" });
17307
+ }
17308
+ const ext = import_path38.default.extname(imagePath).toLowerCase();
17309
+ const mimeMap = {
17310
+ ".png": "image/png",
17311
+ ".jpg": "image/jpeg",
17312
+ ".jpeg": "image/jpeg",
17313
+ ".webp": "image/webp"
17314
+ };
17315
+ const contentType = mimeMap[ext] ?? "application/octet-stream";
17316
+ const buffer = await import_fs_extra29.default.readFile(imagePath);
17317
+ return reply.type(contentType).send(buffer);
17318
+ } catch (err) {
17319
+ return sendError(reply, 500, "Failed to serve image", err);
17320
+ }
17321
+ });
17322
+ }
17323
+ var import_fs_extra29, import_path38;
17324
+ var init_visual_regression = __esm({
17325
+ "src/server/routes/visual-regression.ts"() {
17326
+ "use strict";
17327
+ init_cjs_shims();
17328
+ import_fs_extra29 = __toESM(require("fs-extra"));
17329
+ import_path38 = __toESM(require("path"));
17330
+ init_utils2();
17331
+ init_visual_regression_store();
17332
+ }
17333
+ });
17334
+
17335
+ // src/storage/ci-store.ts
17336
+ function configPath2(rootDir) {
17337
+ return import_path39.default.join(rootDir, ".assuremind", "ci-integration.json");
17338
+ }
17339
+ function qualityGatesDir(rootDir) {
17340
+ return import_path39.default.join(rootDir, "results", "quality-gates");
17341
+ }
17342
+ function qualityGateResultPath(rootDir, runId) {
17343
+ return import_path39.default.join(qualityGatesDir(rootDir), `${runId}.json`);
17344
+ }
17345
+ async function readCiConfig(rootDir) {
17346
+ const filePath = configPath2(rootDir);
17347
+ if (!await import_fs_extra30.default.pathExists(filePath)) {
17348
+ return CiIntegrationConfigSchema.parse({});
17349
+ }
17350
+ try {
17351
+ const raw = await import_fs_extra30.default.readJson(filePath);
17352
+ return CiIntegrationConfigSchema.parse(raw);
17353
+ } catch {
17354
+ return CiIntegrationConfigSchema.parse({});
17355
+ }
17356
+ }
17357
+ async function writeCiConfig(rootDir, config) {
17358
+ const filePath = configPath2(rootDir);
17359
+ const validated = CiIntegrationConfigSchema.parse(config);
17360
+ await import_fs_extra30.default.ensureDir(import_path39.default.dirname(filePath));
17361
+ await atomicWriteJson(filePath, validated);
17362
+ }
17363
+ async function listQualityGateResults(rootDir) {
17364
+ const dir = qualityGatesDir(rootDir);
17365
+ if (!await import_fs_extra30.default.pathExists(dir)) return [];
17366
+ try {
17367
+ const files = await import_fs_extra30.default.readdir(dir);
17368
+ const jsonFiles = files.filter((f) => f.endsWith(".json")).sort().reverse();
17369
+ const results = [];
17370
+ for (const file of jsonFiles) {
17371
+ try {
17372
+ const raw = await import_fs_extra30.default.readJson(import_path39.default.join(dir, file));
17373
+ results.push(QualityGateResultSchema.parse(raw));
17374
+ } catch {
17375
+ }
17376
+ }
17377
+ return results;
17378
+ } catch {
17379
+ return [];
17380
+ }
17381
+ }
17382
+ async function readQualityGateResult(rootDir, runId) {
17383
+ const filePath = qualityGateResultPath(rootDir, runId);
17384
+ if (!await import_fs_extra30.default.pathExists(filePath)) return null;
17385
+ try {
17386
+ const raw = await import_fs_extra30.default.readJson(filePath);
17387
+ return QualityGateResultSchema.parse(raw);
17388
+ } catch {
17389
+ return null;
17390
+ }
17391
+ }
17392
+ var import_path39, import_fs_extra30, import_zod23, NotificationTrigger, SlackConfigSchema, EmailConfigSchema, TeamsConfigSchema, CustomWebhookConfigSchema, CiIntegrationConfigSchema, GateResultSchema, QualityGateResultSchema;
17393
+ var init_ci_store = __esm({
17394
+ "src/storage/ci-store.ts"() {
17395
+ "use strict";
17396
+ init_cjs_shims();
17397
+ import_path39 = __toESM(require("path"));
17398
+ import_fs_extra30 = __toESM(require("fs-extra"));
17399
+ import_zod23 = require("zod");
17400
+ init_utils();
17401
+ NotificationTrigger = import_zod23.z.enum(["every-run", "failure", "healing"]);
17402
+ SlackConfigSchema = import_zod23.z.object({
17403
+ enabled: import_zod23.z.boolean().default(false),
17404
+ webhookUrl: import_zod23.z.string().default(""),
17405
+ on: import_zod23.z.array(NotificationTrigger).default(["failure"])
17406
+ });
17407
+ EmailConfigSchema = import_zod23.z.object({
17408
+ enabled: import_zod23.z.boolean().default(false),
17409
+ recipients: import_zod23.z.array(import_zod23.z.string()).default([]),
17410
+ on: import_zod23.z.array(NotificationTrigger).default(["failure"])
17411
+ });
17412
+ TeamsConfigSchema = import_zod23.z.object({
17413
+ enabled: import_zod23.z.boolean().default(false),
17414
+ webhookUrl: import_zod23.z.string().default(""),
17415
+ on: import_zod23.z.array(NotificationTrigger).default(["failure"])
17416
+ });
17417
+ CustomWebhookConfigSchema = import_zod23.z.object({
17418
+ enabled: import_zod23.z.boolean().default(false),
17419
+ url: import_zod23.z.string().default(""),
17420
+ headers: import_zod23.z.record(import_zod23.z.string()).default({}),
17421
+ on: import_zod23.z.array(NotificationTrigger).default(["failure"])
17422
+ });
17423
+ CiIntegrationConfigSchema = import_zod23.z.object({
17424
+ prComments: import_zod23.z.object({
17425
+ enabled: import_zod23.z.boolean().default(false),
17426
+ provider: import_zod23.z.enum(["github", "gitlab", "azure-devops"]).default("github"),
17427
+ includeHealingSummary: import_zod23.z.boolean().default(true),
17428
+ includeVisualDiffs: import_zod23.z.boolean().default(false)
17429
+ }).default({}),
17430
+ qualityGate: import_zod23.z.object({
17431
+ enabled: import_zod23.z.boolean().default(true),
17432
+ minPassRate: import_zod23.z.number().min(0).max(100).default(95),
17433
+ maxFlakyRate: import_zod23.z.number().min(0).max(100).default(10),
17434
+ maxDuration: import_zod23.z.number().min(0).default(0),
17435
+ requireTags: import_zod23.z.array(import_zod23.z.string()).default([])
17436
+ }).default({}),
17437
+ notifications: import_zod23.z.object({
17438
+ slack: SlackConfigSchema.default({}),
17439
+ email: EmailConfigSchema.default({}),
17440
+ teams: TeamsConfigSchema.default({}),
17441
+ customWebhook: CustomWebhookConfigSchema.default({})
17442
+ }).default({})
17443
+ });
17444
+ GateResultSchema = import_zod23.z.object({
17445
+ name: import_zod23.z.string(),
17446
+ passed: import_zod23.z.boolean(),
17447
+ expected: import_zod23.z.string(),
17448
+ actual: import_zod23.z.string()
17449
+ });
17450
+ QualityGateResultSchema = import_zod23.z.object({
17451
+ passed: import_zod23.z.boolean(),
17452
+ runId: import_zod23.z.string(),
17453
+ timestamp: import_zod23.z.string(),
17454
+ metrics: import_zod23.z.object({
17455
+ passRate: import_zod23.z.number(),
17456
+ totalTests: import_zod23.z.number(),
17457
+ passed: import_zod23.z.number(),
17458
+ failed: import_zod23.z.number(),
17459
+ skipped: import_zod23.z.number(),
17460
+ duration: import_zod23.z.number(),
17461
+ healingCount: import_zod23.z.number()
17462
+ }),
17463
+ gates: import_zod23.z.array(GateResultSchema),
17464
+ reason: import_zod23.z.string().optional()
17465
+ });
17466
+ }
17467
+ });
17468
+
17469
+ // src/engine/ci/notifier.ts
17470
+ function formatDuration(seconds) {
17471
+ const m = Math.floor(seconds / 60);
17472
+ const s = seconds % 60;
17473
+ return m > 0 ? `${m}m ${s}s` : `${s}s`;
17474
+ }
17475
+ function buildSlackPayload(runResult) {
17476
+ const total = runResult.passed + runResult.failed + runResult.skipped;
17477
+ const passRate = total > 0 ? (runResult.passed / total * 100).toFixed(1) : "0";
17478
+ const status = runResult.failed > 0 ? ":x: Failed" : ":white_check_mark: Passed";
17479
+ const failedTests = [];
17480
+ if (runResult.suiteResults) {
17481
+ for (const suite of runResult.suiteResults) {
17482
+ for (const c of suite.cases) {
17483
+ if (c.status === "failed") {
17484
+ failedTests.push(c.caseName);
17485
+ }
17486
+ }
17487
+ }
17488
+ }
17489
+ const blocks = [
17490
+ {
17491
+ type: "header",
17492
+ text: { type: "plain_text", text: "Assuremind Test Results", emoji: true }
17493
+ },
17494
+ {
17495
+ type: "section",
17496
+ fields: [
17497
+ { type: "mrkdwn", text: `*Status:*
17498
+ ${status}` },
17499
+ { type: "mrkdwn", text: `*Pass Rate:*
17500
+ ${passRate}% (${runResult.passed}/${total})` },
17501
+ { type: "mrkdwn", text: `*Failed:*
17502
+ ${runResult.failed}` },
17503
+ { type: "mrkdwn", text: `*Duration:*
17504
+ ${formatDuration(runResult.duration)}` }
17505
+ ]
17506
+ }
17507
+ ];
17508
+ if (failedTests.length > 0) {
17509
+ blocks.push({
17510
+ type: "section",
17511
+ text: {
17512
+ type: "mrkdwn",
17513
+ text: `*Failed Tests:*
17514
+ ${failedTests.map((t) => `\u2022 ${t}`).join("\n")}`
17515
+ }
17516
+ });
17517
+ }
17518
+ blocks.push({
17519
+ type: "context",
17520
+ elements: [{ type: "mrkdwn", text: ":robot_face: Generated by Assuremind" }]
17521
+ });
17522
+ return { blocks };
17523
+ }
17524
+ function buildTeamsPayload(runResult) {
17525
+ const total = runResult.passed + runResult.failed + runResult.skipped;
17526
+ const passRate = total > 0 ? (runResult.passed / total * 100).toFixed(1) : "0";
17527
+ const statusColor = runResult.failed > 0 ? "attention" : "good";
17528
+ const facts = [
17529
+ { title: "Pass Rate", value: `${passRate}% (${runResult.passed}/${total})` },
17530
+ { title: "Failed", value: `${runResult.failed}` },
17531
+ { title: "Skipped", value: `${runResult.skipped}` },
17532
+ { title: "Duration", value: formatDuration(runResult.duration) }
17533
+ ];
17534
+ return {
17535
+ type: "message",
17536
+ attachments: [
17537
+ {
17538
+ contentType: "application/vnd.microsoft.card.adaptive",
17539
+ content: {
17540
+ $schema: "http://adaptivecards.io/schemas/adaptive-card.json",
17541
+ type: "AdaptiveCard",
17542
+ version: "1.4",
17543
+ body: [
17544
+ {
17545
+ type: "TextBlock",
17546
+ text: "Assuremind Test Results",
17547
+ weight: "bolder",
17548
+ size: "medium",
17549
+ color: statusColor
17550
+ },
17551
+ {
17552
+ type: "FactSet",
17553
+ facts
17554
+ }
17555
+ ]
17556
+ }
17557
+ }
17558
+ ]
17559
+ };
17560
+ }
17561
+ async function sendSlack(webhookUrl, runResult) {
17562
+ const payload = buildSlackPayload(runResult);
17563
+ logger46.info("Sending Slack notification");
17564
+ const response = await fetch(webhookUrl, {
17565
+ method: "POST",
17566
+ headers: { "Content-Type": "application/json" },
17567
+ body: JSON.stringify(payload)
17568
+ });
17569
+ if (!response.ok) {
17570
+ const text = await response.text();
17571
+ logger46.error({ status: response.status, text }, "Failed to send Slack notification");
17572
+ } else {
17573
+ logger46.info("Slack notification sent");
17574
+ }
17575
+ }
17576
+ async function sendTeams(webhookUrl, runResult) {
17577
+ const payload = buildTeamsPayload(runResult);
17578
+ logger46.info("Sending Teams notification");
17579
+ const response = await fetch(webhookUrl, {
17580
+ method: "POST",
17581
+ headers: { "Content-Type": "application/json" },
17582
+ body: JSON.stringify(payload)
17583
+ });
17584
+ if (!response.ok) {
17585
+ const text = await response.text();
17586
+ logger46.error({ status: response.status, text }, "Failed to send Teams notification");
17587
+ } else {
17588
+ logger46.info("Teams notification sent");
17589
+ }
17590
+ }
17591
+ async function sendCustomWebhook(url, headers, runResult) {
17592
+ logger46.info({ url }, "Sending custom webhook notification");
17593
+ const response = await fetch(url, {
17594
+ method: "POST",
17595
+ headers: { "Content-Type": "application/json", ...headers },
17596
+ body: JSON.stringify({
17597
+ source: "assuremind",
17598
+ runId: runResult.runId,
17599
+ passed: runResult.passed,
17600
+ failed: runResult.failed,
17601
+ skipped: runResult.skipped,
17602
+ duration: runResult.duration,
17603
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
17604
+ })
17605
+ });
17606
+ if (!response.ok) {
17607
+ const text = await response.text();
17608
+ logger46.error({ status: response.status, text }, "Failed to send custom webhook notification");
17609
+ } else {
17610
+ logger46.info("Custom webhook notification sent");
17611
+ }
17612
+ }
17613
+ async function sendTestNotification(channel, config) {
17614
+ const dummyRun = {
17615
+ runId: "test-notification",
17616
+ passed: 10,
17617
+ failed: 1,
17618
+ skipped: 2,
17619
+ duration: 45,
17620
+ suiteResults: [
17621
+ {
17622
+ suiteId: "demo",
17623
+ suiteName: "Demo Suite",
17624
+ cases: [
17625
+ { caseId: "c1", caseName: "Test notification", status: "passed" },
17626
+ { caseId: "c2", caseName: "Example failed test", status: "failed", error: "This is a test notification" }
17627
+ ]
17628
+ }
17629
+ ]
17630
+ };
17631
+ try {
17632
+ switch (channel) {
17633
+ case "slack": {
17634
+ const { webhookUrl } = config.notifications.slack;
17635
+ if (!webhookUrl) return { ok: false, error: "Slack webhook URL not configured" };
17636
+ await sendSlack(webhookUrl, dummyRun);
17637
+ return { ok: true };
17638
+ }
17639
+ case "teams": {
17640
+ const { webhookUrl } = config.notifications.teams;
17641
+ if (!webhookUrl) return { ok: false, error: "Teams webhook URL not configured" };
17642
+ await sendTeams(webhookUrl, dummyRun);
17643
+ return { ok: true };
17644
+ }
17645
+ case "custom": {
17646
+ const { url, headers } = config.notifications.customWebhook;
17647
+ if (!url) return { ok: false, error: "Custom webhook URL not configured" };
17648
+ await sendCustomWebhook(url, headers, dummyRun);
17649
+ return { ok: true };
17650
+ }
17651
+ default:
17652
+ return { ok: false, error: `Unsupported channel: ${channel}` };
17653
+ }
17654
+ } catch (err) {
17655
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
17656
+ }
17657
+ }
17658
+ var logger46;
17659
+ var init_notifier = __esm({
17660
+ "src/engine/ci/notifier.ts"() {
17661
+ "use strict";
17662
+ init_cjs_shims();
17663
+ init_logger();
17664
+ logger46 = createChildLogger("notifier");
17665
+ }
17666
+ });
17667
+
17668
+ // src/server/routes/ci-integration.ts
17669
+ async function ciIntegrationRoutes(fastify) {
17670
+ const rootDir = fastify.rootDir;
17671
+ fastify.get("/ci-integration/config", async (_req, reply) => {
17672
+ try {
17673
+ const config = await readCiConfig(rootDir);
17674
+ return reply.send(config);
17675
+ } catch (err) {
17676
+ return sendError(reply, 500, "Failed to read CI integration config", err);
17677
+ }
17678
+ });
17679
+ fastify.put("/ci-integration/config", async (req, reply) => {
17680
+ try {
17681
+ const config = req.body;
17682
+ await writeCiConfig(rootDir, config);
17683
+ const saved = await readCiConfig(rootDir);
17684
+ return reply.send(saved);
17685
+ } catch (err) {
17686
+ return sendError(reply, 500, "Failed to save CI integration config", err);
17687
+ }
17688
+ });
17689
+ fastify.get("/ci-integration/quality-gates", async (_req, reply) => {
17690
+ try {
17691
+ const results = await listQualityGateResults(rootDir);
17692
+ return reply.send(results);
17693
+ } catch (err) {
17694
+ return sendError(reply, 500, "Failed to list quality gate results", err);
17695
+ }
17696
+ });
17697
+ fastify.get("/ci-integration/quality-gates/:runId", async (req, reply) => {
17698
+ try {
17699
+ const result = await readQualityGateResult(rootDir, req.params.runId);
17700
+ if (!result) return reply.status(404).send({ error: "Quality gate result not found" });
17701
+ return reply.send(result);
17702
+ } catch (err) {
17703
+ return sendError(reply, 500, "Failed to read quality gate result", err);
17704
+ }
17705
+ });
17706
+ fastify.post("/ci-integration/test-notification", async (req, reply) => {
17707
+ const parsed = TestNotificationBody.safeParse(req.body);
17708
+ if (!parsed.success) {
17709
+ return reply.status(400).send({ error: "Invalid request", details: parsed.error.issues });
17710
+ }
17711
+ try {
17712
+ const config = await readCiConfig(rootDir);
17713
+ const result = await sendTestNotification(parsed.data.channel, config);
17714
+ return reply.send(result);
17715
+ } catch (err) {
17716
+ return sendError(reply, 500, "Failed to send test notification", err);
17717
+ }
17718
+ });
17719
+ }
17720
+ var import_zod24, TestNotificationBody;
17721
+ var init_ci_integration = __esm({
17722
+ "src/server/routes/ci-integration.ts"() {
17723
+ "use strict";
17724
+ init_cjs_shims();
17725
+ import_zod24 = require("zod");
17726
+ init_ci_store();
17727
+ init_notifier();
17728
+ init_utils2();
17729
+ TestNotificationBody = import_zod24.z.object({
17730
+ channel: import_zod24.z.enum(["slack", "teams", "custom"])
17731
+ });
17732
+ }
17733
+ });
17734
+
16522
17735
  // src/server/index.ts
16523
17736
  var server_exports = {};
16524
17737
  __export(server_exports, {
@@ -16561,17 +17774,20 @@ async function startServer(options) {
16561
17774
  await fastify.register(flakinessRoutes, { prefix: "/api" });
16562
17775
  await fastify.register(stepLibraryRoutes, { prefix: "/api" });
16563
17776
  await fastify.register(recorderRoutes, { prefix: "/api" });
17777
+ await fastify.register(fakerRoutes, { prefix: "/api" });
17778
+ await fastify.register(visualRegressionRoutes, { prefix: "/api" });
17779
+ await fastify.register(ciIntegrationRoutes, { prefix: "/api" });
16564
17780
  await registerStatic(fastify, rootDir);
16565
17781
  await fastify.listen({ port, host: "127.0.0.1" });
16566
- logger46.info({ port, rootDir }, `Assuremind Studio listening on http://127.0.0.1:${port}`);
17782
+ logger47.info({ port, rootDir }, `Assuremind Studio listening on http://127.0.0.1:${port}`);
16567
17783
  return {
16568
17784
  async close() {
16569
17785
  await fastify.close();
16570
- logger46.info("Server closed");
17786
+ logger47.info("Server closed");
16571
17787
  }
16572
17788
  };
16573
17789
  }
16574
- var import_fastify, import_cors, import_websocket, logger46;
17790
+ var import_fastify, import_cors, import_websocket, logger47;
16575
17791
  var init_server = __esm({
16576
17792
  "src/server/index.ts"() {
16577
17793
  "use strict";
@@ -16602,8 +17818,11 @@ var init_server = __esm({
16602
17818
  init_flakiness();
16603
17819
  init_step_library();
16604
17820
  init_recorder2();
17821
+ init_faker2();
17822
+ init_visual_regression();
17823
+ init_ci_integration();
16605
17824
  init_logger();
16606
- logger46 = createChildLogger("server");
17825
+ logger47 = createChildLogger("server");
16607
17826
  }
16608
17827
  });
16609
17828
 
@@ -16669,7 +17888,21 @@ async function runStudio(options = {}) {
16669
17888
  });
16670
17889
  } catch (err) {
16671
17890
  spinner.fail("Failed to start server");
16672
- printError(err instanceof Error ? err.message : String(err));
17891
+ const msg = err instanceof Error ? err.message : String(err);
17892
+ if (msg.includes("EADDRINUSE")) {
17893
+ printError(`Port ${port} is already in use.`);
17894
+ printInfo(
17895
+ `Fix: kill the process using the port, then try again:
17896
+ npx kill-port ${port}
17897
+ npx assuremind studio --port ${port}
17898
+
17899
+ Or use a different port:
17900
+ npx assuremind studio --port ${port + 1}
17901
+ `
17902
+ );
17903
+ } else {
17904
+ printError(msg);
17905
+ }
16673
17906
  process.exit(1);
16674
17907
  }
16675
17908
  }
@@ -16723,7 +17956,7 @@ function validateRunOptions(options) {
16723
17956
  );
16724
17957
  }
16725
17958
  }
16726
- function formatDuration(ms) {
17959
+ function formatDuration2(ms) {
16727
17960
  if (ms < 1e3) return `${ms}ms`;
16728
17961
  if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
16729
17962
  const m = Math.floor(ms / 6e4);
@@ -16744,7 +17977,7 @@ function statusIcon(status) {
16744
17977
  }
16745
17978
  function printCaseResult(tc, ci) {
16746
17979
  const icon = statusIcon(tc.status);
16747
- const duration = colors.dim(`(${formatDuration(tc.duration)})`);
17980
+ const duration = colors.dim(`(${formatDuration2(tc.duration)})`);
16748
17981
  process.stdout.write(` ${icon} ${tc.caseName} ${duration}
16749
17982
  `);
16750
17983
  if (!ci || tc.status === "failed") {
@@ -16771,7 +18004,7 @@ function printCaseResult(tc, ci) {
16771
18004
  }
16772
18005
  function printSuiteResult(suite, ci) {
16773
18006
  const icon = statusIcon(suite.status);
16774
- const duration = colors.dim(`(${formatDuration(suite.duration)})`);
18007
+ const duration = colors.dim(`(${formatDuration2(suite.duration)})`);
16775
18008
  const browser = colors.dim(`[${suite.browser}]`);
16776
18009
  process.stdout.write(
16777
18010
  `
@@ -16783,7 +18016,7 @@ function printSuiteResult(suite, ci) {
16783
18016
  }
16784
18017
  }
16785
18018
  function printRunSummary(result, ci) {
16786
- const duration = formatDuration(result.duration);
18019
+ const duration = formatDuration2(result.duration);
16787
18020
  const passedStr = colors.success(`${result.passed} passed`);
16788
18021
  const failedStr = result.failed > 0 ? colors.error(`${result.failed} failed`) : colors.dim("0 failed");
16789
18022
  const skippedStr = result.skipped > 0 ? colors.warn(`${result.skipped} skipped`) : colors.dim("0 skipped");
@@ -16906,14 +18139,14 @@ async function loadStoryText(options) {
16906
18139
  return options.story;
16907
18140
  }
16908
18141
  if (options.storyFile) {
16909
- const filePath = import_path36.default.resolve(options.storyFile);
16910
- if (!await import_fs_extra27.default.pathExists(filePath)) {
18142
+ const filePath = import_path40.default.resolve(options.storyFile);
18143
+ if (!await import_fs_extra31.default.pathExists(filePath)) {
16911
18144
  throw new ConfigError(
16912
18145
  `Story file not found: "${filePath}"`,
16913
18146
  "STORY_FILE_NOT_FOUND"
16914
18147
  );
16915
18148
  }
16916
- return import_fs_extra27.default.readFile(filePath, "utf8");
18149
+ return import_fs_extra31.default.readFile(filePath, "utf8");
16917
18150
  }
16918
18151
  throw new ConfigError(
16919
18152
  'Provide a story with --story "<text>" or --story-file <path>',
@@ -16936,7 +18169,7 @@ async function runGenerate(options) {
16936
18169
  process.exit(1);
16937
18170
  }
16938
18171
  const rootDir = process.cwd();
16939
- const outputDir = options.output ? import_path36.default.resolve(options.output) : import_path36.default.join(rootDir, "tests");
18172
+ const outputDir = options.output ? import_path40.default.resolve(options.output) : import_path40.default.join(rootDir, "tests");
16940
18173
  printInfo(`Story: ${colors.dim(`${storyText.slice(0, 80)}${storyText.length > 80 ? "\u2026" : ""}`)}`);
16941
18174
  if (options.suite) {
16942
18175
  printInfo(`Suite name: ${options.suite}`);
@@ -16968,8 +18201,8 @@ async function runGenerate(options) {
16968
18201
  suiteName: options.suite
16969
18202
  });
16970
18203
  genSpinner.succeed("Suite generated");
16971
- const suiteOutputDir = options.output ?? import_path36.default.join(rootDir, "tests");
16972
- printSuccess(`Test suite created in: ${import_path36.default.relative(rootDir, suiteOutputDir)}`);
18204
+ const suiteOutputDir = options.output ?? import_path40.default.join(rootDir, "tests");
18205
+ printSuccess(`Test suite created in: ${import_path40.default.relative(rootDir, suiteOutputDir)}`);
16973
18206
  printInfo("Open Studio or use the Test Editor to review and generate Playwright code:");
16974
18207
  process.stdout.write(` ${colors.accent("npx assuremind studio")}
16975
18208
 
@@ -16980,13 +18213,13 @@ async function runGenerate(options) {
16980
18213
  process.exit(1);
16981
18214
  }
16982
18215
  }
16983
- var import_path36, import_fs_extra27;
18216
+ var import_path40, import_fs_extra31;
16984
18217
  var init_generate = __esm({
16985
18218
  "src/cli/generate.ts"() {
16986
18219
  "use strict";
16987
18220
  init_cjs_shims();
16988
- import_path36 = __toESM(require("path"));
16989
- import_fs_extra27 = __toESM(require("fs-extra"));
18221
+ import_path40 = __toESM(require("path"));
18222
+ import_fs_extra31 = __toESM(require("fs-extra"));
16990
18223
  init_ui();
16991
18224
  init_env();
16992
18225
  init_errors();
@@ -17000,7 +18233,7 @@ __export(validate_exports, {
17000
18233
  });
17001
18234
  async function checkConfig(rootDir, result) {
17002
18235
  printInfo("Checking configuration\u2026");
17003
- if (!await import_fs_extra28.default.pathExists(import_path37.default.join(rootDir, "autotest.config.json")) && !await import_fs_extra28.default.pathExists(import_path37.default.join(rootDir, "autotest.config.ts"))) {
18236
+ if (!await import_fs_extra32.default.pathExists(import_path41.default.join(rootDir, "autotest.config.json")) && !await import_fs_extra32.default.pathExists(import_path41.default.join(rootDir, "autotest.config.ts"))) {
17004
18237
  printError(' autotest.config.ts not found \u2014 run "npx assuremind init" to create it');
17005
18238
  result.errors++;
17006
18239
  return;
@@ -17016,7 +18249,7 @@ async function checkConfig(rootDir, result) {
17016
18249
  }
17017
18250
  async function checkEnv(result) {
17018
18251
  printInfo("Checking environment\u2026");
17019
- if (!await import_fs_extra28.default.pathExists(".env")) {
18252
+ if (!await import_fs_extra32.default.pathExists(".env")) {
17020
18253
  printWarn(" .env file not found \u2014 copy .env.example to .env and fill in your API key");
17021
18254
  result.warnings++;
17022
18255
  return;
@@ -17032,8 +18265,8 @@ async function checkEnv(result) {
17032
18265
  }
17033
18266
  async function checkTests(rootDir, result) {
17034
18267
  printInfo("Scanning test files\u2026");
17035
- const testsDir = import_path37.default.join(rootDir, "tests");
17036
- if (!await import_fs_extra28.default.pathExists(testsDir)) {
18268
+ const testsDir = import_path41.default.join(rootDir, "tests");
18269
+ if (!await import_fs_extra32.default.pathExists(testsDir)) {
17037
18270
  printWarn(" tests/ directory not found \u2014 no tests to validate");
17038
18271
  result.warnings++;
17039
18272
  return;
@@ -17053,7 +18286,7 @@ async function checkTests(rootDir, result) {
17053
18286
  try {
17054
18287
  await readCase(casePath);
17055
18288
  } catch {
17056
- printError(` Invalid: ${import_path37.default.relative(rootDir, casePath)}`);
18289
+ printError(` Invalid: ${import_path41.default.relative(rootDir, casePath)}`);
17057
18290
  invalidCases++;
17058
18291
  result.errors++;
17059
18292
  }
@@ -17081,7 +18314,7 @@ async function checkVariables(rootDir, result) {
17081
18314
  try {
17082
18315
  await readVariables(file);
17083
18316
  } catch {
17084
- printError(` Invalid: ${import_path37.default.relative(rootDir, file)}`);
18317
+ printError(` Invalid: ${import_path41.default.relative(rootDir, file)}`);
17085
18318
  invalid++;
17086
18319
  result.errors++;
17087
18320
  }
@@ -17118,13 +18351,13 @@ async function runValidate() {
17118
18351
  process.exit(1);
17119
18352
  }
17120
18353
  }
17121
- var import_path37, import_fs_extra28;
18354
+ var import_path41, import_fs_extra32;
17122
18355
  var init_validate = __esm({
17123
18356
  "src/cli/validate.ts"() {
17124
18357
  "use strict";
17125
18358
  init_cjs_shims();
17126
- import_path37 = __toESM(require("path"));
17127
- import_fs_extra28 = __toESM(require("fs-extra"));
18359
+ import_path41 = __toESM(require("path"));
18360
+ import_fs_extra32 = __toESM(require("fs-extra"));
17128
18361
  init_ui();
17129
18362
  init_config_store();
17130
18363
  init_suite_store();
@@ -17189,7 +18422,7 @@ async function checkPlaywright() {
17189
18422
  }
17190
18423
  }
17191
18424
  async function checkEnvFile() {
17192
- if (!await import_fs_extra29.default.pathExists(".env")) {
18425
+ if (!await import_fs_extra33.default.pathExists(".env")) {
17193
18426
  return check(
17194
18427
  ".env file",
17195
18428
  false,
@@ -17227,7 +18460,7 @@ async function checkConfig2() {
17227
18460
  }
17228
18461
  }
17229
18462
  async function checkPackageJson() {
17230
- if (!await import_fs_extra29.default.pathExists("package.json")) {
18463
+ if (!await import_fs_extra33.default.pathExists("package.json")) {
17231
18464
  return check(
17232
18465
  "package.json",
17233
18466
  false,
@@ -17235,7 +18468,7 @@ async function checkPackageJson() {
17235
18468
  );
17236
18469
  }
17237
18470
  try {
17238
- const pkg2 = await import_fs_extra29.default.readJson("package.json");
18471
+ const pkg2 = await import_fs_extra33.default.readJson("package.json");
17239
18472
  return check("package.json", true, `${pkg2.name ?? "unnamed"} v${pkg2.version ?? "?"}`);
17240
18473
  } catch {
17241
18474
  return check("package.json", false, "Exists but is not valid JSON");
@@ -17315,13 +18548,13 @@ async function runDoctor() {
17315
18548
  process.exit(1);
17316
18549
  }
17317
18550
  }
17318
- var import_child_process6, import_fs_extra29;
18551
+ var import_child_process6, import_fs_extra33;
17319
18552
  var init_doctor = __esm({
17320
18553
  "src/cli/doctor.ts"() {
17321
18554
  "use strict";
17322
18555
  init_cjs_shims();
17323
18556
  import_child_process6 = require("child_process");
17324
- import_fs_extra29 = __toESM(require("fs-extra"));
18557
+ import_fs_extra33 = __toESM(require("fs-extra"));
17325
18558
  init_ui();
17326
18559
  init_env();
17327
18560
  init_config_store();
@@ -17348,7 +18581,7 @@ async function findCaseFilePath(testsDir, caseId) {
17348
18581
  return null;
17349
18582
  }
17350
18583
  async function applyHealToFile(rootDir, event) {
17351
- const testsDir = import_path38.default.join(rootDir, "tests");
18584
+ const testsDir = import_path42.default.join(rootDir, "tests");
17352
18585
  const casePath = await findCaseFilePath(testsDir, event.caseId);
17353
18586
  if (!casePath) {
17354
18587
  throw new Error(
@@ -17370,7 +18603,7 @@ async function applyHealToFile(rootDir, event) {
17370
18603
  testCase.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
17371
18604
  await writeCase(casePath, testCase);
17372
18605
  printSuccess(
17373
- ` Updated: ${import_path38.default.relative(rootDir, casePath)} \u203A step "${testCase.steps[stepIdx].instruction}"`
18606
+ ` Updated: ${import_path42.default.relative(rootDir, casePath)} \u203A step "${testCase.steps[stepIdx].instruction}"`
17374
18607
  );
17375
18608
  }
17376
18609
  function ask(rl, question) {
@@ -17512,21 +18745,21 @@ Review the changes and merge if the fixes look correct.
17512
18745
  }
17513
18746
  async function runApplyHealing(options) {
17514
18747
  const rootDir = process.cwd();
17515
- (0, import_dotenv2.config)({ path: import_path38.default.join(rootDir, ".env") });
18748
+ (0, import_dotenv2.config)({ path: import_path42.default.join(rootDir, ".env") });
17516
18749
  process.stdout.write("\n");
17517
18750
  let events;
17518
18751
  if (options.from) {
17519
- const reportPath = import_path38.default.resolve(options.from);
17520
- if (!await import_fs_extra30.default.pathExists(reportPath)) {
18752
+ const reportPath = import_path42.default.resolve(options.from);
18753
+ if (!await import_fs_extra34.default.pathExists(reportPath)) {
17521
18754
  printError(`Healing report not found: ${reportPath}`);
17522
18755
  process.exit(1);
17523
18756
  }
17524
18757
  try {
17525
- const rawRunId = import_path38.default.basename(reportPath, ".json").replace("healing-report-", "");
18758
+ const rawRunId = import_path42.default.basename(reportPath, ".json").replace("healing-report-", "");
17526
18759
  const report = await readHealingReport(rootDir, rawRunId);
17527
18760
  events = report.events.filter((e) => e.status === "pending");
17528
18761
  } catch {
17529
- const raw = await import_fs_extra30.default.readJson(reportPath);
18762
+ const raw = await import_fs_extra34.default.readJson(reportPath);
17530
18763
  events = Array.isArray(raw) ? raw.filter((e) => e.status === "pending") : (raw.events ?? []).filter((e) => e.status === "pending");
17531
18764
  }
17532
18765
  } else {
@@ -17555,13 +18788,13 @@ async function runApplyHealing(options) {
17555
18788
  }
17556
18789
  }
17557
18790
  }
17558
- var import_path38, import_fs_extra30, import_readline, import_dotenv2;
18791
+ var import_path42, import_fs_extra34, import_readline, import_dotenv2;
17559
18792
  var init_apply_healing = __esm({
17560
18793
  "src/cli/apply-healing.ts"() {
17561
18794
  "use strict";
17562
18795
  init_cjs_shims();
17563
- import_path38 = __toESM(require("path"));
17564
- import_fs_extra30 = __toESM(require("fs-extra"));
18796
+ import_path42 = __toESM(require("path"));
18797
+ import_fs_extra34 = __toESM(require("fs-extra"));
17565
18798
  import_readline = __toESM(require("readline"));
17566
18799
  import_dotenv2 = require("dotenv");
17567
18800
  init_healing_store();