@trackunit/iris-app-playwright 0.2.116-alpha-5b28b432cab.0 → 0.2.116

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 (42) hide show
  1. package/package.json +1 -1
  2. package/src/fixtures/hostReadiness.d.ts +0 -5
  3. package/src/fixtures/hostReadiness.js +1 -11
  4. package/src/fixtures/login.fixture.d.ts +7 -75
  5. package/src/fixtures/login.fixture.js +45 -301
  6. package/migrations/entry.js.map +0 -1
  7. package/src/credentials/guards.js.map +0 -1
  8. package/src/credentials/keys.js.map +0 -1
  9. package/src/credentials/resolveE2EPassword.js.map +0 -1
  10. package/src/fixtures/acceptTerms.d.ts +0 -59
  11. package/src/fixtures/acceptTerms.js +0 -136
  12. package/src/fixtures/acceptTerms.js.map +0 -1
  13. package/src/fixtures/baseTest.js.map +0 -1
  14. package/src/fixtures/describe.js.map +0 -1
  15. package/src/fixtures/featureFlags.fixture.js.map +0 -1
  16. package/src/fixtures/harRecording.fixture.js.map +0 -1
  17. package/src/fixtures/hostReadiness.js.map +0 -1
  18. package/src/fixtures/hydrateStorageState.js.map +0 -1
  19. package/src/fixtures/irisApp.fixture.js.map +0 -1
  20. package/src/fixtures/localDevMode.fixture.js.map +0 -1
  21. package/src/fixtures/login.fixture.js.map +0 -1
  22. package/src/fixtures/loginCacheKey.js.map +0 -1
  23. package/src/fixtures/resolveCredentials.js.map +0 -1
  24. package/src/fixtures/withTimeout.js.map +0 -1
  25. package/src/generators/playwright-configuration/generator.js.map +0 -1
  26. package/src/index.js.map +0 -1
  27. package/src/plugins/createLogFile.js.map +0 -1
  28. package/src/plugins/defaultPlaywrightConfig.js.map +0 -1
  29. package/src/plugins/harRecordingPath.js.map +0 -1
  30. package/src/plugins/logsReporter.js.map +0 -1
  31. package/src/plugins/nxPreset.js.map +0 -1
  32. package/src/plugins/redactHar.js.map +0 -1
  33. package/src/plugins/redactPatterns.js.map +0 -1
  34. package/src/plugins/redactSensitive.js.map +0 -1
  35. package/src/plugins/redactTrace.js.map +0 -1
  36. package/src/plugins/scanArtifactsForSecrets.js.map +0 -1
  37. package/src/plugins/setupPlugins.js.map +0 -1
  38. package/src/plugins/writeFileWithPrettier.js.map +0 -1
  39. package/src/support.js.map +0 -1
  40. package/src/utils/Codeowner.js.map +0 -1
  41. package/src/utils/fileNameBuilder.js.map +0 -1
  42. package/src/utils/fileUpdater.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/iris-app-playwright",
3
- "version": "0.2.116-alpha-5b28b432cab.0",
3
+ "version": "0.2.116",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "generators": "./generators.json",
@@ -11,11 +11,6 @@ export declare class HostNeverBecameReadyError extends Error {
11
11
  /** Creates an error whose message is exactly "host never became ready". The wait that ended is kept as `cause`. */
12
12
  constructor(cause?: unknown);
13
13
  }
14
- /** Thrown when the terms gate remains visibly loading after its shorter diagnostic budget. */
15
- export declare class TermsGateDidNotSettleError extends HostNeverBecameReadyError {
16
- /** Creates a readiness error that names the shorter terms-gate timeout. */
17
- constructor(timeoutMs: number);
18
- }
19
14
  /**
20
15
  * True only when `NX_FEATURE_BRANCH_BASE_URL` is a non-empty string.
21
16
  *
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isFeatureBranchHost = exports.TermsGateDidNotSettleError = exports.HostNeverBecameReadyError = void 0;
3
+ exports.isFeatureBranchHost = exports.HostNeverBecameReadyError = void 0;
4
4
  /**
5
5
  * Thrown when a feature-branch host never becomes ready.
6
6
  *
@@ -18,16 +18,6 @@ class HostNeverBecameReadyError extends Error {
18
18
  }
19
19
  }
20
20
  exports.HostNeverBecameReadyError = HostNeverBecameReadyError;
21
- /** Thrown when the terms gate remains visibly loading after its shorter diagnostic budget. */
22
- class TermsGateDidNotSettleError extends HostNeverBecameReadyError {
23
- /** Creates a readiness error that names the shorter terms-gate timeout. */
24
- constructor(timeoutMs) {
25
- super();
26
- this.name = "TermsGateDidNotSettleError";
27
- this.message = `terms gate did not settle within ${String(timeoutMs)}ms`;
28
- }
29
- }
30
- exports.TermsGateDidNotSettleError = TermsGateDidNotSettleError;
31
21
  /**
32
22
  * True only when `NX_FEATURE_BRANCH_BASE_URL` is a non-empty string.
33
23
  *
@@ -1,5 +1,4 @@
1
- import { type TermsStatusPage } from "./acceptTerms";
2
- import { type CachedStorageState, type StorageStateHydrator } from "./hydrateStorageState";
1
+ import { type CachedStorageState } from "./hydrateStorageState";
3
2
  import { type AuthCredentials, type ResolveCredentialsOptions } from "./resolveCredentials";
4
3
  export type { AuthCredentials };
5
4
  export type LoginOptions = ResolveCredentialsOptions;
@@ -27,14 +26,6 @@ export declare const MAIN_MENU_READY_SELECTOR = "[data-testid=\"mainMenu\"]:not(
27
26
  type MainMenuReadinessInput = {
28
27
  readonly selector: string;
29
28
  readonly fragments: ReadonlyArray<string>;
30
- /**
31
- * False on a feature-branch host, so `/terms` is not a finished login.
32
- * {@link waitForMainMenuReady} always sets this in Node. The predicate runs
33
- * in the browser, so the default must not call {@link isFeatureBranchHost}:
34
- * that closure is not serialized, and a dropped argument would throw there.
35
- * Omitted means ready, which is the staging hide-menu behaviour.
36
- */
37
- readonly termsCountsAsReady?: boolean;
38
29
  };
