@esimplicity/stack-tests 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,881 @@
1
+ import {
2
+ assertMasked,
3
+ interpolate,
4
+ parseExpected,
5
+ registerApiAssertionSteps,
6
+ registerApiAuthSteps,
7
+ registerApiHttpSteps,
8
+ registerApiSteps,
9
+ registerCleanup,
10
+ registerHybridSteps,
11
+ registerHybridSuite,
12
+ registerSharedCleanupSteps,
13
+ registerSharedSteps,
14
+ registerSharedVarSteps,
15
+ registerTuiBasicSteps,
16
+ registerTuiSteps,
17
+ registerTuiWizardSteps,
18
+ registerUiBasicSteps,
19
+ registerUiSteps,
20
+ registerWizardSteps,
21
+ selectPath,
22
+ tryParseJson
23
+ } from "./chunk-YPUQQZM2.js";
24
+
25
+ // src/fixtures.ts
26
+ import { test as base } from "playwright-bdd";
27
+
28
+ // src/world.ts
29
+ function initWorld() {
30
+ return {
31
+ vars: {},
32
+ headers: {},
33
+ cleanup: []
34
+ };
35
+ }
36
+
37
+ // src/adapters/api/playwright-api.adapter.ts
38
+ var PlaywrightApiAdapter = class {
39
+ constructor(request) {
40
+ this.request = request;
41
+ }
42
+ async sendJson(method, path, body, headers) {
43
+ const opts = {
44
+ headers: { Accept: "application/json", ...headers || {} }
45
+ };
46
+ if (body !== void 0) {
47
+ opts.data = body;
48
+ opts.headers["Content-Type"] = "application/json";
49
+ }
50
+ const resp = await this.request.fetch(path, { method, ...opts });
51
+ const text = await resp.text();
52
+ const respHeaders = resp.headers();
53
+ const contentType = respHeaders["content-type"] || "";
54
+ const json = contentType.includes("application/json") ? tryParseJson(text) : tryParseJson(text);
55
+ return {
56
+ status: resp.status(),
57
+ text,
58
+ json,
59
+ headers: respHeaders,
60
+ contentType,
61
+ response: resp
62
+ };
63
+ }
64
+ async sendForm(method, path, form, headers) {
65
+ const body = new URLSearchParams(form).toString();
66
+ const resp = await this.request.fetch(path, {
67
+ method,
68
+ headers: {
69
+ Accept: "application/json",
70
+ "Content-Type": "application/x-www-form-urlencoded",
71
+ ...headers || {}
72
+ },
73
+ data: body
74
+ });
75
+ const text = await resp.text();
76
+ const respHeaders = resp.headers();
77
+ const contentType = respHeaders["content-type"] || "";
78
+ const json = contentType.includes("application/json") ? tryParseJson(text) : tryParseJson(text);
79
+ return {
80
+ status: resp.status(),
81
+ text,
82
+ json,
83
+ headers: respHeaders,
84
+ contentType,
85
+ response: resp
86
+ };
87
+ }
88
+ };
89
+
90
+ // src/adapters/ui/playwright-ui.adapter.ts
91
+ import { expect } from "@playwright/test";
92
+ var PlaywrightUiAdapter = class {
93
+ constructor(page) {
94
+ this.page = page;
95
+ }
96
+ async goto(path) {
97
+ await this.page.goto(path);
98
+ await this.page.waitForLoadState("domcontentloaded");
99
+ }
100
+ async clickButton(name) {
101
+ await this.page.getByRole("button", { name }).first().click();
102
+ }
103
+ async clickLink(name) {
104
+ await this.page.getByRole("link", { name }).first().click();
105
+ }
106
+ async fillPlaceholder(placeholder, value) {
107
+ await this.page.getByPlaceholder(placeholder).first().fill(value);
108
+ }
109
+ async fillLabel(label, value) {
110
+ await this.page.getByLabel(label).first().fill(value);
111
+ }
112
+ async expectText(text) {
113
+ await expect(this.page.getByText(text).first()).toBeVisible();
114
+ }
115
+ async expectUrlContains(part) {
116
+ await expect(this.page).toHaveURL(new RegExp(part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
117
+ }
118
+ async goBack() {
119
+ await this.page.goBack();
120
+ }
121
+ async reload() {
122
+ await this.page.reload();
123
+ }
124
+ async waitSeconds(seconds) {
125
+ await this.page.waitForTimeout(seconds * 1e3);
126
+ }
127
+ async waitForPageLoad() {
128
+ await this.page.waitForLoadState("domcontentloaded");
129
+ await this.page.waitForLoadState("load");
130
+ try {
131
+ await this.page.waitForLoadState("networkidle", { timeout: 5e3 });
132
+ } catch {
133
+ }
134
+ }
135
+ async getCurrentUrl() {
136
+ return this.page.url();
137
+ }
138
+ async zoomTo(scale) {
139
+ await this.page.evaluate((z) => {
140
+ document.documentElement.style.zoom = String(z);
141
+ }, scale);
142
+ }
143
+ async typeText(text) {
144
+ await this.page.keyboard.type(text);
145
+ }
146
+ async pressKey(key) {
147
+ await this.page.keyboard.press(key);
148
+ }
149
+ async clickElementThatContains(clickMode, elementType, text) {
150
+ const locator = this.page.locator(elementType).filter({ hasText: text }).first();
151
+ await this.performClick(locator, clickMode);
152
+ }
153
+ async clickElementWith(clickMode, ordinal, text, method) {
154
+ const idx = this.parseOrdinal(ordinal);
155
+ const locator = this.locatorBy(method, text).nth(idx);
156
+ await this.performClick(locator, clickMode);
157
+ }
158
+ async fillDropdown(value, dropdownLabel) {
159
+ const select = this.page.getByLabel(dropdownLabel).first();
160
+ try {
161
+ await select.selectOption({ label: value });
162
+ } catch {
163
+ await select.selectOption(value);
164
+ }
165
+ }
166
+ async inputInElement(action, value, ordinal, text, method) {
167
+ const idx = this.parseOrdinal(ordinal);
168
+ const locator = this.locatorBy(method, text).nth(idx);
169
+ if (action === "type") {
170
+ await locator.type(value);
171
+ return;
172
+ }
173
+ if (action === "fill") {
174
+ await locator.fill(value);
175
+ return;
176
+ }
177
+ try {
178
+ await locator.selectOption({ label: value });
179
+ } catch {
180
+ await locator.click({ force: true }).catch(() => {
181
+ });
182
+ await this.page.getByRole("option", { name: value }).first().click().catch(() => {
183
+ });
184
+ await this.page.getByText(value).first().click();
185
+ }
186
+ }
187
+ async expectUrl(mode, expected) {
188
+ this.assertUrlAgainst(this.page.url(), mode, expected);
189
+ }
190
+ async expectNewTabUrl(mode, expected) {
191
+ const context = this.page.context();
192
+ const alreadyOpen = context.pages().filter((p) => p !== this.page);
193
+ const newPage = alreadyOpen.length > 0 ? alreadyOpen[alreadyOpen.length - 1] : await context.waitForEvent("page", {
194
+ timeout: 1e4
195
+ });
196
+ await newPage.waitForLoadState("domcontentloaded");
197
+ this.assertUrlAgainst(newPage.url(), mode, expected);
198
+ }
199
+ async expectElementWithTextVisible(elementType, text, shouldBeVisible) {
200
+ const locator = this.page.locator(elementType).filter({ hasText: text });
201
+ if (shouldBeVisible) {
202
+ await expect(locator.first()).toBeVisible();
203
+ return;
204
+ }
205
+ if (await locator.count() === 0) {
206
+ await expect(locator).toHaveCount(0);
207
+ return;
208
+ }
209
+ await expect(locator.first()).toBeHidden();
210
+ }
211
+ async expectElementState(ordinal, text, method, state) {
212
+ const idx = this.parseOrdinal(ordinal);
213
+ const locator = this.locatorBy(method, text).nth(idx);
214
+ await this.expectState(locator, state);
215
+ }
216
+ async expectElementStateWithin(ordinal, text, method, state, seconds) {
217
+ const idx = this.parseOrdinal(ordinal);
218
+ const locator = this.locatorBy(method, text).nth(idx);
219
+ await this.expectState(locator, state, { timeout: seconds * 1e3 });
220
+ }
221
+ parseOrdinal(ordinal) {
222
+ const n = Number.parseInt(String(ordinal), 10);
223
+ if (!Number.isFinite(n) || n < 1) {
224
+ throw new Error(`Invalid element ordinal '${ordinal}'. Expected 1,2,3...`);
225
+ }
226
+ return n - 1;
227
+ }
228
+ locatorBy(method, text) {
229
+ switch (method) {
230
+ case "text":
231
+ return this.page.getByText(text);
232
+ case "label":
233
+ return this.page.getByLabel(text);
234
+ case "placeholder":
235
+ return this.page.getByPlaceholder(text);
236
+ case "role":
237
+ return this.page.getByRole(text);
238
+ case "test ID":
239
+ return this.page.getByTestId(text);
240
+ case "alternative text":
241
+ return this.page.getByAltText(text);
242
+ case "title":
243
+ return this.page.getByTitle(text);
244
+ case "locator":
245
+ return this.page.locator(text);
246
+ default: {
247
+ const neverMethod = method;
248
+ throw new Error(`Unsupported locator method: ${neverMethod}`);
249
+ }
250
+ }
251
+ }
252
+ async performClick(locator, clickMode) {
253
+ if (clickMode === "dispatch click") {
254
+ await locator.dispatchEvent("click");
255
+ return;
256
+ }
257
+ if (clickMode === "force click") {
258
+ await locator.click({ force: true });
259
+ return;
260
+ }
261
+ if (clickMode === "force dispatch click") {
262
+ await locator.dispatchEvent("click");
263
+ return;
264
+ }
265
+ await locator.click();
266
+ }
267
+ async expectState(locator, state, options) {
268
+ if (state === "visible") {
269
+ await expect(locator).toBeVisible(options);
270
+ return;
271
+ }
272
+ if (state === "hidden") {
273
+ await expect(locator).toBeHidden(options);
274
+ return;
275
+ }
276
+ if (state === "editable") {
277
+ await expect(locator).toBeEditable(options);
278
+ return;
279
+ }
280
+ if (state === "disabled") {
281
+ await expect(locator).toBeDisabled(options);
282
+ return;
283
+ }
284
+ if (state === "enabled") {
285
+ await expect(locator).toBeEnabled(options);
286
+ return;
287
+ }
288
+ if (state === "read-only") {
289
+ await expect(locator).toHaveJSProperty("readOnly", true, options);
290
+ return;
291
+ }
292
+ const neverState = state;
293
+ throw new Error(`Unsupported expected state: ${neverState}`);
294
+ }
295
+ assertUrlAgainst(actualUrl, mode, expected) {
296
+ const expectedStr = String(expected);
297
+ const assertFor = (actual) => {
298
+ if (mode === "contains") {
299
+ expect(actual).toContain(expectedStr);
300
+ return;
301
+ }
302
+ if (mode === "doesntContain") {
303
+ expect(actual).not.toContain(expectedStr);
304
+ return;
305
+ }
306
+ if (mode === "equals") {
307
+ expect(actual).toBe(expectedStr);
308
+ return;
309
+ }
310
+ const neverMode = mode;
311
+ throw new Error(`Unsupported URL assertion mode: ${neverMode}`);
312
+ };
313
+ if (expectedStr.startsWith("/")) {
314
+ const u = new URL(actualUrl);
315
+ const actualPath = `${u.pathname}${u.search}${u.hash}`;
316
+ assertFor(actualPath);
317
+ return;
318
+ }
319
+ assertFor(actualUrl);
320
+ }
321
+ };
322
+
323
+ // src/adapters/auth/universal-auth.adapter.ts
324
+ var UniversalAuthAdapter = class {
325
+ constructor(deps) {
326
+ this.deps = deps;
327
+ }
328
+ apiSetBearer(world, token) {
329
+ world.headers = { ...world.headers || {}, Authorization: `Bearer ${token}` };
330
+ }
331
+ async apiLoginAsAdmin(world) {
332
+ const username = process.env.DEFAULT_ADMIN_USERNAME || process.env.DEFAULT_ADMIN_EMAIL || "admin@prima.com";
333
+ const password = process.env.DEFAULT_ADMIN_PASSWORD || "admin1234";
334
+ await this.apiLogin(world, username, password);
335
+ }
336
+ async apiLoginAsUser(world) {
337
+ const username = process.env.DEFAULT_USER_USERNAME || process.env.NON_ADMIN_USERNAME || "bob@bob.com";
338
+ const password = process.env.DEFAULT_USER_PASSWORD || process.env.NON_ADMIN_PASSWORD || "bob1234";
339
+ await this.apiLogin(world, username, password);
340
+ }
341
+ async apiLogin(world, username, password) {
342
+ const loginPath = process.env.API_AUTH_LOGIN_PATH || "/auth/login";
343
+ const result = await this.deps.api.sendForm("POST", loginPath, { username, password });
344
+ world.lastStatus = result.status;
345
+ world.lastText = result.text;
346
+ world.lastJson = result.json;
347
+ world.lastHeaders = result.headers;
348
+ const json = result.json || {};
349
+ const token = json?.access_token;
350
+ if (typeof token !== "string" || !token) {
351
+ throw new Error(`API login failed for ${username}: ${result.status} ${result.text}`);
352
+ }
353
+ this.apiSetBearer(world, token);
354
+ }
355
+ async uiLoginAsAdmin(world) {
356
+ const username = process.env.DEFAULT_ADMIN_USERNAME || process.env.DEFAULT_ADMIN_EMAIL || "admin@prima.com";
357
+ const password = process.env.DEFAULT_ADMIN_PASSWORD || "admin1234";
358
+ await this.uiLogin(world, username, password);
359
+ }
360
+ async uiLoginAsUser(world) {
361
+ const username = process.env.DEFAULT_USER_USERNAME || process.env.NON_ADMIN_USERNAME || "bob@bob.com";
362
+ const password = process.env.DEFAULT_USER_PASSWORD || process.env.NON_ADMIN_PASSWORD || "bob1234";
363
+ await this.uiLogin(world, username, password);
364
+ }
365
+ async uiLogin(_world, username, password) {
366
+ await this.deps.ui.goto("/login");
367
+ await this.deps.ui.fillPlaceholder("Username", username);
368
+ await this.deps.ui.fillPlaceholder("Password", password);
369
+ await this.deps.ui.clickButton("Login");
370
+ }
371
+ };
372
+
373
+ // src/adapters/cleanup/default-cleanup.adapter.ts
374
+ function looksTesty(meta) {
375
+ const s = typeof meta === "string" ? meta : JSON.stringify(meta ?? "");
376
+ return /__|run[0-9a-fA-F]{4,}|test/i.test(s);
377
+ }
378
+ function isUuidLike(value) {
379
+ return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(value);
380
+ }
381
+ function matchVar(rule, varNameLower) {
382
+ const m = rule.varMatch;
383
+ if (m.startsWith("/") && m.endsWith("/") && m.length > 2) {
384
+ const re = new RegExp(m.slice(1, -1));
385
+ return re.test(varNameLower);
386
+ }
387
+ return varNameLower.includes(m.toLowerCase());
388
+ }
389
+ function loadRulesFromEnv() {
390
+ const raw = process.env.CLEANUP_RULES?.trim();
391
+ if (!raw) return [];
392
+ try {
393
+ const parsed = JSON.parse(raw);
394
+ if (Array.isArray(parsed)) {
395
+ return parsed.filter((x) => x && typeof x === "object").map((x) => ({
396
+ varMatch: String(x.varMatch ?? ""),
397
+ method: x.method ? String(x.method).toUpperCase() : void 0,
398
+ path: String(x.path ?? "")
399
+ })).filter((r) => r.varMatch && r.path);
400
+ }
401
+ } catch {
402
+ }
403
+ return [];
404
+ }
405
+ var defaultRules = [
406
+ { varMatch: "tool_provider", path: "/admin/tool/providers/{id}" },
407
+ { varMatch: "extsvc", path: "/admin/tool/external-services/{id}" },
408
+ { varMatch: "external_service", path: "/admin/tool/external-services/{id}" },
409
+ { varMatch: "workspace", path: "/admin/workspaces/{id}" },
410
+ { varMatch: "team", path: "/admin/teams/{id}" },
411
+ { varMatch: "prima_model", path: "/admin/llm/prima-models/{id}" },
412
+ { varMatch: "pm", path: "/admin/llm/prima-models/{id}" },
413
+ { varMatch: "cred", path: "/admin/llm/provider-credentials/{id}" },
414
+ { varMatch: "user", path: "/admin/users/{id}" },
415
+ { varMatch: "manager", path: "/admin/users/{id}" },
416
+ { varMatch: "member", path: "/admin/users/{id}" },
417
+ { varMatch: "creator", path: "/admin/users/{id}" },
418
+ { varMatch: "rule", path: "/admin/llm/guardrail-rules/{id}" }
419
+ ];
420
+ var DefaultCleanupAdapter = class {
421
+ constructor(input) {
422
+ const fromEnv = loadRulesFromEnv();
423
+ this.rules = input?.rules && input.rules.length ? input.rules : [...fromEnv, ...defaultRules];
424
+ this.allowHeuristic = input?.allowHeuristic ?? /^(1|true|yes|on)$/i.test(process.env.CLEANUP_ALLOW_ALL || "");
425
+ }
426
+ registerFromVar(world, varName, id, meta) {
427
+ if (!id) return;
428
+ const idStr = String(id);
429
+ if (!isUuidLike(idStr)) return;
430
+ if (!this.allowHeuristic) {
431
+ if (!looksTesty(meta) && !looksTesty(varName)) return;
432
+ }
433
+ const name = (varName || "").toLowerCase();
434
+ const rule = this.rules.find((r) => matchVar(r, name));
435
+ if (!rule) return;
436
+ const path = rule.path.replace(/\{id\}/g, idStr);
437
+ registerCleanup(world, { method: rule.method ?? "DELETE", path });
438
+ }
439
+ };
440
+
441
+ // src/fixtures.ts
442
+ var cachedAdminToken;
443
+ async function getAdminHeaders(request) {
444
+ if (cachedAdminToken) return { Authorization: `Bearer ${cachedAdminToken}` };
445
+ const username = process.env.DEFAULT_ADMIN_USERNAME || process.env.DEFAULT_ADMIN_EMAIL || "admin@prima.com";
446
+ const password = process.env.DEFAULT_ADMIN_PASSWORD || "admin1234";
447
+ const loginPath = process.env.API_AUTH_LOGIN_PATH || "/auth/login";
448
+ const body = new URLSearchParams({ username, password }).toString();
449
+ const resp = await request.post(loginPath, {
450
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
451
+ data: body
452
+ });
453
+ if (!resp.ok()) {
454
+ console.warn(`cleanup auth failed: ${resp.status()} ${resp.statusText()}`);
455
+ cachedAdminToken = void 0;
456
+ return {};
457
+ }
458
+ const json = await resp.json();
459
+ const token = json?.access_token;
460
+ if (typeof token === "string" && token) {
461
+ cachedAdminToken = token;
462
+ return { Authorization: `Bearer ${token}` };
463
+ }
464
+ console.warn("cleanup auth missing access_token in response");
465
+ cachedAdminToken = void 0;
466
+ return {};
467
+ }
468
+ function createBddTest(options = {}) {
469
+ const {
470
+ createApi = ({ apiRequest }) => new PlaywrightApiAdapter(apiRequest),
471
+ createUi = ({ page }) => new PlaywrightUiAdapter(page),
472
+ createAuth = ({ api, ui }) => new UniversalAuthAdapter({ api, ui }),
473
+ createCleanup = () => new DefaultCleanupAdapter(),
474
+ createTui,
475
+ worldFactory = initWorld
476
+ } = options;
477
+ return base.extend({
478
+ world: async ({ apiRequest }, use) => {
479
+ const w = worldFactory();
480
+ await use(w);
481
+ if (w.skipCleanup) return;
482
+ if (!w.cleanup.length) return;
483
+ for (const item of [...w.cleanup].reverse()) {
484
+ const adminHeaders = await getAdminHeaders(apiRequest);
485
+ const headers = { ...adminHeaders, ...item.headers || {} };
486
+ try {
487
+ const resp = await apiRequest.fetch(item.path, {
488
+ method: item.method,
489
+ headers
490
+ });
491
+ const status = resp.status();
492
+ if (status === 401 || status === 403) {
493
+ cachedAdminToken = void 0;
494
+ console.warn(`cleanup auth expired (${status}) for ${item.method} ${item.path}`);
495
+ continue;
496
+ }
497
+ if (status >= 400 && status !== 404) {
498
+ console.warn(`cleanup ${item.method} ${item.path} failed`, status);
499
+ }
500
+ } catch (err) {
501
+ console.warn("cleanup error", item.method, item.path, err);
502
+ }
503
+ }
504
+ },
505
+ apiRequest: async ({ playwright }, use, testInfo) => {
506
+ const projectName = String(testInfo.project.name || "");
507
+ const baseURLFromProject = testInfo.project.use?.baseURL;
508
+ const baseURL = process.env.API_BASE_URL || process.env.CONTROL_TOWER_BASE_URL || (projectName.includes("api") ? baseURLFromProject : void 0) || (process.env.CONTROL_TOWER_PORT ? `http://localhost:${process.env.CONTROL_TOWER_PORT}` : void 0) || "http://localhost:4000";
509
+ const ctx = await playwright.request.newContext({ baseURL });
510
+ try {
511
+ await use(ctx);
512
+ } finally {
513
+ await ctx.dispose();
514
+ }
515
+ },
516
+ api: async ({ apiRequest }, use) => {
517
+ await use(createApi({ apiRequest }));
518
+ },
519
+ cleanup: async ({ apiRequest }, use) => {
520
+ await use(createCleanup({ apiRequest }));
521
+ },
522
+ ui: async ({ page }, use) => {
523
+ await use(createUi({ page }));
524
+ },
525
+ auth: async ({ api, ui }, use) => {
526
+ await use(createAuth({ api, ui }));
527
+ },
528
+ /**
529
+ * TUI fixture for terminal user interface testing.
530
+ * Automatically stops the TUI application after the test completes.
531
+ */
532
+ tui: async ({}, use) => {
533
+ const tuiAdapter = createTui?.();
534
+ await use(tuiAdapter);
535
+ if (tuiAdapter?.isRunning?.()) {
536
+ try {
537
+ await tuiAdapter.stop();
538
+ } catch (error) {
539
+ console.warn("Error stopping TUI adapter:", error);
540
+ }
541
+ }
542
+ }
543
+ });
544
+ }
545
+
546
+ // src/adapters/tui/tui-tester.adapter.ts
547
+ var DEFAULT_WAIT_OPTIONS = {
548
+ timeout: 3e4,
549
+ interval: 100
550
+ };
551
+ var KEY_MAP = {
552
+ enter: "enter",
553
+ return: "enter",
554
+ tab: "tab",
555
+ escape: "escape",
556
+ esc: "escape",
557
+ backspace: "backspace",
558
+ delete: "delete",
559
+ up: "up",
560
+ down: "down",
561
+ left: "left",
562
+ right: "right",
563
+ home: "home",
564
+ end: "end",
565
+ pageup: "pageup",
566
+ pagedown: "pagedown",
567
+ space: "space",
568
+ f1: "f1",
569
+ f2: "f2",
570
+ f3: "f3",
571
+ f4: "f4",
572
+ f5: "f5",
573
+ f6: "f6",
574
+ f7: "f7",
575
+ f8: "f8",
576
+ f9: "f9",
577
+ f10: "f10",
578
+ f11: "f11",
579
+ f12: "f12"
580
+ };
581
+ var TuiTesterAdapter = class {
582
+ constructor(config) {
583
+ this.tester = null;
584
+ this.running = false;
585
+ this.tuiTesterModule = null;
586
+ this.config = {
587
+ size: { cols: 80, rows: 24 },
588
+ ...config
589
+ };
590
+ }
591
+ /**
592
+ * Lazily load the tui-tester module to support optional dependency
593
+ */
594
+ async loadTuiTester() {
595
+ if (this.tuiTesterModule) {
596
+ return this.tuiTesterModule;
597
+ }
598
+ try {
599
+ const module = await import("tui-tester");
600
+ this.tuiTesterModule = module;
601
+ return module;
602
+ } catch (error) {
603
+ throw new Error(
604
+ "tui-tester is not installed. Please install it with: npm install tui-tester\nAlso ensure tmux is installed on your system."
605
+ );
606
+ }
607
+ }
608
+ /**
609
+ * Get the tester instance, throwing if not started
610
+ */
611
+ getTester() {
612
+ if (!this.tester) {
613
+ throw new Error("TUI tester not started. Call start() first.");
614
+ }
615
+ return this.tester;
616
+ }
617
+ // ═══════════════════════════════════════════════════════════════════════════
618
+ // Lifecycle Methods
619
+ // ═══════════════════════════════════════════════════════════════════════════
620
+ async start() {
621
+ if (this.running) {
622
+ return;
623
+ }
624
+ const { TmuxTester } = await this.loadTuiTester();
625
+ this.tester = new TmuxTester({
626
+ command: this.config.command,
627
+ size: this.config.size,
628
+ cwd: this.config.cwd,
629
+ env: this.config.env,
630
+ debug: this.config.debug,
631
+ shell: this.config.shell,
632
+ snapshotDir: this.config.snapshotDir
633
+ });
634
+ await this.tester.start();
635
+ this.running = true;
636
+ }
637
+ async stop() {
638
+ if (!this.running || !this.tester) {
639
+ return;
640
+ }
641
+ try {
642
+ await this.tester.stop();
643
+ } finally {
644
+ this.running = false;
645
+ this.tester = null;
646
+ }
647
+ }
648
+ async restart() {
649
+ await this.stop();
650
+ await this.start();
651
+ }
652
+ isRunning() {
653
+ return this.running && this.tester !== null;
654
+ }
655
+ // ═══════════════════════════════════════════════════════════════════════════
656
+ // Input Methods
657
+ // ═══════════════════════════════════════════════════════════════════════════
658
+ async typeText(text, options) {
659
+ const tester = this.getTester();
660
+ await tester.typeText(text, options?.delay);
661
+ }
662
+ async pressKey(key, modifiers) {
663
+ const tester = this.getTester();
664
+ const normalizedKey = KEY_MAP[key.toLowerCase()] || key.toLowerCase();
665
+ await tester.sendKey(normalizedKey, {
666
+ ctrl: modifiers?.ctrl,
667
+ alt: modifiers?.alt,
668
+ shift: modifiers?.shift
669
+ });
670
+ }
671
+ async sendText(text) {
672
+ const tester = this.getTester();
673
+ await tester.sendText(text);
674
+ }
675
+ async fillField(fieldLabel, value) {
676
+ const tester = this.getTester();
677
+ await tester.waitForText(fieldLabel, { timeout: DEFAULT_WAIT_OPTIONS.timeout });
678
+ await tester.typeText(value);
679
+ }
680
+ async selectOption(option) {
681
+ const tester = this.getTester();
682
+ await tester.waitForText(option, { timeout: DEFAULT_WAIT_OPTIONS.timeout });
683
+ await tester.sendKey("enter");
684
+ }
685
+ // ═══════════════════════════════════════════════════════════════════════════
686
+ // Mouse Methods
687
+ // ═══════════════════════════════════════════════════════════════════════════
688
+ async sendMouse(event) {
689
+ const tester = this.getTester();
690
+ await tester.sendMouse({
691
+ type: event.type,
692
+ position: event.position,
693
+ button: event.button
694
+ });
695
+ }
696
+ async click(x, y, button = "left") {
697
+ await this.sendMouse({
698
+ type: "click",
699
+ position: { x, y },
700
+ button
701
+ });
702
+ }
703
+ async clickOnText(text) {
704
+ const tester = this.getTester();
705
+ const lines = await tester.getScreenLines();
706
+ let found = false;
707
+ for (let y = 0; y < lines.length; y++) {
708
+ const x = lines[y].indexOf(text);
709
+ if (x !== -1) {
710
+ await this.click(x, y);
711
+ found = true;
712
+ break;
713
+ }
714
+ }
715
+ if (!found) {
716
+ throw new Error(`Text "${text}" not found on screen`);
717
+ }
718
+ }
719
+ // ═══════════════════════════════════════════════════════════════════════════
720
+ // Assertion Methods
721
+ // ═══════════════════════════════════════════════════════════════════════════
722
+ async expectText(text, options) {
723
+ const tester = this.getTester();
724
+ const opts = { ...DEFAULT_WAIT_OPTIONS, ...options };
725
+ await tester.waitForText(text, { timeout: opts.timeout });
726
+ }
727
+ async expectPattern(pattern, options) {
728
+ const tester = this.getTester();
729
+ const opts = { ...DEFAULT_WAIT_OPTIONS, ...options };
730
+ await tester.waitForPattern(pattern, { timeout: opts.timeout });
731
+ }
732
+ async expectNotText(text) {
733
+ const screenText = await this.getScreenText();
734
+ if (screenText.includes(text)) {
735
+ throw new Error(`Expected text "${text}" to NOT be present on screen, but it was found.`);
736
+ }
737
+ }
738
+ async assertScreenContains(text) {
739
+ const tester = this.getTester();
740
+ await tester.assertScreenContains(text);
741
+ }
742
+ async assertScreenMatches(pattern) {
743
+ const screenText = await this.getScreenText();
744
+ if (!pattern.test(screenText)) {
745
+ throw new Error(`Screen content does not match pattern: ${pattern}
746
+ Screen content:
747
+ ${screenText}`);
748
+ }
749
+ }
750
+ // ═══════════════════════════════════════════════════════════════════════════
751
+ // Wait Methods
752
+ // ═══════════════════════════════════════════════════════════════════════════
753
+ async waitForText(text, options) {
754
+ await this.expectText(text, options);
755
+ }
756
+ async waitForPattern(pattern, options) {
757
+ await this.expectPattern(pattern, options);
758
+ }
759
+ async waitForReady() {
760
+ const tester = this.getTester();
761
+ await tester.sleep(500);
762
+ }
763
+ async waitSeconds(seconds) {
764
+ const tester = this.getTester();
765
+ await tester.sleep(seconds * 1e3);
766
+ }
767
+ // ═══════════════════════════════════════════════════════════════════════════
768
+ // Screen Capture Methods
769
+ // ═══════════════════════════════════════════════════════════════════════════
770
+ async captureScreen() {
771
+ const tester = this.getTester();
772
+ const capture = await tester.captureScreen();
773
+ const lines = capture.lines || await tester.getScreenLines();
774
+ return {
775
+ text: capture.text,
776
+ lines,
777
+ timestamp: Date.now(),
778
+ size: this.getSize()
779
+ };
780
+ }
781
+ async getScreenText() {
782
+ const tester = this.getTester();
783
+ return tester.getScreenText();
784
+ }
785
+ async getScreenLines() {
786
+ const tester = this.getTester();
787
+ return tester.getScreenLines();
788
+ }
789
+ // ═══════════════════════════════════════════════════════════════════════════
790
+ // Snapshot Methods
791
+ // ═══════════════════════════════════════════════════════════════════════════
792
+ async takeSnapshot(name) {
793
+ const tester = this.getTester();
794
+ await tester.takeSnapshot(name);
795
+ }
796
+ async matchSnapshot(name) {
797
+ const tester = this.getTester();
798
+ try {
799
+ const result = await tester.compareSnapshot(name);
800
+ return {
801
+ pass: result.pass,
802
+ diff: result.diff
803
+ };
804
+ } catch (error) {
805
+ if (error instanceof Error && error.message.includes("not found")) {
806
+ await this.takeSnapshot(name);
807
+ return { pass: true };
808
+ }
809
+ throw error;
810
+ }
811
+ }
812
+ // ═══════════════════════════════════════════════════════════════════════════
813
+ // Utility Methods
814
+ // ═══════════════════════════════════════════════════════════════════════════
815
+ async clear() {
816
+ const tester = this.getTester();
817
+ await tester.clear();
818
+ }
819
+ async resize(size) {
820
+ const tester = this.getTester();
821
+ await tester.resize(size);
822
+ this.config.size = size;
823
+ }
824
+ getSize() {
825
+ if (this.tester) {
826
+ return this.tester.getSize();
827
+ }
828
+ return this.config.size || { cols: 80, rows: 24 };
829
+ }
830
+ getConfig() {
831
+ return { ...this.config };
832
+ }
833
+ };
834
+
835
+ // src/config.ts
836
+ function tagsForProject({ projectTag, extraTags, defaultExcludes = "not @Skip and not @ignore" }) {
837
+ if (extraTags) return `${defaultExcludes} and ${projectTag} and (${extraTags})`;
838
+ return `${defaultExcludes} and ${projectTag}`;
839
+ }
840
+ function resolveExtraTags(raw) {
841
+ const tagFilterRaw = raw?.trim();
842
+ if (!tagFilterRaw) return void 0;
843
+ const looksLikeExpression = /\s|@|\bnot\b|\band\b|\bor\b/.test(tagFilterRaw);
844
+ if (looksLikeExpression) return tagFilterRaw;
845
+ const parts = tagFilterRaw.split(",").map((s) => s.trim()).filter(Boolean).map((t) => t.startsWith("@") ? t : `@${t}`);
846
+ if (!parts.length) return void 0;
847
+ return parts.join(" or ");
848
+ }
849
+ export {
850
+ DefaultCleanupAdapter,
851
+ PlaywrightApiAdapter,
852
+ PlaywrightUiAdapter,
853
+ TuiTesterAdapter,
854
+ UniversalAuthAdapter,
855
+ assertMasked,
856
+ base as baseTest,
857
+ createBddTest,
858
+ initWorld,
859
+ interpolate,
860
+ parseExpected,
861
+ registerApiAssertionSteps,
862
+ registerApiAuthSteps,
863
+ registerApiHttpSteps,
864
+ registerApiSteps,
865
+ registerCleanup,
866
+ registerHybridSteps,
867
+ registerHybridSuite,
868
+ registerSharedCleanupSteps,
869
+ registerSharedSteps,
870
+ registerSharedVarSteps,
871
+ registerTuiBasicSteps,
872
+ registerTuiSteps,
873
+ registerTuiWizardSteps,
874
+ registerUiBasicSteps,
875
+ registerUiSteps,
876
+ registerWizardSteps,
877
+ resolveExtraTags,
878
+ selectPath,
879
+ tagsForProject,
880
+ tryParseJson
881
+ };