assuremind 1.2.1 → 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/README.md +83 -8
- package/dist/cli/index.js +1595 -362
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +27 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.js +204 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +202 -1
- package/dist/index.mjs.map +1 -1
- package/docs/CLI-REFERENCE.md +19 -3
- package/docs/GETTING-STARTED.md +42 -1
- package/docs/STUDIO.md +205 -3
- package/package.json +5 -1
- package/templates/ASSUREMIND.md +7 -0
- package/templates/docs/GETTING-STARTED.md +13 -0
- package/templates/docs/STUDIO.md +11 -1
- package/ui/dist/assets/index-B2cmnLeA.js +942 -0
- package/ui/dist/assets/index-Bz5b82ao.css +1 -0
- package/ui/dist/favicon.svg +7 -7
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-DTtYd1hD.js +0 -837
- package/ui/dist/assets/index-lOAh29q9.css +0 -1
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:
|
|
357
|
-
await import_fs_extra.default.writeJson("autotest.config.json",
|
|
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,
|
|
471
|
+
constructor(message, path41, code = "STORAGE_ERROR") {
|
|
472
472
|
super(message, code);
|
|
473
473
|
this.name = "StorageError";
|
|
474
|
-
this.path =
|
|
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:
|
|
6358
|
-
const report = await
|
|
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:
|
|
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
|
|
6975
|
-
return `${u.hostname}${
|
|
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/
|
|
8283
|
-
function
|
|
8284
|
-
return
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
}
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
}
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
}
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
}
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
}
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
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
|
|
8573
|
+
return callFaker("person", "firstName");
|
|
8328
8574
|
case "FAKE_LAST_NAME":
|
|
8329
|
-
return
|
|
8575
|
+
return callFaker("person", "lastName");
|
|
8330
8576
|
case "FAKE_AGE":
|
|
8331
|
-
return
|
|
8577
|
+
return String(Math.floor(Math.random() * 63) + 18);
|
|
8578
|
+
// 18–80
|
|
8332
8579
|
case "FAKE_EMAIL":
|
|
8333
|
-
return
|
|
8580
|
+
return callFaker("internet", "email");
|
|
8334
8581
|
case "FAKE_PHONE":
|
|
8335
|
-
return
|
|
8582
|
+
return callFaker("phone", "number");
|
|
8336
8583
|
case "FAKE_PASSWORD":
|
|
8337
|
-
return
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
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
|
|
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
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
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
|
|
11161
|
+
var fs34 = require("fs");
|
|
10709
11162
|
var isDocker;
|
|
10710
11163
|
function hasDockerEnv() {
|
|
10711
11164
|
try {
|
|
10712
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
11267
|
+
function getLocalAppDataPath(path41) {
|
|
10815
11268
|
const userRegExp = /\/mnt\/([a-z])\/Users\/([^\/:]+)\/AppData\//;
|
|
10816
|
-
const results = userRegExp.exec(
|
|
11269
|
+
const results = userRegExp.exec(path41) || [];
|
|
10817
11270
|
return `/mnt/${results[1]}/Users/${results[2]}/AppData/Local`;
|
|
10818
11271
|
}
|
|
10819
|
-
function getWSLLocalAppDataPath(
|
|
11272
|
+
function getWSLLocalAppDataPath(path41) {
|
|
10820
11273
|
const userRegExp = /\/([a-z])\/Users\/([^\/:]+)\/AppData\//;
|
|
10821
|
-
const results = userRegExp.exec(
|
|
10822
|
-
return toWSLPath(`${results[1]}:\\Users\\${results[2]}\\AppData\\Local`, getLocalAppDataPath(
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
`${
|
|
11004
|
-
`${
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
11746
|
-
await
|
|
11747
|
-
await
|
|
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" ?
|
|
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,
|
|
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 =
|
|
11925
|
-
await
|
|
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 =
|
|
12465
|
+
const userDataDir = import_path28.default.join(lhBaseDir, `audit-${Date.now()}-${auditIdx}`);
|
|
11932
12466
|
try {
|
|
11933
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
12761
|
+
import_path28 = __toESM(require("path"));
|
|
12228
12762
|
import_crypto10 = require("crypto");
|
|
12229
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
12997
|
-
return
|
|
13539
|
+
function baselinesDir2(rootDir) {
|
|
13540
|
+
return import_path30.default.join(rootDir, "results", "baselines");
|
|
12998
13541
|
}
|
|
12999
13542
|
function baselinePath(rootDir, stepId) {
|
|
13000
|
-
return
|
|
13543
|
+
return import_path30.default.join(baselinesDir2(rootDir), `${stepId}.png`);
|
|
13001
13544
|
}
|
|
13002
13545
|
async function saveBaseline(rootDir, stepId, sourcePath) {
|
|
13003
|
-
const dir =
|
|
13004
|
-
await
|
|
13005
|
-
await
|
|
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
|
|
13551
|
+
return import_fs_extra23.default.pathExists(baselinePath(rootDir, stepId));
|
|
13009
13552
|
}
|
|
13010
|
-
var
|
|
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
|
-
|
|
13016
|
-
|
|
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 =
|
|
13028
|
-
const filePath =
|
|
13029
|
-
if (!await
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
13046
|
-
if (!await
|
|
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 =
|
|
13058
|
-
const stat = await
|
|
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
|
|
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
|
-
|
|
13071
|
-
|
|
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,
|
|
13116
|
-
const url = `${this.config.jiraBaseUrl}/rest/api/3${
|
|
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} ${
|
|
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,
|
|
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}${
|
|
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} ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
13833
|
-
|
|
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,
|
|
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
|
-
|
|
14575
|
-
|
|
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 =
|
|
14776
|
-
const content = await
|
|
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 =
|
|
14803
|
-
await
|
|
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 =
|
|
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 =
|
|
15128
|
-
const dirname =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
15201
|
-
await
|
|
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
|
|
15205
|
-
const entries = await
|
|
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 =
|
|
15219
|
-
const filePath =
|
|
15220
|
-
if (!await
|
|
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
|
|
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 =
|
|
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 =
|
|
15249
|
-
const filePath =
|
|
15250
|
-
await
|
|
15251
|
-
await
|
|
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 =
|
|
15275
|
-
const filePath =
|
|
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
|
|
15279
|
-
await
|
|
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 =
|
|
15289
|
-
const filePath =
|
|
15290
|
-
if (!await
|
|
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
|
|
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
|
|
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
|
-
|
|
15307
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
16224
|
+
if (!await import_fs_extra28.default.pathExists(filePath)) return [];
|
|
15682
16225
|
try {
|
|
15683
|
-
const raw = await
|
|
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
|
|
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
|
|
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
|
-
|
|
15730
|
-
|
|
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) {
|
|
15877
|
-
|
|
15878
|
-
else if (iframe.getAttribute('
|
|
15879
|
-
|
|
15880
|
-
|
|
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
|
-
|
|
15888
|
-
|
|
15889
|
-
|
|
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)
|
|
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
|
-
|
|
15919
|
-
|
|
15920
|
-
|
|
15921
|
-
|
|
15922
|
-
|
|
15923
|
-
|
|
15924
|
-
|
|
15925
|
-
|
|
15926
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
15976
|
-
var activeInput = null;
|
|
15977
|
-
var pendingInput = null;
|
|
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
|
|
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 | Ctrl+Shift+
|
|
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 | Ctrl+Shift+U = URL | 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
|
-
|
|
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
|
|
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
|
|
16614
|
+
// Click
|
|
16027
16615
|
document.addEventListener('click', function(e) {
|
|
16028
16616
|
flushInput();
|
|
16029
|
-
|
|
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
|
|
16653
|
+
// Input \u2014 mark element as active; capture final value on blur/Enter
|
|
16066
16654
|
document.addEventListener('input', function(e) {
|
|
16067
|
-
var el = e.
|
|
16068
|
-
if (el
|
|
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
|
|
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.
|
|
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
|
-
|
|
16113
|
-
|
|
16114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17786
|
+
logger47.info("Server closed");
|
|
16571
17787
|
}
|
|
16572
17788
|
};
|
|
16573
17789
|
}
|
|
16574
|
-
var import_fastify, import_cors, import_websocket,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(`(${
|
|
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(`(${
|
|
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 =
|
|
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 =
|
|
16910
|
-
if (!await
|
|
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
|
|
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 ?
|
|
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 ??
|
|
16972
|
-
printSuccess(`Test suite created in: ${
|
|
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
|
|
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
|
-
|
|
16989
|
-
|
|
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
|
|
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
|
|
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 =
|
|
17036
|
-
if (!await
|
|
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: ${
|
|
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: ${
|
|
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
|
|
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
|
-
|
|
17127
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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: ${
|
|
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:
|
|
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 =
|
|
17520
|
-
if (!await
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
17564
|
-
|
|
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();
|