@iblai/iblai-js 1.20.13 → 1.20.15

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.
@@ -1470,12 +1470,16 @@ declare const TASKS_LABELS: {
1470
1470
  readonly startTimeInPast: "Start time must be in the future.";
1471
1471
  };
1472
1472
  readonly deleteDialog: {
1473
+ /** `aria-label` on the dialog's `DialogContent` — scopes in-dialog locators. */
1474
+ readonly dialogName: "delete-task";
1473
1475
  readonly title: "Delete Task";
1474
1476
  readonly delete: "Delete Task";
1475
1477
  readonly deleting: "Deleting...";
1476
1478
  readonly cancel: "Cancel";
1477
1479
  };
1478
1480
  readonly logDetails: {
1481
+ /** `aria-label` on the dialog's `DialogContent` — scopes in-dialog locators. */
1482
+ readonly dialogName: "log-details";
1479
1483
  readonly title: "Log Details";
1480
1484
  readonly status: "Status";
1481
1485
  readonly createdAt: "Created";
@@ -1840,20 +1840,23 @@ async function openNewInstanceDialog(page) {
1840
1840
  * @param gatewayToken - Optional auth token
1841
1841
  */
1842
1842
  async function createInstance(page, { name, serverUrl, clawType = 'openclaw', gatewayToken, }) {
1843
- await openNewInstanceDialog(page);
1844
- await page.getByLabel('Name').fill(name);
1845
- await page.getByLabel('Server URL').fill(serverUrl);
1843
+ // Scope in-dialog queries to the dialog: "Name" also labels the mentor-name
1844
+ // field on the edit-mentor modal behind it, and "Create" is a generic name.
1845
+ const dialog = await openNewInstanceDialog(page);
1846
+ await dialog.getByLabel('Name').fill(name);
1847
+ await dialog.getByLabel('Server URL').fill(serverUrl);
1846
1848
  if (clawType !== 'openclaw') {
1847
- const typeSelect = page.getByLabel('Type');
1849
+ const typeSelect = dialog.getByLabel('Type');
1848
1850
  await typeSelect.click();
1851
+ // Radix renders the option list in a portal outside the dialog subtree.
1849
1852
  const option = page.getByRole('option', { name: new RegExp(clawType, 'i') });
1850
1853
  await expect(option).toBeVisible({ timeout: 5000 });
1851
1854
  await option.click();
1852
1855
  }
1853
1856
  if (gatewayToken) {
1854
- await page.getByLabel('Gateway Token').fill(gatewayToken);
1857
+ await dialog.getByLabel('Gateway Token').fill(gatewayToken);
1855
1858
  }
1856
- const createButton = page.getByRole('button', { name: /^Create$/i });
1859
+ const createButton = dialog.getByRole('button', { name: /^Create$/i });
1857
1860
  await expect(createButton).toBeEnabled({ timeout: 5000 });
1858
1861
  await createButton.click();
1859
1862
  await expect(page.getByText('Instance created', { exact: false })).toBeVisible({
@@ -1892,23 +1895,25 @@ async function openEditInstanceDialog(page, instanceName) {
1892
1895
  * @param updates - Fields to update
1893
1896
  */
1894
1897
  async function editInstance(page, instanceName, updates) {
1895
- await openEditInstanceDialog(page, instanceName);
1898
+ // Scope in-dialog queries to the dialog (see createInstance).
1899
+ const dialog = await openEditInstanceDialog(page, instanceName);
1896
1900
  if (updates.name !== undefined) {
1897
- const nameInput = page.getByLabel('Name');
1901
+ const nameInput = dialog.getByLabel('Name');
1898
1902
  await nameInput.fill(updates.name);
1899
1903
  }
1900
1904
  if (updates.serverUrl !== undefined) {
1901
- const urlInput = page.getByLabel('Server URL');
1905
+ const urlInput = dialog.getByLabel('Server URL');
1902
1906
  await urlInput.fill(updates.serverUrl);
1903
1907
  }
1904
1908
  if (updates.clawType !== undefined) {
1905
- const typeSelect = page.getByLabel('Type');
1909
+ const typeSelect = dialog.getByLabel('Type');
1906
1910
  await typeSelect.click();
1911
+ // Radix renders the option list in a portal outside the dialog subtree.
1907
1912
  const option = page.getByRole('option', { name: new RegExp(updates.clawType, 'i') });
1908
1913
  await expect(option).toBeVisible({ timeout: 5000 });
1909
1914
  await option.click();
1910
1915
  }
1911
- const saveButton = page.getByRole('button', { name: /^Save$/i });
1916
+ const saveButton = dialog.getByRole('button', { name: /^Save$/i });
1912
1917
  await expect(saveButton).toBeEnabled({ timeout: 5000 });
1913
1918
  await saveButton.click();
1914
1919
  await expect(page.getByText('Instance updated', { exact: false })).toBeVisible({
@@ -2137,9 +2142,10 @@ async function openLLMProviderPicker(page) {
2137
2142
  * @param modelName - The model name (e.g. 'claude-sonnet-4-5-20250929')
2138
2143
  */
2139
2144
  async function selectLLMModel(page, providerName, modelName) {
2140
- await openLLMProviderPicker(page);
2141
- // Click the provider card
2142
- const providerCard = page.getByRole('dialog').getByText(new RegExp(`^${providerName}$`, 'i'));
2145
+ const providerDialog = await openLLMProviderPicker(page);
2146
+ // Click the provider card, scoped to the picker dialog (bare
2147
+ // page.getByRole('dialog') also matches the edit-mentor modal behind it).
2148
+ const providerCard = providerDialog.getByText(new RegExp(`^${providerName}$`, 'i'));
2143
2149
  await expect(providerCard).toBeVisible({ timeout: 10000 });
2144
2150
  await providerCard.click();
2145
2151
  // Model selection modal ("LLM Selection")
@@ -2217,10 +2223,12 @@ async function openAgentPromptEditModal(page, field) {
2217
2223
  * Edit an agent prompt field (opens the modal, types content, saves).
2218
2224
  */
2219
2225
  async function editAgentPrompt(page, field, content) {
2220
- await openAgentPromptEditModal(page, field);
2226
+ // Scope to the dialog so the contenteditable/textarea `.first()` and the
2227
+ // generic "Save" button don't match elements on the modal behind it.
2228
+ const dialog = await openAgentPromptEditModal(page, field);
2221
2229
  // The RichTextEditor renders into a contenteditable element; fall back to textarea if simpler.
2222
- const editorRegion = page.locator('[contenteditable="true"]').first();
2223
- const textarea = page.locator('textarea').first();
2230
+ const editorRegion = dialog.locator('[contenteditable="true"]').first();
2231
+ const textarea = dialog.locator('textarea').first();
2224
2232
  const hasEditor = await editorRegion.isVisible().catch(() => false);
2225
2233
  if (hasEditor) {
2226
2234
  await editorRegion.click();
@@ -2231,7 +2239,7 @@ async function editAgentPrompt(page, field, content) {
2231
2239
  else {
2232
2240
  await textarea.fill(content);
2233
2241
  }
2234
- const saveButton = page.getByRole('button', { name: /^Save$/i });
2242
+ const saveButton = dialog.getByRole('button', { name: /^Save$/i });
2235
2243
  await expect(saveButton).toBeEnabled({ timeout: 5000 });
2236
2244
  await saveButton.click();
2237
2245
  await expect(page.getByText(`${field} updated successfully`, { exact: false })).toBeVisible({
@@ -2362,10 +2370,15 @@ async function openNewSkillDialog(page) {
2362
2370
  /**
2363
2371
  * Type content into the RichTextEditor Instruction field.
2364
2372
  * The editor renders as a contenteditable element.
2373
+ *
2374
+ * `dialog` scopes the lookups to the open skill modal so the
2375
+ * contenteditable `.first()` and the "Instruction" label don't match
2376
+ * elements on the modal behind it. The keyboard is page-global.
2365
2377
  */
2366
- async function fillInstructionEditor(page, content) {
2378
+ async function fillInstructionEditor(dialog, content) {
2379
+ const page = dialog.page();
2367
2380
  // RichTextEditor uses a contenteditable region
2368
- const editor = page.locator('[contenteditable="true"]').first();
2381
+ const editor = dialog.locator('[contenteditable="true"]').first();
2369
2382
  const hasEditor = await editor.isVisible().catch(() => false);
2370
2383
  if (hasEditor) {
2371
2384
  await editor.click();
@@ -2377,34 +2390,35 @@ async function fillInstructionEditor(page, content) {
2377
2390
  return;
2378
2391
  }
2379
2392
  // Fallback: a plain labelled input/textarea named "Instruction"
2380
- const textarea = page.getByLabel('Instruction');
2393
+ const textarea = dialog.getByLabel('Instruction');
2381
2394
  await textarea.fill(content);
2382
2395
  }
2383
2396
  /**
2384
2397
  * Fill the skill form fields (name, slug, description, version, instruction).
2385
- * The form is used by both the New and Edit dialogs.
2398
+ * The form is used by both the New and Edit dialogs. `dialog` scopes every
2399
+ * field query to the open modal (see fillInstructionEditor).
2386
2400
  * The Instruction field is a rich-text (markdown) editor.
2387
2401
  */
2388
- async function fillSkillForm(page, values) {
2389
- await page.getByLabel('Name').fill(values.name);
2390
- await page.getByLabel('Slug').fill(values.slug);
2402
+ async function fillSkillForm(dialog, values) {
2403
+ await dialog.getByLabel('Name').fill(values.name);
2404
+ await dialog.getByLabel('Slug').fill(values.slug);
2391
2405
  if (values.version !== undefined) {
2392
- await page.getByLabel('Version').fill(values.version);
2406
+ await dialog.getByLabel('Version').fill(values.version);
2393
2407
  }
2394
2408
  if (values.description !== undefined) {
2395
- await page.getByLabel('Description').fill(values.description);
2409
+ await dialog.getByLabel('Description').fill(values.description);
2396
2410
  }
2397
2411
  if (values.instruction !== undefined) {
2398
- await fillInstructionEditor(page, values.instruction);
2412
+ await fillInstructionEditor(dialog, values.instruction);
2399
2413
  }
2400
2414
  }
2401
2415
  /**
2402
2416
  * Create a new platform-level skill via the Skills tab.
2403
2417
  */
2404
2418
  async function createSkill(page, values) {
2405
- await openNewSkillDialog(page);
2406
- await fillSkillForm(page, values);
2407
- const createButton = page.getByRole('button', { name: /^Create$/i });
2419
+ const dialog = await openNewSkillDialog(page);
2420
+ await fillSkillForm(dialog, values);
2421
+ const createButton = dialog.getByRole('button', { name: /^Create$/i });
2408
2422
  await expect(createButton).toBeEnabled({ timeout: 5000 });
2409
2423
  await createButton.click();
2410
2424
  await expect(page.getByText('Skill created', { exact: false })).toBeVisible({
@@ -2442,23 +2456,23 @@ async function openEditSkillDialog(page, skillName) {
2442
2456
  * @param updates - Fields to update on the skill
2443
2457
  */
2444
2458
  async function editSkill(page, skillName, updates) {
2445
- await openEditSkillDialog(page, skillName);
2459
+ const dialog = await openEditSkillDialog(page, skillName);
2446
2460
  if (updates.name !== undefined) {
2447
- await page.getByLabel('Name').fill(updates.name);
2461
+ await dialog.getByLabel('Name').fill(updates.name);
2448
2462
  }
2449
2463
  if (updates.slug !== undefined) {
2450
- await page.getByLabel('Slug').fill(updates.slug);
2464
+ await dialog.getByLabel('Slug').fill(updates.slug);
2451
2465
  }
2452
2466
  if (updates.version !== undefined) {
2453
- await page.getByLabel('Version').fill(updates.version);
2467
+ await dialog.getByLabel('Version').fill(updates.version);
2454
2468
  }
2455
2469
  if (updates.description !== undefined) {
2456
- await page.getByLabel('Description').fill(updates.description);
2470
+ await dialog.getByLabel('Description').fill(updates.description);
2457
2471
  }
2458
2472
  if (updates.instruction !== undefined) {
2459
- await fillInstructionEditor(page, updates.instruction);
2473
+ await fillInstructionEditor(dialog, updates.instruction);
2460
2474
  }
2461
- const saveButton = page.getByRole('button', { name: /^Save$/i });
2475
+ const saveButton = dialog.getByRole('button', { name: /^Save$/i });
2462
2476
  await expect(saveButton).toBeEnabled({ timeout: 5000 });
2463
2477
  await saveButton.click();
2464
2478
  await expect(page.getByText('Skill updated', { exact: false })).toBeVisible({
@@ -4285,12 +4299,16 @@ const TASKS_LABELS = {
4285
4299
  startTimeInPast: 'Start time must be in the future.',
4286
4300
  },
4287
4301
  deleteDialog: {
4302
+ /** `aria-label` on the dialog's `DialogContent` — scopes in-dialog locators. */
4303
+ dialogName: 'delete-task',
4288
4304
  title: 'Delete Task',
4289
4305
  delete: 'Delete Task',
4290
4306
  deleting: 'Deleting...',
4291
4307
  cancel: 'Cancel',
4292
4308
  },
4293
4309
  logDetails: {
4310
+ /** `aria-label` on the dialog's `DialogContent` — scopes in-dialog locators. */
4311
+ dialogName: 'log-details',
4294
4312
  title: 'Log Details',
4295
4313
  status: 'Status',
4296
4314
  createdAt: 'Created',
@@ -4473,6 +4491,9 @@ async function expectScheduleStartTimeInPastError(scope) {
4473
4491
  });
4474
4492
  }
4475
4493
  // ── Delete flow ────────────────────────────────────────────────────────
4494
+ function getDeleteTaskDialog(scope) {
4495
+ return asPage(scope).locator(`[role="dialog"][aria-label="${TASKS_LABELS.deleteDialog.dialogName}"]`);
4496
+ }
4476
4497
  /**
4477
4498
  * Click the trash icon on a task row and confirm the delete in the
4478
4499
  * follow-up dialog.
@@ -4480,17 +4501,20 @@ async function expectScheduleStartTimeInPastError(scope) {
4480
4501
  async function deleteTask(scope, taskName) {
4481
4502
  const row = getTaskRow(scope, taskName);
4482
4503
  await row.getByRole('button', { name: TASKS_LABELS.list.deleteTask }).click();
4483
- const page = asPage(scope);
4484
- const confirm = page.getByRole('button', { name: TASKS_LABELS.deleteDialog.delete });
4504
+ // Scope to the dialog: the confirm button's "Delete Task" name collides
4505
+ // (case-insensitively) with the row trash buttons' "Delete task" label.
4506
+ const dialog = getDeleteTaskDialog(scope);
4507
+ const confirm = dialog.getByRole('button', { name: TASKS_LABELS.deleteDialog.delete });
4485
4508
  await expect(confirm).toBeVisible({ timeout: 5000 });
4486
4509
  await confirm.click();
4487
4510
  // Wait for the dialog to close.
4488
- await expect(page.getByRole('heading', { name: TASKS_LABELS.deleteDialog.title })).toBeHidden({
4489
- timeout: 10000,
4490
- });
4511
+ await expect(dialog).toBeHidden({ timeout: 10000 });
4491
4512
  logger.info(`Deleted task: ${taskName}`);
4492
4513
  }
4493
4514
  // ── Logs panel + log details ───────────────────────────────────────────
4515
+ function getLogDetailsDialog(scope) {
4516
+ return asPage(scope).locator(`[role="dialog"][aria-label="${TASKS_LABELS.logDetails.dialogName}"]`);
4517
+ }
4494
4518
  /**
4495
4519
  * Wait for the logs panel to finish loading and assert that the selected
4496
4520
  * task name is shown in the header.
@@ -4516,14 +4540,13 @@ async function openFirstLogDetails(scope) {
4516
4540
  // first interactive child.
4517
4541
  const panel = scope.getByText(TASKS_LABELS.logs.title, { exact: true }).locator('xpath=../..');
4518
4542
  await panel.getByRole('button').first().click();
4519
- const page = asPage(scope);
4520
- await expect(page.getByRole('heading', { name: TASKS_LABELS.logDetails.title })).toBeVisible({
4521
- timeout: 10000,
4522
- });
4543
+ await expect(getLogDetailsDialog(scope)).toBeVisible({ timeout: 10000 });
4523
4544
  }
4524
4545
  async function expectLogDetailsStatus(scope, status) {
4525
- const page = asPage(scope);
4526
- await expect(page.getByText(status, { exact: true })).toBeVisible({ timeout: 5000 });
4546
+ // Scope to the dialog so the status text isn't matched on a task row.
4547
+ await expect(getLogDetailsDialog(scope).getByText(status, { exact: true })).toBeVisible({
4548
+ timeout: 5000,
4549
+ });
4527
4550
  }
4528
4551
 
4529
4552
  /** Extract browser key from device name (e.g., 'Desktop Chrome' -> 'chrome') */