@open-mercato/core 0.6.2-develop.3439.1.94b9d9733c → 0.6.2-develop.3446.1.bd060c6017

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.
Files changed (39) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/entity_index_row/index.js +2 -0
  3. package/dist/generated/entities/entity_index_row/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +1 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/helpers/integration/queue-runner.js +58 -0
  7. package/dist/helpers/integration/queue-runner.js.map +7 -0
  8. package/dist/helpers/integration/queue.js +67 -0
  9. package/dist/helpers/integration/queue.js.map +7 -0
  10. package/dist/helpers/integration/salesUi.js +33 -15
  11. package/dist/helpers/integration/salesUi.js.map +2 -2
  12. package/dist/modules/customers/ai-agents.js +5 -2
  13. package/dist/modules/customers/ai-agents.js.map +2 -2
  14. package/dist/modules/customers/ai-tools/deals-pack.js +52 -8
  15. package/dist/modules/customers/ai-tools/deals-pack.js.map +2 -2
  16. package/dist/modules/customers/ai-tools/types.js.map +2 -2
  17. package/dist/modules/customers/api/people/[id]/route.js +10 -1
  18. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  19. package/dist/modules/query_index/data/entities.js +13 -1
  20. package/dist/modules/query_index/data/entities.js.map +2 -2
  21. package/dist/modules/query_index/migrations/Migration20260518162054_query_index.js +43 -0
  22. package/dist/modules/query_index/migrations/Migration20260518162054_query_index.js.map +7 -0
  23. package/generated/entities/entity_index_row/index.ts +1 -0
  24. package/generated/entity-fields-registry.ts +1 -0
  25. package/package.json +8 -8
  26. package/src/helpers/integration/queue-runner.ts +72 -0
  27. package/src/helpers/integration/queue.ts +70 -0
  28. package/src/helpers/integration/salesUi.ts +38 -23
  29. package/src/modules/attachments/i18n/de.json +13 -0
  30. package/src/modules/attachments/i18n/en.json +13 -0
  31. package/src/modules/attachments/i18n/es.json +13 -0
  32. package/src/modules/attachments/i18n/pl.json +13 -0
  33. package/src/modules/customers/ai-agents.ts +4 -1
  34. package/src/modules/customers/ai-tools/deals-pack.ts +68 -10
  35. package/src/modules/customers/ai-tools/types.ts +6 -0
  36. package/src/modules/customers/api/people/[id]/route.ts +10 -1
  37. package/src/modules/query_index/data/entities.ts +12 -0
  38. package/src/modules/query_index/migrations/.snapshot-open-mercato.json +792 -343
  39. package/src/modules/query_index/migrations/Migration20260518162054_query_index.ts +43 -0