39
30
  type MainMenuWaitPage = {
40
31
  waitForFunction: (pageFunction: (input: MainMenuReadinessInput) => boolean, input: MainMenuReadinessInput, options?: {
@@ -62,24 +53,12 @@ export declare const extendTestTimeout: (testInfo: TestTimeoutInfo | undefined,
62
53
  * branch is unchanged.
63
54
  */
64
55
  export declare const reserveFeatureBranchLoginBudget: (testInfo?: TestTimeoutInfo) => void;
65
- /**
66
- * Returns whether the main menu is ready or intentionally hidden for the current route.
67
- *
68
- * Root cause (GLU-2632): `/terms` is in {@link HIDE_MENU_URL_FRAGMENTS}, so every
69
- * host treated the unfinished terms prompt as a finished login. Callers on a
70
- * feature-branch host pass `termsCountsAsReady: false`, so a URL containing
71
- * `/terms` is not ready even if a menu node is present. An omitted flag stays
72
- * ready, which keeps today's behaviour for every other host.
73
- */
74
- export declare const isMainMenuReady: ({ selector, fragments, termsCountsAsReady, }: MainMenuReadinessInput) => boolean;
56
+ /** Returns whether the main menu is ready or intentionally hidden for the current route. */
57
+ export declare const isMainMenuReady: ({ selector, fragments }: MainMenuReadinessInput) => boolean;
75
58
  /**
76
59
  * Waits until Manager's main menu is rendered without a loading skeleton, or
77
60
  * until the browser is on a HostLayout hide-menu route (marketplace / auth /
78
61
  * terms) where the menu is intentionally absent.
79
- *
80
- * On a feature-branch host, `/terms` is excluded: the prompt is not a finished
81
- * login (GLU-2632). The flag is computed here, in Node, and passed in because
82
- * {@link isMainMenuReady} is evaluated in the page.
83
62
  */
84
63
  export declare const waitForMainMenuReady: (page: MainMenuWaitPage, testInfo?: TestTimeoutInfo) => Promise<void>;
85
64
  type EnvReadyResponse = {
@@ -159,10 +138,6 @@ type LoginRequestContext = {
159
138
  type SessionDocument = {
160
139
  ok: () => boolean;
161
140
  };
162
- type TermsButton = {
163
- isVisible: () => Promise<boolean>;
164
- click: () => Promise<void>;
165
- };
166
141
  type OktaLoginPage = {
167
142
  goto: (url: string, options?: {
168
143
  timeout?: number;
@@ -172,38 +147,19 @@ type OktaLoginPage = {
172
147
  }) => Promise<unknown>;
173
148
  locator: (selector: string) => {
174
149
  waitFor: (options: {
175
- state: "visible" | "hidden";
150
+ state: "visible";
176
151
  timeout: number;
177
152
  }) => Promise<unknown>;
178
- isVisible?: () => Promise<boolean>;
179
153
  };
180
- /** Present on a real Playwright page. Session-handoff fakes omit it. */
181
- url?: () => string;
182
- getByRole?: (role: "button", options: {
183
- name: string;
184
- exact?: boolean;
185
- }) => TermsButton;
186
- on?: TermsStatusPage["on"];
187
- off?: TermsStatusPage["off"];
188
- waitForFunction?: MainMenuWaitPage["waitForFunction"];
189
154
  };
190
- /**
191
- * How long a ready shell must stay off `/terms`, with the gate no longer loading, before
192
- * login treats the prompt as not coming.
193
- */
194
- export declare const TERMS_GATE_QUIET_MS = 1000;
195
- /** How long a visibly loading terms gate may remain unchanged before login fails specifically. */
196
- export declare const TERMS_GATE_STALL_TIMEOUT_MS = 20000;
197
155
  /**
198
156
  * Performs the slow path: wait for the branch host to serve `/env`, hit Okta's
199
157
  * `/api/v1/authn`, redeem the `sessionToken` via the manager-classic redirect,
200
- * and wait for the host layout to render. On a feature-branch host it then
201
- * finishes the terms prompt and waits for the main shell inside the same
202
- * budget. Factored out so the cache hit/miss branches read clearly.
158
+ * and wait for the host layout to render. Factored out so the cache hit/miss
159
+ * branches read clearly.
203
160
  *
204
161
  * `createRequestContext`, `now`, and `sleep` are injectable so a unit test can
205
- * drive the session handoff and the terms prompt without a browser or a real
206
- * three-minute wait.
162
+ * drive the session handoff without a browser or a real three-minute wait.
207
163
  */
208
164
  export declare const performOktaLogin: ({ page, baseURL, credentials, testInfo, createRequestContext, now, sleep: pause, }: {
209
165
  page: OktaLoginPage;
@@ -216,28 +172,4 @@ export declare const performOktaLogin: ({ page, baseURL, credentials, testInfo,
216
172
  /** Injectable pause between session document reloads. Production waits on the event loop. */
217
173
  sleep?: (ms: number) => Promise<void>;
218
174
  }) => Promise<void>;
219
- type SignInPage = Omit<OktaLoginPage, "waitForFunction"> & {
220
- context: () => StorageStateHydrator & {
221
- storageState: () => Promise<CachedStorageState>;
222
- };
223
- /** Required so the shell wait after a restored session can run in the page. */
224
- waitForFunction: MainMenuWaitPage["waitForFunction"];
225
- };
226
- /**
227
- * Signs in, reusing this worker's cached session when one exists.
228
- *
229
- * `now` and `sleep` are injectable so a unit test can drive the restored
230
- * feature-branch terms wait without a browser or a real three-minute clock.
231
- * Production uses `Date.now` and the event loop.
232
- */
233
- export declare const signIn: ({ page, baseURL, cache, testInfo, options, now, sleep: pause, createRequestContext, }: {
234
- page: SignInPage;
235
- baseURL: string | undefined;
236
- cache: Map<string, CachedStorageState>;
237
- testInfo?: TestTimeoutInfo;
238
- options?: LoginOptions;
239
- now?: () => number;
240
- sleep?: (ms: number) => Promise<void>;
241
- createRequestContext?: (baseURL: string | undefined) => Promise<LoginRequestContext>;
242
- }) => Promise<void>;
243
175
  export { expect } from "@playwright/test";
@@ -1,8 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.expect = exports.signIn = exports.performOktaLogin = exports.TERMS_GATE_STALL_TIMEOUT_MS = exports.TERMS_GATE_QUIET_MS = exports.test = exports.waitForEnvReady = exports.EnvReadyTimeoutError = exports.waitForMainMenuReady = exports.isMainMenuReady = exports.reserveFeatureBranchLoginBudget = exports.extendTestTimeout = exports.MAIN_MENU_READY_SELECTOR = exports.SESSION_DOCUMENT_RETRY_DELAY_MS = exports.ENV_READY_POLL_INTERVAL_MS = exports.ENV_READY_TIMEOUT_ENV_VAR = exports.ENV_READY_TIMEOUT_MS = exports.MAIN_MENU_READY_TIMEOUT_MS = exports.HIDE_MENU_URL_FRAGMENTS = void 0;
3
+ exports.expect = exports.performOktaLogin = exports.test = exports.waitForEnvReady = exports.EnvReadyTimeoutError = exports.waitForMainMenuReady = exports.isMainMenuReady = exports.reserveFeatureBranchLoginBudget = exports.extendTestTimeout = exports.MAIN_MENU_READY_SELECTOR = exports.SESSION_DOCUMENT_RETRY_DELAY_MS = exports.ENV_READY_POLL_INTERVAL_MS = exports.ENV_READY_TIMEOUT_ENV_VAR = exports.ENV_READY_TIMEOUT_MS = exports.MAIN_MENU_READY_TIMEOUT_MS = exports.HIDE_MENU_URL_FRAGMENTS = void 0;
4
4
  const test_1 = require("@playwright/test");
5
- const acceptTerms_1 = require("./acceptTerms");
6
5
  const hostReadiness_1 = require("./hostReadiness");
7
6
  const hydrateStorageState_1 = require("./hydrateStorageState");
8
7
  const loginCacheKey_1 = require("./loginCacheKey");
@@ -70,19 +69,8 @@ const reserveFeatureBranchLoginBudget = (testInfo) => {
70
69
  (0, exports.extendTestTimeout)(testInfo, exports.MAIN_MENU_READY_TIMEOUT_MS);
71
70
  };
72
71
  exports.reserveFeatureBranchLoginBudget = reserveFeatureBranchLoginBudget;
73
- /**
74
- * Returns whether the main menu is ready or intentionally hidden for the current route.
75
- *
76
- * Root cause (GLU-2632): `/terms` is in {@link HIDE_MENU_URL_FRAGMENTS}, so every
77
- * host treated the unfinished terms prompt as a finished login. Callers on a
78
- * feature-branch host pass `termsCountsAsReady: false`, so a URL containing
79
- * `/terms` is not ready even if a menu node is present. An omitted flag stays
80
- * ready, which keeps today's behaviour for every other host.
81
- */
82
- const isMainMenuReady = ({ selector, fragments, termsCountsAsReady = true, }) => {
83
- if (!termsCountsAsReady && window.location.pathname.includes("/terms")) {
84
- return false;
85
- }
72
+ /** Returns whether the main menu is ready or intentionally hidden for the current route. */
73
+ const isMainMenuReady = ({ selector, fragments }) => {
86
74
  if (document.querySelector(selector) !== null) {
87
75
  return true;
88
76
  }
@@ -93,29 +81,10 @@ exports.isMainMenuReady = isMainMenuReady;
93
81
  * Waits until Manager's main menu is rendered without a loading skeleton, or
94
82
  * until the browser is on a HostLayout hide-menu route (marketplace / auth /
95
83
  * terms) where the menu is intentionally absent.
96
- *
97
- * On a feature-branch host, `/terms` is excluded: the prompt is not a finished
98
- * login (GLU-2632). The flag is computed here, in Node, and passed in because
99
- * {@link isMainMenuReady} is evaluated in the page.
100
84
  */
101
85
  const waitForMainMenuReady = async (page, testInfo) => {
102
86
  (0, exports.extendTestTimeout)(testInfo, exports.MAIN_MENU_READY_TIMEOUT_MS);
103
- try {
104
- await page.waitForFunction(exports.isMainMenuReady, {
105
- selector: exports.MAIN_MENU_READY_SELECTOR,
106
- fragments: exports.HIDE_MENU_URL_FRAGMENTS,
107
- termsCountsAsReady: !(0, hostReadiness_1.isFeatureBranchHost)(),
108
- }, { timeout: exports.MAIN_MENU_READY_TIMEOUT_MS });
109
- }
110
- catch (error) {
111
- // The terms prompt can redirect after the inner shell wait has returned.
112
- // On a feature-branch host that timeout is the host never becoming ready,
113
- // before any page assertion. Other hosts keep Playwright's timeout.
114
- if ((0, hostReadiness_1.isFeatureBranchHost)() && error instanceof test_1.errors.TimeoutError) {
115
- throw new hostReadiness_1.HostNeverBecameReadyError(error);
116
- }
117
- throw error;
118
- }
87
+ await page.waitForFunction(exports.isMainMenuReady, { selector: exports.MAIN_MENU_READY_SELECTOR, fragments: exports.HIDE_MENU_URL_FRAGMENTS }, { timeout: exports.MAIN_MENU_READY_TIMEOUT_MS });
119
88
  };
120
89
  exports.waitForMainMenuReady = waitForMainMenuReady;
121
90
  const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
@@ -195,187 +164,44 @@ exports.test = test_1.test.extend({
195
164
  { scope: "worker" },
196
165
  ],
197
166
  login: async ({ page, baseURL, _loginCache }, provide, testInfo) => {
198
- await provide(options => (0, exports.signIn)({ page, baseURL, cache: _loginCache, testInfo, options }));
199
- },
200
- });
201
- const noopTermsWatcher = () => ({
202
- revision: () => 0,
203
- stop: () => undefined,
204
- });
205
- /** Suspense fallback that sits in the host layout until the lazy `TermsGate` chunk loads. */
206
- const TERMS_GATE_FALLBACK_SELECTOR = '[data-testid="manager-app-fallback"]';
207
- /** Spinner `TermsGate` renders once its chunk has loaded, while the status lookup is still loading. */
208
- const TERMS_GATE_LOADING_SELECTOR = '[data-testid="termsGateLoading"]';
209
- /**
210
- * How long a ready shell must stay off `/terms`, with the gate no longer loading, before
211
- * login treats the prompt as not coming.
212
- */
213
- exports.TERMS_GATE_QUIET_MS = 1000;
214
- /** How long a visibly loading terms gate may remain unchanged before login fails specifically. */
215
- exports.TERMS_GATE_STALL_TIMEOUT_MS = 20000;
216
- /**
217
- * Starts the terms-status watcher only on a feature-branch host, and only when the
218
- * page can listen for requests. An unset or empty `NX_FEATURE_BRANCH_BASE_URL` does
219
- * not subscribe and does not click Accept (GLU-2632).
220
- */
221
- const startFeatureBranchTermsWatch = (page) => {
222
- if (!(0, hostReadiness_1.isFeatureBranchHost)() || page.on === undefined || page.off === undefined) {
223
- return noopTermsWatcher();
224
- }
225
- // Call through the page. A detached `page.on` runs without `this`, and Playwright
226
- // then throws `this.listenerCount is not a function` before any request is seen.
227
- return (0, acceptTerms_1.watchTermsStatus)({
228
- on: (event, listener) => {
229
- page.on?.(event, listener);
230
- },
231
- off: (event, listener) => {
232
- page.off?.(event, listener);
233
- },
234
- });
235
- };
236
- /**
237
- * True while `TermsGate` has not decided yet: either the lazy chunk has not replaced its
238
- * Suspense fallback, or the chunk is mounted and shows its own spinner because the status
239
- * lookup is still loading. A page that cannot see those nodes is treated as decided.
240
- */
241
- const isTermsGateStillLoading = async (page) => {
242
- for (const selector of [TERMS_GATE_FALLBACK_SELECTOR, TERMS_GATE_LOADING_SELECTOR]) {
243
- const node = page.locator(selector);
244
- if (node.isVisible !== undefined && (await node.isVisible())) {
245
- return true;
246
- }
247
- }
248
- return false;
249
- };
250
- /** True only for the spinner owned by `TermsGate`, not the shared application Suspense fallback. */
251
- const isTermsStatusStillLoading = async (page) => {
252
- const node = page.locator(TERMS_GATE_LOADING_SELECTOR);
253
- return node.isVisible !== undefined && (await node.isVisible());
254
- };
255
- /**
256
- * Finishes the terms prompt, then waits for the main shell, inside the login budget.
257
- *
258
- * Root cause (GLU-2632): feature-branch `/terms` is not a finished login, and waiting
259
- * for the menu never clicks the prompt. `#host-layout-content` is visible while
260
- * `TermsGate` is still a Suspense fallback, so returning as soon as the layout appears
261
- * can miss a later redirect. URL and rendered gate state are authoritative: a `/terms`
262
- * URL starts the accept flow, while a ready shell that stays off `/terms` with no gate
263
- * loader for {@link TERMS_GATE_QUIET_MS} is finished. Starting a new status request
264
- * resets that quiet window, but its response is never awaited because browser requests
265
- * can remain pending across navigation. A visibly loading gate gets the shorter
266
- * {@link TERMS_GATE_STALL_TIMEOUT_MS} diagnostic budget.
267
- */
268
- const finishFeatureBranchTerms = async ({ page, watcher, now, sleep: pause, deadline, }) => {
269
- if (!(0, hostReadiness_1.isFeatureBranchHost)()) {
270
- return;
271
- }
272
- let acceptedPrompt = false;
273
- let quietSince;
274
- let quietRevision = 0;
275
- let gateLoadingSince;
276
- const acceptIfPrompted = async () => {
277
- // Call through the page. Detached Playwright methods run without `this` and throw
278
- // reading `_mainFrame` before terms can finish.
279
- const pageUrl = page.url;
280
- if (pageUrl === undefined || !(0, acceptTerms_1.isTermsRoute)(pageUrl.call(page))) {
281
- return false;
282
- }
283
- if (page.getByRole === undefined) {
284
- throw new hostReadiness_1.HostNeverBecameReadyError();
285
- }
286
- const readUrl = pageUrl.bind(page);
287
- const findButton = page.getByRole.bind(page);
288
- await (0, acceptTerms_1.acceptTermsWithinBudget)({
289
- page: {
290
- url: readUrl,
291
- getByRole: findButton,
292
- waitForURL: (predicate, options) => page.waitForURL(predicate, options),
293
- },
294
- now,
295
- sleep: pause,
296
- deadline,
297
- });
298
- return true;
299
- };
300
- if (page.waitForFunction === undefined) {
301
- await acceptIfPrompted();
302
- return;
303
- }
304
- const waitForFunction = page.waitForFunction.bind(page);
305
- const shellInput = {
306
- selector: exports.MAIN_MENU_READY_SELECTOR,
307
- fragments: exports.HIDE_MENU_URL_FRAGMENTS,
308
- termsCountsAsReady: false,
309
- };
310
- while (now() < deadline) {
311
- const started = now();
312
- acceptedPrompt = (await acceptIfPrompted()) || acceptedPrompt;
313
- const remaining = deadline - now();
314
- if (remaining <= 0) {
315
- break;
316
- }
317
- const slice = Math.min(exports.TERMS_GATE_QUIET_MS, remaining);
318
- let shellReady = false;
319
- try {
320
- await waitForFunction(exports.isMainMenuReady, shellInput, { timeout: slice });
321
- shellReady = true;
322
- }
323
- catch (error) {
324
- if (!isPlaywrightTimeout(error) && now() < deadline) {
325
- throw error;
326
- }
327
- }
328
- if (shellReady) {
329
- acceptedPrompt = (await acceptIfPrompted()) || acceptedPrompt;
330
- }
331
- const onTerms = page.url !== undefined && (0, acceptTerms_1.isTermsRoute)(page.url());
332
- const gateLoading = await isTermsGateStillLoading(page);
333
- const termsStatusLoading = gateLoading && (await isTermsStatusStillLoading(page));
334
- if (termsStatusLoading) {
335
- gateLoadingSince ?? (gateLoadingSince = now());
336
- if (now() - gateLoadingSince >= exports.TERMS_GATE_STALL_TIMEOUT_MS) {
337
- throw new hostReadiness_1.TermsGateDidNotSettleError(exports.TERMS_GATE_STALL_TIMEOUT_MS);
338
- }
339
- }
340
- else {
341
- gateLoadingSince = undefined;
342
- }
343
- if (!shellReady || onTerms || gateLoading) {
344
- quietSince = undefined;
345
- }
346
- else if (acceptedPrompt) {
347
- return;
348
- }
349
- else {
350
- // A request that first appears mid-window restarts it: the app may still be
351
- // committing a redirect after the current DOM state was observed.
352
- const revision = watcher.revision();
353
- if (quietSince === undefined || revision !== quietRevision) {
354
- quietSince = now();
355
- quietRevision = revision;
356
- }
357
- if (now() - quietSince >= exports.TERMS_GATE_QUIET_MS) {
358
- return;
167
+ const loginFn = async (options) => {
168
+ const credentials = await (0, resolveCredentials_1.resolveCredentials)(options);
169
+ const cacheKey = (0, loginCacheKey_1.buildLoginCacheKey)(baseURL, credentials);
170
+ const cached = _loginCache.get(cacheKey);
171
+ if (cached !== undefined) {
172
+ let cachedSessionRestored = false;
173
+ try {
174
+ await (0, hydrateStorageState_1.hydrateStorageState)(page.context(), cached);
175
+ await page.goto("/");
176
+ await page.locator("#host-layout-content").waitFor({ state: "visible", timeout: 15000 });
177
+ cachedSessionRestored = true;
178
+ }
179
+ catch {
180
+ _loginCache.delete(cacheKey);
181
+ }
182
+ if (cachedSessionRestored) {
183
+ await (0, exports.waitForMainMenuReady)(page, testInfo);
184
+ return;
185
+ }
359
186
  }
360
- }
361
- if (now() >= deadline) {
362
- break;
363
- }
364
- if (now() === started) {
365
- const mark = `${page.url?.() ?? ""}|${String(watcher.revision())}|${String(acceptedPrompt)}`;
366
- const left = deadline - now();
367
- if (left <= 0) {
368
- break;
187
+ (0, exports.reserveFeatureBranchLoginBudget)(testInfo);
188
+ await (0, exports.performOktaLogin)({ page, baseURL, credentials, testInfo });
189
+ // Capture the fully-authenticated state for reuse by later tests in
190
+ // this worker. Errors here would mask the actual login success, so we
191
+ // never let snapshot capture throw — at worst we'll re-hit Okta next
192
+ // time, which is the pre-cache behaviour.
193
+ try {
194
+ const snapshot = await page.context().storageState();
195
+ _loginCache.set(cacheKey, snapshot);
369
196
  }
370
- await pause(Math.min(exports.TERMS_GATE_QUIET_MS, left));
371
- const after = `${page.url?.() ?? ""}|${String(watcher.revision())}|${String(acceptedPrompt)}`;
372
- if (now() === started && after === mark) {
373
- throw new hostReadiness_1.HostNeverBecameReadyError();
197
+ catch {
198
+ /* Snapshot capture is best-effort; failing here is non-fatal. */
374
199
  }
375
- }
376
- }
377
- throw new hostReadiness_1.HostNeverBecameReadyError();
378
- };
200
+ await (0, exports.waitForMainMenuReady)(page, testInfo);
201
+ };
202
+ await provide(loginFn);
203
+ },
204
+ });
379
205
  const openDefaultRequestContext = async (baseURL) => {
380
206
  const context = await test_1.request.newContext({ baseURL });
381
207
  return {
@@ -492,18 +318,14 @@ const gotoSessionDocument = async (page, url, now, deadline, pause) => {
492
318
  /**
493
319
  * Performs the slow path: wait for the branch host to serve `/env`, hit Okta's
494
320
  * `/api/v1/authn`, redeem the `sessionToken` via the manager-classic redirect,
495
- * and wait for the host layout to render. On a feature-branch host it then
496
- * finishes the terms prompt and waits for the main shell inside the same
497
- * budget. Factored out so the cache hit/miss branches read clearly.
321
+ * and wait for the host layout to render. Factored out so the cache hit/miss
322
+ * branches read clearly.
498
323
  *
499
324
  * `createRequestContext`, `now`, and `sleep` are injectable so a unit test can
500
- * drive the session handoff and the terms prompt without a browser or a real
501
- * three-minute wait.
325
+ * drive the session handoff without a browser or a real three-minute wait.
502
326
  */
503
327
  const performOktaLogin = async ({ page, baseURL, credentials, testInfo, createRequestContext = openDefaultRequestContext, now = Date.now, sleep: pause = sleep, }) => {
504
328
  const context = await createRequestContext(baseURL);
505
- // Before the session navigation: the menu can render before TermsGate redirects.
506
- const termsWatcher = startFeatureBranchTermsWatch(page);
507
329
  try {
508
330
  const env = await readHostEnv(context, baseURL, testInfo);
509
331
  const domainValue = env.auth?.url;
@@ -511,10 +333,9 @@ const performOktaLogin = async ({ page, baseURL, credentials, testInfo, createRe
511
333
  throw new Error(`No domain found from servers /env found env: ${JSON.stringify(env)}`);
512
334
  }
513
335
  const domain = domainValue;
514
- // Started once `/env` is ready. The Okta post, the session document, the
515
- // handoff, the terms prompt and the shell wait share this window. Only the
516
- // document, the handoff and the prompt are retried against it; the post keeps
517
- // its own fixed timeout (GLU-2631, GLU-2632).
336
+ // Started once `/env` is ready. The Okta post, the session document and the
337
+ // handoff all run inside this window; only the last two are retried against
338
+ // it, the post keeps its own fixed timeout (GLU-2631).
518
339
  const readyDeadline = (0, hostReadiness_1.isFeatureBranchHost)() ? now() + exports.MAIN_MENU_READY_TIMEOUT_MS : undefined;
519
340
  const authResponse = await context.post(`${domain}/api/v1/authn`, {
520
341
  data: {
@@ -536,89 +357,12 @@ const performOktaLogin = async ({ page, baseURL, credentials, testInfo, createRe
536
357
  await gotoSessionDocument(page, `/auth/manager-classic#session_token=${sessionToken}&userProfile=true`, now, readyDeadline, pause);
537
358
  await waitForSessionHandoff(page, now, readyDeadline);
538
359
  await page.locator("#host-layout-content").waitFor({ state: "visible", timeout: 15000 });
539
- if (readyDeadline !== undefined) {
540
- await finishFeatureBranchTerms({
541
- page,
542
- watcher: termsWatcher,
543
- now,
544
- sleep: pause,
545
- deadline: readyDeadline,
546
- });
547
- }
548
360
  }
549
361
  finally {
550
- termsWatcher.stop();
551
362
  await context.dispose();
552
363
  }
553
364
  };
554
365
  exports.performOktaLogin = performOktaLogin;
555
- /**
556
- * Signs in, reusing this worker's cached session when one exists.
557
- *
558
- * `now` and `sleep` are injectable so a unit test can drive the restored
559
- * feature-branch terms wait without a browser or a real three-minute clock.
560
- * Production uses `Date.now` and the event loop.
561
- */
562
- const signIn = async ({ page, baseURL, cache, testInfo, options, now = Date.now, sleep: pause = sleep, createRequestContext, }) => {
563
- const credentials = await (0, resolveCredentials_1.resolveCredentials)(options);
564
- const cacheKey = (0, loginCacheKey_1.buildLoginCacheKey)(baseURL, credentials);
565
- const cached = cache.get(cacheKey);
566
- if (cached !== undefined) {
567
- let cachedSessionRestored = false;
568
- const termsWatcher = startFeatureBranchTermsWatch(page);
569
- try {
570
- try {
571
- await (0, hydrateStorageState_1.hydrateStorageState)(page.context(), cached);
572
- await page.goto("/");
573
- await page.locator("#host-layout-content").waitFor({ state: "visible", timeout: 15000 });
574
- if ((0, hostReadiness_1.isFeatureBranchHost)()) {
575
- // The terms wait can use the whole three-minute budget. Reserve it
576
- // before that wait: the miss-path call below is skipped when restore
577
- // succeeds, and the menu wait extends the timeout only after this returns.
578
- (0, exports.reserveFeatureBranchLoginBudget)(testInfo);
579
- const deadline = now() + exports.MAIN_MENU_READY_TIMEOUT_MS;
580
- await finishFeatureBranchTerms({
581
- page,
582
- watcher: termsWatcher,
583
- now,
584
- sleep: pause,
585
- deadline,
586
- });
587
- }
588
- cachedSessionRestored = true;
589
- }
590
- catch (error) {
591
- if (error instanceof hostReadiness_1.HostNeverBecameReadyError) {
592
- throw error;
593
- }
594
- cache.delete(cacheKey);
595
- }
596
- if (cachedSessionRestored) {
597
- await (0, exports.waitForMainMenuReady)(page, testInfo);
598
- return;
599
- }
600
- }
601
- finally {
602
- termsWatcher.stop();
603
- }
604
- }
605
- (0, exports.reserveFeatureBranchLoginBudget)(testInfo);
606
- await (0, exports.performOktaLogin)({ page, baseURL, credentials, testInfo, now, sleep: pause, createRequestContext });
607
- await (0, exports.waitForMainMenuReady)(page, testInfo);
608
- // Capture only after the shell is ready. A feature-branch host can sit
609
- // on `/terms` with the layout mounted; storing that snapshot would reuse
610
- // a session whose prompt is not finished (GLU-2632). Errors here would
611
- // mask the actual login success, so snapshot capture never throws — at
612
- // worst we'll re-hit Okta next time, which is the pre-cache behaviour.
613
- try {
614
- const snapshot = await page.context().storageState();
615
- cache.set(cacheKey, snapshot);
616
- }
617
- catch {
618
- /* Snapshot capture is best-effort; failing here is non-fatal. */
619
- }
620
- };
621
- exports.signIn = signIn;
622
366
  var test_2 = require("@playwright/test");
623
367
  Object.defineProperty(exports, "expect", { enumerable: true, get: function () { return test_2.expect; } });
624
368
  //# sourceMappingURL=login.fixture.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"entry.js","sourceRoot":"","sources":["../../../../../libs/iris-app-sdk/iris-app-playwright/migrations/entry.ts"],"names":[],"mappings":"","sourcesContent":["export {};\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"guards.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/credentials/guards.ts"],"names":[],"mappings":";;;AAAA,oEAAoE;AAC7D,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAoC,EAAE,CAC3E,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AADjC,QAAA,QAAQ,YACyB","sourcesContent":["/** Narrows `unknown` to a plain object without a type assertion. */\nexport const isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null;\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"keys.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/credentials/keys.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AAEH;;;;;;GAMG;AACI,MAAM,eAAe,GAAG,CAAC,QAAgB,EAAU,EAAE,CAC1D,QAAQ;KACL,WAAW,EAAE;KACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;KAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AAJhB,QAAA,eAAe,mBAIC;AAE7B,+EAA+E;AACxE,MAAM,YAAY,GAAG,CAAC,QAAgB,EAAU,EAAE,CAAC,UAAU,IAAA,uBAAe,EAAC,QAAQ,CAAC,EAAE,CAAC;AAAnF,QAAA,YAAY,gBAAuE;AAEhG;;;;;;GAMG;AACI,MAAM,gBAAgB,GAAG,CAAC,QAAgB,EAAU,EAAE,CAAC,IAAA,oBAAY,EAAC,QAAQ,CAAC,CAAC;AAAxE,QAAA,gBAAgB,oBAAwD","sourcesContent":["/**\n * Credential-key derivation for the manager E2E password lookup (GLU-1451 / GLU-1452).\n *\n * An `auth.json` fixture identifies its account by `username` only — the password\n * is resolved at runtime by a stable key derived from the name: from a CircleCI\n * env variable in CI and from Vault locally.\n *\n * The `E2E_PW_<SLUG>` convention MUST stay in sync with the publish side in\n * `devtools/e2e-okta-rotation/slug.ts` (which writes the CircleCI/Vault values).\n */\n\n/**\n * Uppercase slug of an account name: non-alphanumeric runs collapse to a single\n * `_`, with leading/trailing separators trimmed. Keeps account names with spaces\n * and dashes valid as env-var/Vault-field keys.\n *\n * `Team Helios - E2E Test` -> `TEAM_HELIOS_E2E_TEST`\n */\nexport const slugifyUsername = (username: string): string =>\n username\n .toUpperCase()\n .replace(/[^A-Z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\");\n\n/** The environment-variable key for an account's password: `E2E_PW_<SLUG>`. */\nexport const deriveEnvKey = (username: string): string => `E2E_PW_${slugifyUsername(username)}`;\n\n/**\n * The Vault field key for an account's password. This is the **same** key as the\n * env variable (`E2E_PW_<SLUG>`): the publish side writes each account's password\n * to a Vault field named by `devtools/e2e-okta-rotation/slug.ts`'s `envKey()`\n * (`E2E_PW_<SLUG>`), so the resolver must read it back under that exact key. A\n * lowercased-slug field would never be found once Vault is seeded.\n */\nexport const deriveVaultField = (username: string): string => deriveEnvKey(username);\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"resolveE2EPassword.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/credentials/resolveE2EPassword.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync } from \"fs\";\nimport * as os from \"os\";\nimport * as path from \"path\";\nimport { redactSensitive } from \"../plugins/redactSensitive\";\nimport { isRecord } from \"./guards\";\nimport { deriveEnvKey, deriveVaultField } from \"./keys\";\n\n/**\n * Minimal structural view of the `node-vault` client's KV read: it returns\n * `{ data: ... }` (KV v1) or `{ data: { data: ... } }` (KV v2). Kept as an\n * interface so tests inject a fake and neither `node-vault` nor a real\n * Vault/VPN is needed. `read` returns `unknown` so callers narrow structurally\n * (no `as` cast at the boundary).\n */\nexport interface VaultReader {\n read(secretPath: string): Promise<unknown>;\n}\n\nexport interface ResolveE2EPasswordOptions {\n /** Process env to read (env-first precedence). Defaults to `process.env`. */\n env?: NodeJS.ProcessEnv;\n /**\n * Vault client factory (local path). Returns `undefined` when Vault can't be\n * reached/authenticated (e.g. no `~/.vault-token`). Defaults to a `node-vault`\n * client using the local token, matching `libs/server/vault`.\n */\n createVault?: (env: NodeJS.ProcessEnv) => Promise<VaultReader | undefined>;\n}\n\n/**\n * Reads a field from a node-vault KV read result. KV v2 nests the fields under\n * `data.data`; KV v1 puts them directly on `data`. Narrows structurally.\n */\nconst readVaultField = (result: unknown, field: string): string | undefined => {\n if (!isRecord(result)) {\n return undefined;\n }\n const outer = result.data;\n if (!isRecord(outer)) {\n return undefined;\n }\n const fields = isRecord(outer.data) ? outer.data : outer;\n const value = fields[field];\n return typeof value === \"string\" && value !== \"\" ? value : undefined;\n};\n\n/**\n * Default Vault client: reads the local `~/.vault-token` and points `node-vault`\n * at the endpoint from `E2E_VAULT_ENDPOINT` (or `VAULT_ENDPOINT`).\n *\n * The Vault address is **never hardcoded** — it is supplied by the environment,\n * so any consumer of this package can opt into their own Vault. `node-vault` is\n * an optional peer dependency, imported lazily and only on the local path, so\n * consumers that only use the env-var (CI) path never load it.\n */\nconst defaultCreateVault = async (env: NodeJS.ProcessEnv): Promise<VaultReader | undefined> => {\n const home = env.HOME ?? os.homedir();\n let token: string;\n try {\n token = readFileSync(path.join(home, \".vault-token\"), \"utf-8\").trim();\n } catch {\n return undefined; // not logged in / not on VPN\n }\n if (token === \"\") {\n return undefined;\n }\n const endpoint = env.E2E_VAULT_ENDPOINT ?? env.VAULT_ENDPOINT;\n if (endpoint === undefined || endpoint === \"\") {\n throw new Error(\n \"E2E credentials: a Vault token was found but no endpoint — set E2E_VAULT_ENDPOINT (or VAULT_ENDPOINT) to your Vault address.\"\n );\n }\n // Imported lazily so consumers on the env-var path (and unit tests, which\n // inject `createVault`) never load the optional `node-vault` peer. Its absence\n // is an expected configuration state, so fail with the resolver's actionable,\n // redacted style instead of leaking a raw `ERR_MODULE_NOT_FOUND` stack.\n const nodeVault = await import(\"node-vault\").catch(() => undefined);\n if (nodeVault === undefined) {\n return fail(\n \"E2E credentials: install the optional `node-vault` peer dependency for local Vault lookup, \" +\n \"or set the E2E_PW_<SLUG> env variable.\"\n );\n }\n // `timeout` bounds the whole request so an off-VPN endpoint that black-holes\n // the connection fails fast (~10s) instead of hanging until the test times out.\n const client = nodeVault.default({\n endpoint,\n token,\n requestOptions: { strictSSL: true, timeout: 10_000 },\n });\n return { read: secretPath => client.read(secretPath) };\n};\n\n/**\n * Node/socket error codes that mean the Vault host could not be reached at all —\n * as opposed to a reachable Vault answering with a 403/404. Off-VPN, the internal\n * Vault hostname either fails DNS (`ENOTFOUND`/`EAI_AGAIN`) or the connection is\n * refused/timed out, so these map to an actionable \"connect to the VPN\" hint.\n */\nconst VAULT_UNREACHABLE_CODES: ReadonlySet<string> = new Set([\n \"ENOTFOUND\",\n \"EAI_AGAIN\",\n \"ECONNREFUSED\",\n \"ECONNRESET\",\n \"ETIMEDOUT\",\n \"ESOCKETTIMEDOUT\",\n \"EHOSTUNREACH\",\n \"ENETUNREACH\",\n]);\n\n/** True when a thrown Vault-read error is a transport failure (host unreachable), not an HTTP response. */\nconst isVaultUnreachableError = (error: unknown): boolean => {\n if (!isRecord(error)) {\n return false;\n }\n const code = error.code;\n return typeof code === \"string\" && VAULT_UNREACHABLE_CODES.has(code);\n};\n\n/**\n * HTTP statuses a *reachable* Vault returns when the caller's token is missing,\n * invalid, or expired — i.e. \"not logged in / not authenticated\" (as opposed to a\n * 404 for a real missing path). `node-vault` attaches the HTTP response to the\n * thrown error as `error.response.statusCode`, so we narrow to it structurally\n * rather than making an extra `tokenLookupSelf` round-trip.\n */\nconst VAULT_UNAUTHENTICATED_STATUS: ReadonlySet<number> = new Set([401, 403]);\n\n/** Extracts the HTTP status code from a thrown `node-vault` read error, if present. */\nconst getVaultErrorStatus = (error: unknown): number | undefined => {\n if (!isRecord(error)) {\n return undefined;\n }\n const response = error.response;\n if (!isRecord(response)) {\n return undefined;\n }\n const statusCode = response.statusCode;\n return typeof statusCode === \"number\" ? statusCode : undefined;\n};\n\n/** Throws an error whose message is routed through `redactSensitive` (defence in depth). */\nconst fail = (message: string): never => {\n throw new Error(redactSensitive(message));\n};\n\nconst passwordCache = new Map<string, string | undefined>();\n\n/**\n * Clears the in-memory resolved-password cache. The cache is a module-level\n * singleton that lives for the whole process, so a resolved value would\n * otherwise leak across calls (and across test cases). Call this between tests\n * to keep each `resolveE2EPassword` call independent.\n */\nexport const resetE2EPasswordCache = (): void => {\n passwordCache.clear();\n};\n\n/**\n * Resolves an account's E2E password by name.\n *\n * Order: env variable `E2E_PW_<SLUG>` (CI path, never touches Vault) -> Vault\n * field `<slug>` (local path). Throws a clear, account-named, redacted error on\n * miss. The password is never included in any error/diagnostic (on failure there\n * is no resolved value to leak).\n *\n * @param username - Account identifier from the fixture (`auth.json` `username`).\n * @param options - Process env and Vault-client seams.\n * @returns {Promise<string>} The resolved password.\n * @throws when the fixture is a placeholder, or neither env nor Vault yields a value.\n */\nexport const resolveE2EPassword = async (\n username: string,\n options: ResolveE2EPasswordOptions = {}\n): Promise<string> => {\n if (username === \"\" || username === \"REPLACE_ME\") {\n return fail(`E2E credentials: fixture account name is unresolved (\"${username}\").`);\n }\n const cached = passwordCache.get(username);\n if (cached !== undefined) {\n return cached;\n }\n\n const env = options.env ?? process.env;\n const key = deriveEnvKey(username);\n\n // CI path: an injected env variable wins and Vault is never contacted.\n const fromEnv = env[key];\n if (fromEnv !== undefined && fromEnv !== \"\") {\n passwordCache.set(username, fromEnv);\n return fromEnv;\n }\n\n // Local path: read the password from Vault by the same key.\n const createVault = options.createVault ?? defaultCreateVault;\n const vault = await createVault(env);\n if (vault === undefined) {\n return fail(\n `E2E credentials for \"${username}\": no env variable (${key}) and Vault is unavailable ` +\n `(not authenticated / not on VPN). Run \\`vault login -address=${env.E2E_VAULT_ENDPOINT} -method=oidc -path=okta\\` on VPN, or set the env variable.`\n );\n }\n\n const logicalPath = env.E2E_VAULT_PATH;\n if (!logicalPath) {\n return fail(\"E2E_VAULT_PATH is not set\");\n }\n const readPath = logicalPath;\n\n let result: unknown;\n try {\n result = await vault.read(readPath);\n } catch (error) {\n if (isVaultUnreachableError(error)) {\n return fail(\n `E2E credentials for \"${username}\": Vault endpoint is unreachable ` +\n `(likely not on VPN, or E2E_VAULT_ENDPOINT is misconfigured). ` +\n `Connect to the VPN and retry, or set the ${key} env variable, right now vault is trying to connect to ${env.E2E_VAULT_ENDPOINT}.`\n );\n }\n const status = getVaultErrorStatus(error);\n if (status !== undefined && VAULT_UNAUTHENTICATED_STATUS.has(status)) {\n return fail(\n `E2E credentials for \"${username}\": Vault rejected the request (HTTP ${status}) — ` +\n `your Vault token is missing or expired. ` +\n `Run something like \\`vault login -address=${env.E2E_VAULT_ENDPOINT} -method=oidc -path=okta\\` on VPN, or set the ${key} env variable, right now vault is trying to connect to a logged in vault at ${env.E2E_VAULT_ENDPOINT}.`\n );\n }\n return fail(\n `E2E credentials for \"${username}\": Vault read of \"${readPath}\" failed on host ${env.E2E_VAULT_ENDPOINT}.`\n );\n }\n\n const fromVault = readVaultField(result, deriveVaultField(username));\n if (fromVault === undefined) {\n return fail(\n `E2E credentials for \"${username}\": no field \"${deriveVaultField(username)}\" at Vault path \"${readPath}\".`\n );\n }\n passwordCache.set(username, fromVault);\n return fromVault;\n};\n"]}