@esimplicitylabs/katalyst-xspec 0.7.1 → 0.8.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/README.md CHANGED
@@ -22,7 +22,7 @@ npm test
22
22
 
23
23
  - **Fixtures**: `createBddTest` wiring world, api/ui/auth/cleanup adapters.
24
24
  - **Ports**: `ApiPort`, `UiPort`, `AuthPort`, `CleanupPort`.
25
- - **Adapters**: Playwright API/UI adapters, default cleanup, example auth adapter.
25
+ - **Adapters**: Playwright API/UI adapters, default cleanup, `UniversalAuthAdapter` (role-based API and UI login).
26
26
  - **Step registrations**: API (auth/http/assertions), UI (basic + wizard), shared vars/cleanup, hybrid helpers.
27
27
  - **Config helpers**: `tagsForProject` / `resolveExtraTags` for optional tag filtering (`@Skip`/`@ignore`, `TEST_TAGS`).
28
28
 
@@ -57,6 +57,20 @@ registerApiSteps(test);
57
57
 
58
58
  3) Configure Playwright projects with your features/steps globs (each project selects feature files by folder; tags are optional). Keep `@playwright/test` and `playwright-bdd` aligned with peer ranges.
59
59
 
60
+ ## Logging in
61
+
62
+ Set credentials per role in `.env` (`AUTH_ADMIN_USERNAME` / `AUTH_ADMIN_PASSWORD`, `AUTH_PM_USERNAME` / ..., any role name) and use the role in features:
63
+
64
+ ```gherkin
65
+ Given I am authenticated as "pm" via API
66
+ Given I am logged in as "pm"
67
+ When I log in as "pm" in UI
68
+ ```
69
+
70
+ Missing credentials fail the step with a message naming the variables. Login endpoint, body format, token location and form fields are set with `API_AUTH_*` / `UI_*` variables; see the [Authentication guide](https://github.com/esimplicityinc/katalyst-xspec/blob/main/docs/guides/authentication.md).
71
+
72
+ `FRONTEND_URL` sets where UI steps go; `API_BASE_URL` is optional (API calls go to `FRONTEND_URL` without it).
73
+
60
74
  ## Publishing (npm)
61
75
 
62
76
  Publishing is automated. The `.github/workflows/publish.yml` workflow builds the package and publishes it to the
package/cli/init.cjs CHANGED
@@ -301,7 +301,7 @@ function templates(packageName) {
301
301
  clean: 'rm -rf .features-gen node_modules test-results storage cucumber-report playwright-report'
302
302
  },
303
303
  devDependencies: {
304
- '@esimplicitylabs/katalyst-xspec': '^0.7.0',
304
+ '@esimplicitylabs/katalyst-xspec': '^0.8.0',
305
305
  '@playwright/test': '^1.49.0',
306
306
  'playwright-bdd': '^9.1.0',
307
307
  dotenv: '^16.1.4',
@@ -335,7 +335,12 @@ function templates(packageName) {
335
335
  export const { test } = createBddTest({
336
336
  createApi: ({ apiRequest }) => new PlaywrightApiAdapter(apiRequest),
337
337
  createUi: ({ page }) => new PlaywrightUiAdapter(page),
338
- createAuth: ({ api, ui }) => new UniversalAuthAdapter({ api, ui }),
338
+ // Logins read AUTH_<ROLE>_USERNAME / AUTH_<ROLE>_PASSWORD and the API_AUTH_* /
339
+ // UI_LOGIN_* settings in .env. You can also give credentials in code:
340
+ // roles: { pm: { username: 'pm@example.com', password: process.env.PM_PASSWORD } },
341
+ // For SSO or unusual login flows, subclass UniversalAuthAdapter and override
342
+ // apiLogin() / uiLogin() (see the Authentication guide).
343
+ createAuth: ({ api, ui }) => new UniversalAuthAdapter({ api, ui, roles: {} }),
339
344
  createCleanup: () => new DefaultCleanupAdapter(),
340
345
  // TUI testing (optional - requires tui-tester and tmux installed)
341
346
  // Uncomment and configure for your CLI application:
@@ -370,7 +375,7 @@ export { test };
370
375
 
371
376
  const playwrightConfig = `import { defineConfig } from '@playwright/test';
372
377
  import { defineBddProject, cucumberReporter } from 'playwright-bdd';
373
- import { resolveWorkers, tagsForProject, resolveExtraTags } from '@esimplicitylabs/katalyst-xspec';
378
+ import { resolveWorkers, tagsForProject, resolveExtraTags, resolveTargets, logTargets } from '@esimplicitylabs/katalyst-xspec';
374
379
  import dotenv from 'dotenv';
375
380
  import fs from 'node:fs';
376
381
  import path from 'node:path';
@@ -388,6 +393,10 @@ if (fs.existsSync(localEnvPath)) {
388
393
  dotenv.config();
389
394
  }
390
395
 
396
+ // UI and API base URLs from FRONTEND_URL / API_BASE_URL (API falls back to the
397
+ // frontend URL). Printed once per run; silence with KATALYST_XSPEC_QUIET=true.
398
+ const targets = logTargets(resolveTargets());
399
+
391
400
  // Each project runs the feature files in its folder. Any scenario can use any
392
401
  // step (API, UI, shared). Tags are optional: use your own (e.g. @smoke) and
393
402
  // filter with TEST_TAGS="@smoke"; scenarios tagged @Skip or @ignore are skipped.
@@ -425,7 +434,7 @@ export default defineConfig({
425
434
  // Add tuiBdd to this array when TUI testing is enabled
426
435
  projects: [apiBdd, uiBdd /* , tuiBdd */],
427
436
  use: {
428
- baseURL: process.env.BASE_URL || process.env.FRONTEND_URL || 'http://localhost:3000',
437
+ baseURL: targets.frontendUrl,
429
438
  headless: process.env.HEADLESS === 'false' ? false : true,
430
439
  },
431
440
  });
@@ -480,27 +489,45 @@ storage
480
489
  .env
481
490
  `;
482
491
 
483
- const envExample = `# API defaults used by the auth and cleanup helpers
484
- DEFAULT_ADMIN_USERNAME=admin@example.com
485
- DEFAULT_ADMIN_PASSWORD=changeme
486
- API_AUTH_LOGIN_PATH=/auth/login
487
- API_BASE_URL=http://localhost:3000
488
-
489
- # UI defaults
492
+ const envExample = `# ── Where to test ──────────────────────────────────────────────
493
+ # Relative paths in steps ("/login", "/api/users") use these.
494
+ # API_BASE_URL is optional: without it, API calls go to FRONTEND_URL.
490
495
  FRONTEND_URL=http://localhost:3000
496
+ # API_BASE_URL=http://localhost:4000
491
497
  HEADLESS=true
492
498
 
493
- # Cleanup rules (JSON array)
499
+ # ── Who logs in ────────────────────────────────────────────────
500
+ # One pair per role. Use any role name: "pm" reads AUTH_PM_USERNAME/PASSWORD.
501
+ # Given I am authenticated as "admin" via API
502
+ # Given I am logged in as "admin"
503
+ AUTH_ADMIN_USERNAME=admin@example.com
504
+ AUTH_ADMIN_PASSWORD=changeme
505
+ # AUTH_USER_USERNAME=user@example.com
506
+ # AUTH_USER_PASSWORD=changeme
507
+
508
+ # ── API login (defaults shown) ─────────────────────────────────
509
+ # API_AUTH_LOGIN_PATH=/auth/login
510
+ # API_AUTH_BODY=form # or json
511
+ # API_AUTH_USERNAME_FIELD=username # e.g. email
512
+ # API_AUTH_PASSWORD_FIELD=password
513
+ # API_AUTH_TOKEN_PATH=access_token # e.g. data.token; default tries common names.
514
+ # # No token but a session cookie also works.
515
+
516
+ # ── UI login (defaults shown) ──────────────────────────────────
517
+ # UI_LOGIN_PATH=/login
518
+ # UI_USERNAME_FIELD=Username # label, placeholder or name of the field
519
+ # UI_PASSWORD_FIELD=Password
520
+ # UI_LOGIN_BUTTON=Login
521
+ # UI_LOGIN_SUCCESS_URL=/dashboard # default: any page other than the login page
522
+ # UI_LOGIN_SUCCESS_TEXT=Welcome # use if the URL doesn't change after login
523
+ # UI_SESSION_REUSE=true # "I am logged in as" reuses the session
524
+
525
+ # ── Other ──────────────────────────────────────────────────────
494
526
  # CLEANUP_RULES=[{"varMatch":"user","path":"/api/users/{id}"}]
495
-
496
- # TUI testing (optional)
497
- # Set DEBUG=true to see TUI tester output
498
- DEBUG=false
499
-
500
- # Worker configuration
501
- # Set to a number for explicit worker count, or "auto" to let Playwright decide
502
- # In CI, defaults to 1 for stability unless explicitly overridden
503
- # WORKERS=auto
527
+ # TEST_TAGS=@smoke # run only scenarios with these tags
528
+ # WORKERS=auto # defaults to 1 in CI
529
+ # KATALYST_XSPEC_QUIET=true # don't print the targets line
530
+ # DEBUG=false # TUI tester output
504
531
  `;
505
532
 
506
533
  const readme = `# ${packageName}
package/cli/upgrade.cjs CHANGED
@@ -553,7 +553,7 @@ function getTemplates() {
553
553
  clean: 'rm -rf .features-gen node_modules test-results storage cucumber-report playwright-report'
554
554
  },
555
555
  devDependencies: {
556
- '@esimplicitylabs/katalyst-xspec': '^0.7.0',
556
+ '@esimplicitylabs/katalyst-xspec': '^0.8.0',
557
557
  '@playwright/test': '^1.49.0',
558
558
  'playwright-bdd': '^9.1.0',
559
559
  dotenv: '^16.1.4',
@@ -576,7 +576,12 @@ function getTemplates() {
576
576
  export const { test } = createBddTest({
577
577
  createApi: ({ apiRequest }) => new PlaywrightApiAdapter(apiRequest),
578
578
  createUi: ({ page }) => new PlaywrightUiAdapter(page),
579
- createAuth: ({ api, ui }) => new UniversalAuthAdapter({ api, ui }),
579
+ // Logins read AUTH_<ROLE>_USERNAME / AUTH_<ROLE>_PASSWORD and the API_AUTH_* /
580
+ // UI_LOGIN_* settings in .env. You can also give credentials in code:
581
+ // roles: { pm: { username: 'pm@example.com', password: process.env.PM_PASSWORD } },
582
+ // For SSO or unusual login flows, subclass UniversalAuthAdapter and override
583
+ // apiLogin() / uiLogin() (see the Authentication guide).
584
+ createAuth: ({ api, ui }) => new UniversalAuthAdapter({ api, ui, roles: {} }),
580
585
  createCleanup: () => new DefaultCleanupAdapter(),
581
586
  // TUI testing (optional - requires tui-tester and tmux installed)
582
587
  // Uncomment and configure for your CLI application:
@@ -634,6 +639,7 @@ async function migrate(cwd, options) {
634
639
 
635
640
  log('Starting migration...');
636
641
  log(`Backup directory: ${backupDir}`);
642
+ const templates = getTemplates();
637
643
  console.log('');
638
644
 
639
645
  // =========================================================================
@@ -714,6 +720,7 @@ async function migrate(cwd, options) {
714
720
  }
715
721
  if (existingFixturesTs) {
716
722
  fs.writeFileSync(path.join(backupDir, 'steps', 'fixtures.ts.original'), existingFixturesTs);
723
+ fs.writeFileSync(path.join(backupDir, 'steps', 'fixtures.ts.template'), templates['features/steps/fixtures.ts']);
717
724
  }
718
725
  }
719
726
 
@@ -728,7 +735,6 @@ async function migrate(cwd, options) {
728
735
  // =========================================================================
729
736
  log('Phase 3: Merging configurations...');
730
737
 
731
- const templates = getTemplates();
732
738
  const filesToUpdate = {};
733
739
 
734
740
  // Merge package.json
@@ -745,13 +751,18 @@ async function migrate(cwd, options) {
745
751
  );
746
752
  console.log(` steps.ts: merged (${customImports.length} custom imports preserved)`);
747
753
 
748
- // Merge fixtures.ts
749
- filesToUpdate['features/steps/fixtures.ts'] = mergeFixturesTs(
750
- existingFixturesTs,
751
- templates['features/steps/fixtures.ts'],
752
- cleanupRules
753
- );
754
- console.log(` fixtures.ts: merged (cleanup rules ${cleanupRules ? 'preserved' : 'using defaults'})`);
754
+ // fixtures.ts is your adapter wiring (custom auth, roles, cleanup rules): keep
755
+ // it, and leave the current template in the backup folder for comparison.
756
+ if (existingFixturesTs) {
757
+ console.log(` fixtures.ts: kept (template saved as ${path.join(backupDir, 'steps', 'fixtures.ts.template')})`);
758
+ } else {
759
+ filesToUpdate['features/steps/fixtures.ts'] = mergeFixturesTs(
760
+ existingFixturesTs,
761
+ templates['features/steps/fixtures.ts'],
762
+ cleanupRules
763
+ );
764
+ console.log(' fixtures.ts: created');
765
+ }
755
766
  console.log('');
756
767
 
757
768
  // =========================================================================
@@ -145,13 +145,34 @@ function registerApiAssertionSteps(test) {
145
145
 
146
146
  // src/steps/api.auth.ts
147
147
  import { createBdd as createBdd3 } from "playwright-bdd";
148
+
149
+ // src/auth/login-steps.ts
150
+ async function apiLoginAsRole(auth, world, role) {
151
+ const name = interpolate(role, world.vars);
152
+ if (auth.apiLoginAs) return auth.apiLoginAs(world, name);
153
+ if (name === "admin") return auth.apiLoginAsAdmin(world);
154
+ if (name === "user") return auth.apiLoginAsUser(world);
155
+ throw new Error(`Your auth adapter doesn't implement apiLoginAs(world, role), so it can't log in as "${name}".`);
156
+ }
157
+ async function uiLoginAsRole(auth, world, role, options) {
158
+ const name = interpolate(role, world.vars);
159
+ if (auth.uiLoginAs) return auth.uiLoginAs(world, name, options);
160
+ if (name === "admin") return auth.uiLoginAsAdmin(world);
161
+ if (name === "user") return auth.uiLoginAsUser(world);
162
+ throw new Error(`Your auth adapter doesn't implement uiLoginAs(world, role), so it can't log in as "${name}".`);
163
+ }
164
+
165
+ // src/steps/api.auth.ts
148
166
  function registerApiAuthSteps(test) {
149
167
  const { Given } = createBdd3(test);
168
+ Given("I am authenticated as {string} via API", async ({ auth, world }, role) => {
169
+ await apiLoginAsRole(auth, world, role);
170
+ });
150
171
  Given("I am authenticated as an admin via API", async ({ auth, world }) => {
151
- await auth.apiLoginAsAdmin(world);
172
+ await apiLoginAsRole(auth, world, "admin");
152
173
  });
153
174
  Given("I am authenticated as a user via API", async ({ auth, world }) => {
154
- await auth.apiLoginAsUser(world);
175
+ await apiLoginAsRole(auth, world, "user");
155
176
  });
156
177
  Given("I set bearer token from variable {string}", async ({ auth, world }, varName) => {
157
178
  const token = world.vars[varName];
@@ -353,11 +374,21 @@ function registerUiBasicSteps(test) {
353
374
  When("I fill in {string} with {string}", async ({ ui, world }, label, value) => {
354
375
  await ui.fillLabel(interpolate(label, world.vars), interpolate(value, world.vars));
355
376
  });
356
- When("I log in as admin in UI", async ({ auth, world }) => {
357
- await auth.uiLoginAsAdmin(world);
377
+ Given("I am logged in as {string}", async ({ auth, ui, world }, role) => {
378
+ void ui;
379
+ await uiLoginAsRole(auth, world, role, { reuseSession: true });
380
+ });
381
+ When("I log in as {string} in UI", async ({ auth, ui, world }, role) => {
382
+ void ui;
383
+ await uiLoginAsRole(auth, world, role);
384
+ });
385
+ When("I log in as admin in UI", async ({ auth, ui, world }) => {
386
+ void ui;
387
+ await uiLoginAsRole(auth, world, "admin");
358
388
  });
359
- When("I log in as user in UI", async ({ auth, world }) => {
360
- await auth.uiLoginAsUser(world);
389
+ When("I log in as user in UI", async ({ auth, ui, world }) => {
390
+ void ui;
391
+ await uiLoginAsRole(auth, world, "user");
361
392
  });
362
393
  When("I click the element {string}", async ({ page, world }, selector) => {
363
394
  await page.locator(interpolate(selector, world.vars)).click();
@@ -1528,6 +1559,8 @@ export {
1528
1559
  setupBearerAuth,
1529
1560
  registerApiHttpSteps,
1530
1561
  registerApiAssertionSteps,
1562
+ apiLoginAsRole,
1563
+ uiLoginAsRole,
1531
1564
  registerApiAuthSteps,
1532
1565
  registerHybridSteps,
1533
1566
  registerSharedCleanupSteps,
package/dist/index.d.ts CHANGED
@@ -68,14 +68,51 @@ interface UiPort {
68
68
  expectElementWithTextVisible(elementType: string, text: string, shouldBeVisible: boolean): Promise<void>;
69
69
  expectElementState(ordinal: string, text: string, method: UiLocatorMethod, state: UiElementState): Promise<void>;
70
70
  expectElementStateWithin(ordinal: string, text: string, method: UiLocatorMethod, state: UiElementState, seconds: number): Promise<void>;
71
+ /**
72
+ * Fill a field found by (in order) exact label, exact placeholder, label,
73
+ * placeholder, or name attribute. Resolves false if none appears in time.
74
+ */
75
+ fillField?(name: string, value: string, options?: {
76
+ timeoutMs?: number;
77
+ }): Promise<boolean>;
78
+ /** Wait until the URL satisfies `predicate`; resolves false on timeout. */
79
+ waitForUrl?(predicate: (url: string) => boolean, timeoutMs: number): Promise<boolean>;
80
+ /** Wait until `text` is visible; resolves false on timeout. */
81
+ waitForText?(text: string, timeoutMs: number): Promise<boolean>;
82
+ /** Snapshot cookies + localStorage (Playwright storageState). */
83
+ saveSession?(): Promise<UiSessionState>;
84
+ /** Apply a snapshot from saveSession() to the current browser context. */
85
+ restoreSession?(state: UiSessionState): Promise<void>;
71
86
  }
87
+ /** Opaque browser session snapshot (Playwright `storageState()` shape). */
88
+ type UiSessionState = {
89
+ cookies: Array<Record<string, unknown>>;
90
+ origins: Array<{
91
+ origin: string;
92
+ localStorage: Array<{
93
+ name: string;
94
+ value: string;
95
+ }>;
96
+ }>;
97
+ };
72
98
 
99
+ type UiLoginOptions = {
100
+ /** Log in through the form once per role (per worker), then restore the saved session. */
101
+ reuseSession?: boolean;
102
+ };
73
103
  interface AuthPort {
74
104
  apiLoginAsAdmin(world: World): Promise<void>;
75
105
  apiLoginAsUser(world: World): Promise<void>;
76
106
  apiSetBearer(world: World, token: string): void;
77
107
  uiLoginAsAdmin(world: World): Promise<void>;
78
108
  uiLoginAsUser(world: World): Promise<void>;
109
+ /**
110
+ * Log in to the API as any named role. Optional so pre-0.8 custom adapters
111
+ * keep compiling; the built-in steps fall back to the admin/user methods.
112
+ */
113
+ apiLoginAs?(world: World, role: string): Promise<void>;
114
+ /** Log in through the UI as any named role. */
115
+ uiLoginAs?(world: World, role: string, options?: UiLoginOptions): Promise<void>;
79
116
  }
80
117
 
81
118
  interface CleanupPort {
@@ -363,6 +400,10 @@ type TuiFactory = () => TuiPort | undefined;
363
400
  type CreateBddTestOptions = {
364
401
  createApi?: (ctx: CreateContext) => ApiPort;
365
402
  createUi?: (ctx: CreateContext) => UiPort;
403
+ /**
404
+ * Build the auth adapter. `ui` is available to UI login methods once a step
405
+ * requests the `ui` fixture; API login never starts a browser.
406
+ */
366
407
  createAuth?: (ctx: CreateContext & {
367
408
  api: ApiPort;
368
409
  ui: UiPort;
@@ -407,6 +448,9 @@ declare function createBddTest(options?: CreateBddTestOptions): {
407
448
  cleanup: CleanupPort;
408
449
  tui: TuiPort | undefined;
409
450
  apiRequest: APIRequestContext;
451
+ uiBinding: {
452
+ current?: UiPort;
453
+ };
410
454
  }, PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions & playwright_bdd.BddWorkerFixtures>;
411
455
  expect: _playwright_test.Expect<{}>;
412
456
  };
@@ -458,24 +502,82 @@ declare class PlaywrightUiAdapter implements UiPort {
458
502
  expectElementStateWithin(ordinal: string, text: string, method: UiLocatorMethod, state: UiElementState, seconds: number): Promise<void>;
459
503
  private parseOrdinal;
460
504
  private locatorBy;
505
+ fillField(name: string, value: string, options?: {
506
+ timeoutMs?: number;
507
+ }): Promise<boolean>;
508
+ waitForUrl(predicate: (url: string) => boolean, timeoutMs: number): Promise<boolean>;
509
+ waitForText(text: string, timeoutMs: number): Promise<boolean>;
510
+ saveSession(): Promise<UiSessionState>;
511
+ restoreSession(state: UiSessionState): Promise<void>;
461
512
  private performClick;
462
513
  private expectState;
463
514
  private assertUrlAgainst;
464
515
  }
465
516
 
517
+ /**
518
+ * Login credentials by role.
519
+ *
520
+ * Any role name works: "pm" reads AUTH_PM_USERNAME / AUTH_PM_PASSWORD,
521
+ * "project manager" reads AUTH_PROJECT_MANAGER_USERNAME / ..._PASSWORD.
522
+ * Roles passed in code (UniversalAuthAdapter `roles` option) take precedence.
523
+ * "admin" and "user" also accept the pre-0.8 DEFAULT_* / NON_ADMIN_* names.
524
+ */
525
+ type Env$3 = Record<string, string | undefined>;
526
+ type Credentials = {
527
+ username: string;
528
+ password: string;
529
+ };
530
+ type RoleCredentials = Record<string, Partial<Credentials>>;
531
+ declare class MissingCredentialsError extends Error {
532
+ constructor(message: string);
533
+ }
534
+ declare function roleEnvKeys(role: string): {
535
+ username: string;
536
+ password: string;
537
+ };
538
+ declare function resolveCredentials(role: string, { env, roles }?: {
539
+ env?: Env$3;
540
+ roles?: RoleCredentials;
541
+ }): Credentials;
542
+
543
+ type Env$2 = Record<string, string | undefined>;
544
+ type UniversalAuthOptions = {
545
+ api: ApiPort;
546
+ ui: UiPort;
547
+ /** Credentials per role, e.g. { pm: { username, password } }. Overrides AUTH_<ROLE>_* env vars. */
548
+ roles?: RoleCredentials;
549
+ /** Settings source; defaults to process.env. */
550
+ env?: Env$2;
551
+ };
552
+ /** Forget saved UI sessions (e.g. after changing a password mid-run). */
553
+ declare function clearUiSessions(): void;
554
+ /**
555
+ * Logs in by role through the API or the UI, configured by environment
556
+ * variables (see docs/guides/authentication.md).
557
+ *
558
+ * Extend it by subclassing and overriding `apiLogin` / `uiLogin` (e.g. for SSO
559
+ * or a multi-step form); credentials, steps and session reuse keep working.
560
+ */
466
561
  declare class UniversalAuthAdapter implements AuthPort {
467
- private readonly deps;
468
- constructor(deps: {
469
- api: ApiPort;
470
- ui: UiPort;
471
- });
562
+ protected readonly api: ApiPort;
563
+ protected readonly ui: UiPort;
564
+ protected readonly roles?: RoleCredentials;
565
+ protected readonly env: Env$2;
566
+ constructor(options: UniversalAuthOptions);
567
+ /** Credentials for a role; throws MissingCredentialsError with the variables to set. */
568
+ credentialsFor(role: string): Credentials;
472
569
  apiSetBearer(world: World, token: string): void;
570
+ apiLoginAs(world: World, role: string): Promise<void>;
571
+ uiLoginAs(world: World, role: string, options?: UiLoginOptions): Promise<void>;
473
572
  apiLoginAsAdmin(world: World): Promise<void>;
474
573
  apiLoginAsUser(world: World): Promise<void>;
475
- private apiLogin;
476
574
  uiLoginAsAdmin(world: World): Promise<void>;
477
575
  uiLoginAsUser(world: World): Promise<void>;
478
- private uiLogin;
576
+ /** POST credentials to the login endpoint; keep the bearer token or session cookie. */
577
+ protected apiLogin(world: World, role: string, creds: Credentials): Promise<void>;
578
+ /** Fill and submit the login form, then confirm the login worked. */
579
+ protected uiLogin(_world: World, role: string, creds: Credentials): Promise<void>;
580
+ private sessionReuseEnabled;
479
581
  }
480
582
 
481
583
  type CleanupRule = {
@@ -881,4 +983,54 @@ type ApiRequestTarget = {
881
983
  };
882
984
  declare function resolveApiRequestTarget(baseURL: string, env?: Record<string, string | undefined>): ApiRequestTarget;
883
985
 
884
- export { type ApiMethod, type ApiPort, type ApiRequestTarget, type ApiResult, type AuthPort, type CleanupAuthProvider, type CleanupItem, type CleanupPort, type CleanupRule, type CreateBddTestOptions, DefaultCleanupAdapter, type FetchInterceptAuthData, type FetchInterceptConfig, type OidcCleanupAuthConfig, PlaywrightApiAdapter, PlaywrightUiAdapter, type ResolveFeaturesOptions, type ResolveStepsOptions, type ResolveWorkersOptions, type TuiConfig, type TuiFactory, type TuiKeyModifiers, type TuiMouseButton, type TuiMouseEvent, type TuiMouseEventType, type TuiPort, type TuiScreenCapture, type TuiSnapshotResult, TuiTesterAdapter, type TuiWaitOptions, type UiClickMode, type UiElementState, type UiInputMode, type UiLocatorMethod, type UiPort, type UiUrlAssertMode, UniversalAuthAdapter, type World, assertMasked, clearFetchIntercept, createBddTest, createOidcCleanupAuth, defaultBearerConfig, defaultBypassConfig, getCpuCount, initWorld, interpolate, parseExpected, registerCleanup, resetOidcCleanupAuth, resolveApiRequestTarget, resolveBddPaths, resolveExtraTags, resolveFeatures, resolveSteps, resolveWorkers, selectPath, setupBearerAuth, setupBypassAuth, setupFetchIntercept, tagsForProject, tryParseJson };
986
+ /**
987
+ * Where tests point: the frontend (UI) and the API.
988
+ *
989
+ * Canonical variables are FRONTEND_URL and API_BASE_URL. BASE_URL and
990
+ * TARGET_BASE_URL are accepted as older aliases. When no API URL is set, API
991
+ * requests go to the frontend URL, so same-origin apps (`/api/...`) and
992
+ * scenarios that mix API and UI steps work with a single setting.
993
+ */
994
+ type Env$1 = Record<string, string | undefined>;
995
+ type Targets = {
996
+ frontendUrl: string;
997
+ frontendSource: 'FRONTEND_URL' | 'BASE_URL' | 'default';
998
+ apiBaseUrl: string;
999
+ apiSource: 'API_BASE_URL' | 'TARGET_BASE_URL' | 'TARGET_PORT' | 'FRONTEND_URL' | 'default';
1000
+ };
1001
+ /** Resolve the frontend and API targets from environment variables. */
1002
+ declare function resolveTargets(env?: Env$1): Targets;
1003
+ /**
1004
+ * Base URL for the API request context in a given Playwright project.
1005
+ * Order: API_BASE_URL > TARGET_BASE_URL > baseURL of a project named like
1006
+ * "api" > TARGET_PORT > the project's baseURL (any project) > localhost:3000.
1007
+ */
1008
+ declare function resolveApiBaseUrl({ env, projectName, projectBaseURL, }: {
1009
+ env?: Env$1;
1010
+ projectName?: string;
1011
+ projectBaseURL?: string;
1012
+ }): string;
1013
+ declare function formatTargets(t: Targets): string;
1014
+ /** Only the main `playwright test` process logs (not workers, not bddgen). */
1015
+ declare function shouldLogTargets(env?: Env$1, argv?: string[]): boolean;
1016
+ /**
1017
+ * Print the resolved targets once per run. Call from playwright.config.ts.
1018
+ * Silence with KATALYST_XSPEC_QUIET=true.
1019
+ */
1020
+ declare function logTargets(targets?: Targets): Targets;
1021
+
1022
+ type Env = Record<string, string | undefined>;
1023
+ type ApiLoginRequest = {
1024
+ path: string;
1025
+ body: 'form' | 'json';
1026
+ fields: Record<string, string>;
1027
+ };
1028
+ declare function buildApiLoginRequest(creds: Credentials, env?: Env): ApiLoginRequest;
1029
+ declare function extractToken(json: unknown, env?: Env): string | undefined;
1030
+
1031
+ /** Log in to the API as a role, supporting pre-0.8 adapters that only know admin/user. */
1032
+ declare function apiLoginAsRole(auth: AuthPort, world: World, role: string): Promise<void>;
1033
+ /** Log in through the UI as a role, supporting pre-0.8 adapters that only know admin/user. */
1034
+ declare function uiLoginAsRole(auth: AuthPort, world: World, role: string, options?: UiLoginOptions): Promise<void>;
1035
+
1036
+ export { type ApiLoginRequest, type ApiMethod, type ApiPort, type ApiRequestTarget, type ApiResult, type AuthPort, type CleanupAuthProvider, type CleanupItem, type CleanupPort, type CleanupRule, type CreateBddTestOptions, type Credentials, DefaultCleanupAdapter, type FetchInterceptAuthData, type FetchInterceptConfig, MissingCredentialsError, type OidcCleanupAuthConfig, PlaywrightApiAdapter, PlaywrightUiAdapter, type ResolveFeaturesOptions, type ResolveStepsOptions, type ResolveWorkersOptions, type RoleCredentials, type Targets, type TuiConfig, type TuiFactory, type TuiKeyModifiers, type TuiMouseButton, type TuiMouseEvent, type TuiMouseEventType, type TuiPort, type TuiScreenCapture, type TuiSnapshotResult, TuiTesterAdapter, type TuiWaitOptions, type UiClickMode, type UiElementState, type UiInputMode, type UiLocatorMethod, type UiLoginOptions, type UiPort, type UiSessionState, type UiUrlAssertMode, UniversalAuthAdapter, type UniversalAuthOptions, type World, apiLoginAsRole, assertMasked, buildApiLoginRequest, clearFetchIntercept, clearUiSessions, createBddTest, createOidcCleanupAuth, defaultBearerConfig, defaultBypassConfig, extractToken, formatTargets, getCpuCount, initWorld, interpolate, logTargets, parseExpected, registerCleanup, resetOidcCleanupAuth, resolveApiBaseUrl, resolveApiRequestTarget, resolveBddPaths, resolveCredentials, resolveExtraTags, resolveFeatures, resolveSteps, resolveTargets, resolveWorkers, roleEnvKeys, selectPath, setupBearerAuth, setupBypassAuth, setupFetchIntercept, shouldLogTargets, tagsForProject, tryParseJson, uiLoginAsRole };