@@ -0,0 +1,58 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { bootstrapFromAppRoot } from "@open-mercato/shared/lib/bootstrap/dynamicLoader";
4
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { createQueue } from "@open-mercato/queue";
6
+ async function drainQueueFromAppRoot(queueName, options) {
7
+ const data = await bootstrapFromAppRoot(options.appRoot);
8
+ const worker = data.modules.flatMap((module) => module.workers ?? []).find((entry) => entry.queue === queueName);
9
+ if (!worker) return 0;
10
+ const container = await createRequestContainer();
11
+ const queue = createQueue(queueName, "local", {
12
+ baseDir: path.resolve(options.appRoot, ".mercato/queue"),
13
+ concurrency: 1
14
+ });
15
+ const resolve = (name) => container.resolve(name);
16
+ try {
17
+ let processedJobs = 0;
18
+ while (true) {
19
+ const result = await queue.process(
20
+ async (job, ctx) => {
21
+ await Promise.resolve(worker.handler(job, { ...ctx, resolve }));
22
+ },
23
+ { limit: options.jobLimit }
24
+ );
25
+ const handled = result.processed + result.failed;
26
+ processedJobs += handled;
27
+ if (handled === 0) return processedJobs;
28
+ }
29
+ } finally {
30
+ await queue.close();
31
+ }
32
+ }
33
+ async function runQueueDrainFromEnv() {
34
+ const queueName = process.env.OM_INTEGRATION_QUEUE_NAME?.trim();
35
+ const appRoot = process.env.OM_INTEGRATION_APP_ROOT?.trim();
36
+ const jobLimit = Number.parseInt(process.env.OM_INTEGRATION_QUEUE_JOB_LIMIT || "100", 10) || 100;
37
+ if (!queueName || !appRoot) throw new Error("Missing queue drain environment");
38
+ const processed = await drainQueueFromAppRoot(queueName, {
39
+ appRoot: path.resolve(appRoot),
40
+ jobLimit
41
+ });
42
+ console.log(JSON.stringify({ processed }));
43
+ }
44
+ const currentFile = fileURLToPath(import.meta.url);
45
+ const invokedFile = process.argv[1] ? path.resolve(process.argv[1]) : null;
46
+ if (invokedFile === currentFile) {
47
+ runQueueDrainFromEnv().then(() => {
48
+ process.exit(0);
49
+ }).catch((error) => {
50
+ console.error(error);
51
+ process.exit(1);
52
+ });
53
+ }
54
+ export {
55
+ drainQueueFromAppRoot,
56
+ runQueueDrainFromEnv
57
+ };
58
+ //# sourceMappingURL=queue-runner.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/helpers/integration/queue-runner.ts"],
4
+ "sourcesContent": ["import path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { bootstrapFromAppRoot } from '@open-mercato/shared/lib/bootstrap/dynamicLoader'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { createQueue } from '@open-mercato/queue'\n\nexport type QueueDrainRunnerOptions = {\n appRoot: string\n jobLimit: number\n}\n\nexport async function drainQueueFromAppRoot(\n queueName: string,\n options: QueueDrainRunnerOptions,\n): Promise<number> {\n const data = await bootstrapFromAppRoot(options.appRoot)\n const worker = data.modules\n .flatMap((module) => module.workers ?? [])\n .find((entry) => entry.queue === queueName)\n if (!worker) return 0\n\n const container = await createRequestContainer()\n const queue = createQueue(queueName, 'local', {\n baseDir: path.resolve(options.appRoot, '.mercato/queue'),\n concurrency: 1,\n })\n const resolve = <T = unknown>(name: string): T => container.resolve(name) as T\n\n try {\n let processedJobs = 0\n while (true) {\n const result = await queue.process(\n async (job, ctx) => {\n await Promise.resolve(worker.handler(job, { ...ctx, resolve }))\n },\n { limit: options.jobLimit },\n )\n const handled = result.processed + result.failed\n processedJobs += handled\n if (handled === 0) return processedJobs\n }\n } finally {\n await queue.close()\n }\n}\n\nexport async function runQueueDrainFromEnv(): Promise<void> {\n const queueName = process.env.OM_INTEGRATION_QUEUE_NAME?.trim()\n const appRoot = process.env.OM_INTEGRATION_APP_ROOT?.trim()\n const jobLimit = Number.parseInt(process.env.OM_INTEGRATION_QUEUE_JOB_LIMIT || '100', 10) || 100\n if (!queueName || !appRoot) throw new Error('Missing queue drain environment')\n\n const processed = await drainQueueFromAppRoot(queueName, {\n appRoot: path.resolve(appRoot),\n jobLimit,\n })\n console.log(JSON.stringify({ processed }))\n}\n\nconst currentFile = fileURLToPath(import.meta.url)\nconst invokedFile = process.argv[1] ? path.resolve(process.argv[1]) : null\n\nif (invokedFile === currentFile) {\n runQueueDrainFromEnv()\n .then(() => {\n process.exit(0)\n })\n .catch((error) => {\n console.error(error)\n process.exit(1)\n })\n}\n"],
5
+ "mappings": "AAAA,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAC9B,SAAS,4BAA4B;AACrC,SAAS,8BAA8B;AACvC,SAAS,mBAAmB;AAO5B,eAAsB,sBACpB,WACA,SACiB;AACjB,QAAM,OAAO,MAAM,qBAAqB,QAAQ,OAAO;AACvD,QAAM,SAAS,KAAK,QACjB,QAAQ,CAAC,WAAW,OAAO,WAAW,CAAC,CAAC,EACxC,KAAK,CAAC,UAAU,MAAM,UAAU,SAAS;AAC5C,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,QAAQ,YAAY,WAAW,SAAS;AAAA,IAC5C,SAAS,KAAK,QAAQ,QAAQ,SAAS,gBAAgB;AAAA,IACvD,aAAa;AAAA,EACf,CAAC;AACD,QAAM,UAAU,CAAc,SAAoB,UAAU,QAAQ,IAAI;AAExE,MAAI;AACF,QAAI,gBAAgB;AACpB,WAAO,MAAM;AACX,YAAM,SAAS,MAAM,MAAM;AAAA,QACzB,OAAO,KAAK,QAAQ;AAClB,gBAAM,QAAQ,QAAQ,OAAO,QAAQ,KAAK,EAAE,GAAG,KAAK,QAAQ,CAAC,CAAC;AAAA,QAChE;AAAA,QACA,EAAE,OAAO,QAAQ,SAAS;AAAA,MAC5B;AACA,YAAM,UAAU,OAAO,YAAY,OAAO;AAC1C,uBAAiB;AACjB,UAAI,YAAY,EAAG,QAAO;AAAA,IAC5B;AAAA,EACF,UAAE;AACA,UAAM,MAAM,MAAM;AAAA,EACpB;AACF;AAEA,eAAsB,uBAAsC;AAC1D,QAAM,YAAY,QAAQ,IAAI,2BAA2B,KAAK;AAC9D,QAAM,UAAU,QAAQ,IAAI,yBAAyB,KAAK;AAC1D,QAAM,WAAW,OAAO,SAAS,QAAQ,IAAI,kCAAkC,OAAO,EAAE,KAAK;AAC7F,MAAI,CAAC,aAAa,CAAC,QAAS,OAAM,IAAI,MAAM,iCAAiC;AAE7E,QAAM,YAAY,MAAM,sBAAsB,WAAW;AAAA,IACvD,SAAS,KAAK,QAAQ,OAAO;AAAA,IAC7B;AAAA,EACF,CAAC;AACD,UAAQ,IAAI,KAAK,UAAU,EAAE,UAAU,CAAC,CAAC;AAC3C;AAEA,MAAM,cAAc,cAAc,YAAY,GAAG;AACjD,MAAM,cAAc,QAAQ,KAAK,CAAC,IAAI,KAAK,QAAQ,QAAQ,KAAK,CAAC,CAAC,IAAI;AAEtE,IAAI,gBAAgB,aAAa;AAC/B,uBAAqB,EAClB,KAAK,MAAM;AACV,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC,EACA,MAAM,CAAC,UAAU;AAChB,YAAQ,MAAM,KAAK;AACnB,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACL;",
6
+ "names": []
7
+ }
@@ -0,0 +1,67 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import { drainQueueFromAppRoot } from "./queue-runner.js";
5
+ const DEFAULT_JOB_LIMIT = 100;
6
+ function resolveAppRoot(input) {
7
+ return path.resolve(input?.trim() || process.env.OM_TEST_APP_ROOT?.trim() || path.resolve(process.cwd(), "apps/mercato"));
8
+ }
9
+ async function drainQueueInCurrentProcess(queueName, options) {
10
+ return drainQueueFromAppRoot(queueName, options);
11
+ }
12
+ function resolveAppRunnerPath(appRoot) {
13
+ const requireFromApp = createRequire(path.join(appRoot, "package.json"));
14
+ return requireFromApp.resolve("@open-mercato/core/helpers/integration/queue-runner");
15
+ }
16
+ function drainQueueInAppProcess(queueName, options) {
17
+ return new Promise((resolve, reject) => {
18
+ const child = spawn(process.execPath, [resolveAppRunnerPath(options.appRoot)], {
19
+ cwd: options.appRoot,
20
+ env: {
21
+ ...process.env,
22
+ OM_INTEGRATION_APP_ROOT: options.appRoot,
23
+ OM_INTEGRATION_QUEUE_NAME: queueName,
24
+ OM_INTEGRATION_QUEUE_JOB_LIMIT: String(options.jobLimit),
25
+ QUEUE_BASE_DIR: path.resolve(options.appRoot, ".mercato/queue")
26
+ },
27
+ stdio: ["ignore", "pipe", "pipe"]
28
+ });
29
+ let stdout = "";
30
+ let stderr = "";
31
+ child.stdout.on("data", (chunk) => {
32
+ stdout += chunk.toString();
33
+ });
34
+ child.stderr.on("data", (chunk) => {
35
+ stderr += chunk.toString();
36
+ });
37
+ child.on("error", reject);
38
+ child.on("close", (code) => {
39
+ if (code !== 0) {
40
+ reject(new Error(`Queue drain failed for "${queueName}" in ${options.appRoot} (exit ${code}).
41
+ ${stderr || stdout}`));
42
+ return;
43
+ }
44
+ const lastLine = stdout.trim().split(/\r?\n/).reverse().find((line) => line.trim().startsWith("{")) ?? "{}";
45
+ try {
46
+ const parsed = JSON.parse(lastLine);
47
+ resolve(Number(parsed.processed ?? 0));
48
+ } catch {
49
+ resolve(0);
50
+ }
51
+ });
52
+ });
53
+ }
54
+ async function drainIntegrationQueue(queueName, options = {}) {
55
+ const resolved = {
56
+ appRoot: resolveAppRoot(options.appRoot),
57
+ jobLimit: options.jobLimit ?? DEFAULT_JOB_LIMIT
58
+ };
59
+ if (process.env.OM_TEST_APP_ROOT?.trim()) {
60
+ return drainQueueInAppProcess(queueName, resolved);
61
+ }
62
+ return drainQueueInCurrentProcess(queueName, resolved);
63
+ }
64
+ export {
65
+ drainIntegrationQueue
66
+ };
67
+ //# sourceMappingURL=queue.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/helpers/integration/queue.ts"],
4
+ "sourcesContent": ["import { spawn } from 'node:child_process'\nimport { createRequire } from 'node:module'\nimport path from 'node:path'\nimport { drainQueueFromAppRoot } from './queue-runner'\n\ntype DrainQueueOptions = {\n appRoot?: string\n jobLimit?: number\n}\n\nconst DEFAULT_JOB_LIMIT = 100\n\nfunction resolveAppRoot(input?: string): string {\n return path.resolve(input?.trim() || process.env.OM_TEST_APP_ROOT?.trim() || path.resolve(process.cwd(), 'apps/mercato'))\n}\n\nasync function drainQueueInCurrentProcess(queueName: string, options: Required<DrainQueueOptions>): Promise<number> {\n return drainQueueFromAppRoot(queueName, options)\n}\n\nfunction resolveAppRunnerPath(appRoot: string): string {\n const requireFromApp = createRequire(path.join(appRoot, 'package.json'))\n return requireFromApp.resolve('@open-mercato/core/helpers/integration/queue-runner')\n}\n\nfunction drainQueueInAppProcess(queueName: string, options: Required<DrainQueueOptions>): Promise<number> {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [resolveAppRunnerPath(options.appRoot)], {\n cwd: options.appRoot,\n env: {\n ...process.env,\n OM_INTEGRATION_APP_ROOT: options.appRoot,\n OM_INTEGRATION_QUEUE_NAME: queueName,\n OM_INTEGRATION_QUEUE_JOB_LIMIT: String(options.jobLimit),\n QUEUE_BASE_DIR: path.resolve(options.appRoot, '.mercato/queue'),\n },\n stdio: ['ignore', 'pipe', 'pipe'],\n })\n\n let stdout = ''\n let stderr = ''\n child.stdout.on('data', (chunk) => { stdout += chunk.toString() })\n child.stderr.on('data', (chunk) => { stderr += chunk.toString() })\n child.on('error', reject)\n child.on('close', (code) => {\n if (code !== 0) {\n reject(new Error(`Queue drain failed for \"${queueName}\" in ${options.appRoot} (exit ${code}).\\n${stderr || stdout}`))\n return\n }\n const lastLine = stdout.trim().split(/\\r?\\n/).reverse().find((line) => line.trim().startsWith('{')) ?? '{}'\n try {\n const parsed = JSON.parse(lastLine) as { processed?: unknown }\n resolve(Number(parsed.processed ?? 0))\n } catch {\n resolve(0)\n }\n })\n })\n}\n\nexport async function drainIntegrationQueue(queueName: string, options: DrainQueueOptions = {}): Promise<number> {\n const resolved = {\n appRoot: resolveAppRoot(options.appRoot),\n jobLimit: options.jobLimit ?? DEFAULT_JOB_LIMIT,\n }\n if (process.env.OM_TEST_APP_ROOT?.trim()) {\n return drainQueueInAppProcess(queueName, resolved)\n }\n return drainQueueInCurrentProcess(queueName, resolved)\n}\n"],
5
+ "mappings": "AAAA,SAAS,aAAa;AACtB,SAAS,qBAAqB;AAC9B,OAAO,UAAU;AACjB,SAAS,6BAA6B;AAOtC,MAAM,oBAAoB;AAE1B,SAAS,eAAe,OAAwB;AAC9C,SAAO,KAAK,QAAQ,OAAO,KAAK,KAAK,QAAQ,IAAI,kBAAkB,KAAK,KAAK,KAAK,QAAQ,QAAQ,IAAI,GAAG,cAAc,CAAC;AAC1H;AAEA,eAAe,2BAA2B,WAAmB,SAAuD;AAClH,SAAO,sBAAsB,WAAW,OAAO;AACjD;AAEA,SAAS,qBAAqB,SAAyB;AACrD,QAAM,iBAAiB,cAAc,KAAK,KAAK,SAAS,cAAc,CAAC;AACvE,SAAO,eAAe,QAAQ,qDAAqD;AACrF;AAEA,SAAS,uBAAuB,WAAmB,SAAuD;AACxG,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,QAAQ,MAAM,QAAQ,UAAU,CAAC,qBAAqB,QAAQ,OAAO,CAAC,GAAG;AAAA,MAC7E,KAAK,QAAQ;AAAA,MACb,KAAK;AAAA,QACH,GAAG,QAAQ;AAAA,QACX,yBAAyB,QAAQ;AAAA,QACjC,2BAA2B;AAAA,QAC3B,gCAAgC,OAAO,QAAQ,QAAQ;AAAA,QACvD,gBAAgB,KAAK,QAAQ,QAAQ,SAAS,gBAAgB;AAAA,MAChE;AAAA,MACA,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,IAClC,CAAC;AAED,QAAI,SAAS;AACb,QAAI,SAAS;AACb,UAAM,OAAO,GAAG,QAAQ,CAAC,UAAU;AAAE,gBAAU,MAAM,SAAS;AAAA,IAAE,CAAC;AACjE,UAAM,OAAO,GAAG,QAAQ,CAAC,UAAU;AAAE,gBAAU,MAAM,SAAS;AAAA,IAAE,CAAC;AACjE,UAAM,GAAG,SAAS,MAAM;AACxB,UAAM,GAAG,SAAS,CAAC,SAAS;AAC1B,UAAI,SAAS,GAAG;AACd,eAAO,IAAI,MAAM,2BAA2B,SAAS,QAAQ,QAAQ,OAAO,UAAU,IAAI;AAAA,EAAO,UAAU,MAAM,EAAE,CAAC;AACpH;AAAA,MACF;AACA,YAAM,WAAW,OAAO,KAAK,EAAE,MAAM,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,KAAK,KAAK,EAAE,WAAW,GAAG,CAAC,KAAK;AACvG,UAAI;AACF,cAAM,SAAS,KAAK,MAAM,QAAQ;AAClC,gBAAQ,OAAO,OAAO,aAAa,CAAC,CAAC;AAAA,MACvC,QAAQ;AACN,gBAAQ,CAAC;AAAA,MACX;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;AAEA,eAAsB,sBAAsB,WAAmB,UAA6B,CAAC,GAAoB;AAC/G,QAAM,WAAW;AAAA,IACf,SAAS,eAAe,QAAQ,OAAO;AAAA,IACvC,UAAU,QAAQ,YAAY;AAAA,EAChC;AACA,MAAI,QAAQ,IAAI,kBAAkB,KAAK,GAAG;AACxC,WAAO,uBAAuB,WAAW,QAAQ;AAAA,EACnD;AACA,SAAO,2BAA2B,WAAW,QAAQ;AACvD;",
6
+ "names": []
7
+ }
@@ -765,12 +765,19 @@ async function addCustomLine(page, options) {
765
765
  await dialog.getByRole("textbox", { name: "0.00" }).fill(String(options.unitPriceGross));
766
766
  await dialog.getByRole("textbox", { name: "1" }).fill(String(options.quantity));
767
767
  if (options.taxClassName) {
768
- const taxClassTrigger = dialog.locator('[role="combobox"]').filter({ hasText: /No tax class selected/i }).first();
768
+ const taxClassTrigger = dialog.locator('[data-crud-field-id="taxRateId"] [role="combobox"]').first();
769
769
  if (await taxClassTrigger.count() > 0) {
770
- await taxClassTrigger.click();
771
- const opt = page.getByRole("option", { name: options.taxClassName, exact: true });
772
- await opt.first().waitFor({ state: "visible", timeout: 3e3 });
773
- await opt.first().click({ force: true });
770
+ await expect(taxClassTrigger).toBeEnabled({ timeout: TEST_WAIT_TIMEOUT_MS });
771
+ const selectedLabel = options.taxClassName.split("\u2022")[0].trim();
772
+ for (let attempt = 0; attempt < 3; attempt += 1) {
773
+ await taxClassTrigger.click();
774
+ const opt = page.getByRole("option", { name: options.taxClassName, exact: true }).first();
775
+ await opt.waitFor({ state: "visible", timeout: 3e3 });
776
+ await opt.click();
777
+ const selected = await taxClassTrigger.filter({ hasText: new RegExp(escapeRegExp(selectedLabel), "i") }).waitFor({ state: "visible", timeout: 1500 }).then(() => true, () => false);
778
+ if (selected) break;
779
+ }
780
+ await expect(taxClassTrigger).toContainText(selectedLabel, { timeout: TEST_WAIT_TIMEOUT_MS });
774
781
  }
775
782
  }
776
783
  const submitButton = dialog.getByRole("button", { name: /Add item/i });
@@ -843,17 +850,28 @@ async function addAdjustment(page, options) {
843
850
  TEST_WAIT_TIMEOUT_MS
844
851
  );
845
852
  await expect(kindTrigger).toBeEnabled({ timeout: TEST_WAIT_TIMEOUT_MS });
846
- await kindTrigger.click();
847
- const kindOption = page.getByRole("option", { name: new RegExp(`^${escapeRegExp(kindLabel)}$`, "i") }).first();
848
- const optionVisible = await kindOption.waitFor({ state: "visible", timeout: TEST_WAIT_TIMEOUT_MS }).then(() => true, () => false);
849
- if (optionVisible) {
850
- await kindOption.click({ force: true });
851
- } else {
852
- await page.keyboard.type(kindLabel.charAt(0));
853
- await page.waitForTimeout(150);
854
- await page.keyboard.press("Enter");
853
+ for (let attempt = 0; attempt < 3; attempt += 1) {
854
+ const currentLabel = await kindTrigger.textContent().catch(() => "");
855
+ if (currentLabel?.includes(kindLabel)) return;
856
+ const expanded = await kindTrigger.getAttribute("aria-expanded").catch(() => null);
857
+ if (expanded === "true") {
858
+ await page.keyboard.press("Escape").catch(() => {
859
+ });
860
+ }
861
+ await kindTrigger.click();
862
+ const kindOption = page.getByRole("option", { name: new RegExp(`^${escapeRegExp(kindLabel)}$`, "i") }).first();
863
+ const optionVisible = await kindOption.waitFor({ state: "visible", timeout: TEST_WAIT_TIMEOUT_MS }).then(() => true, () => false);
864
+ if (optionVisible) {
865
+ await kindOption.click({ force: true });
866
+ } else {
867
+ await page.keyboard.type(kindLabel.charAt(0));
868
+ await page.waitForTimeout(150);
869
+ await page.keyboard.press("Enter");
870
+ }
871
+ await expect(kindTrigger).toContainText(kindLabel, { timeout: 2e3 }).catch(() => {
872
+ });
855
873
  }
856
- await expect(kindTrigger).toContainText(kindLabel, { timeout: 2e3 });
874
+ await expect(kindTrigger).toContainText(kindLabel, { timeout: 5e3 });
857
875
  };
858
876
  await selectKindOption();
859
877
  const labelInput = dialog.getByPlaceholder(/e\.g\. Shipping fee/i).first();