@trackunit/iris-app-playwright 0.2.113 → 0.2.114-alpha-bd50f0f7911.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/package.json +1 -1
- package/src/fixtures/hostReadiness.d.ts +19 -0
- package/src/fixtures/hostReadiness.js +28 -0
- package/src/fixtures/hostReadiness.js.map +1 -0
- package/src/fixtures/login.fixture.d.ts +87 -0
- package/src/fixtures/login.fixture.js +165 -12
- package/src/fixtures/login.fixture.js.map +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when a feature-branch host never becomes ready.
|
|
3
|
+
*
|
|
4
|
+
* Root cause (GLU-2631): feature-branch login had no shared error whose
|
|
5
|
+
* message is exactly "host never became ready", and no gate on a non-empty
|
|
6
|
+
* `NX_FEATURE_BRANCH_BASE_URL`. A host that never served `/env` or a stuck
|
|
7
|
+
* session handoff therefore failed as a page error. Later phases import this
|
|
8
|
+
* error and {@link isFeatureBranchHost}.
|
|
9
|
+
*/
|
|
10
|
+
export declare class HostNeverBecameReadyError extends Error {
|
|
11
|
+
/** Creates an error whose message is exactly "host never became ready". The wait that ended is kept as `cause`. */
|
|
12
|
+
constructor(cause?: unknown);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* True only when `NX_FEATURE_BRANCH_BASE_URL` is a non-empty string.
|
|
16
|
+
*
|
|
17
|
+
* Unset or `""` leaves the gate off. Staging and production do not set the variable.
|
|
18
|
+
*/
|
|
19
|
+
export declare const isFeatureBranchHost: () => boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isFeatureBranchHost = exports.HostNeverBecameReadyError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Thrown when a feature-branch host never becomes ready.
|
|
6
|
+
*
|
|
7
|
+
* Root cause (GLU-2631): feature-branch login had no shared error whose
|
|
8
|
+
* message is exactly "host never became ready", and no gate on a non-empty
|
|
9
|
+
* `NX_FEATURE_BRANCH_BASE_URL`. A host that never served `/env` or a stuck
|
|
10
|
+
* session handoff therefore failed as a page error. Later phases import this
|
|
11
|
+
* error and {@link isFeatureBranchHost}.
|
|
12
|
+
*/
|
|
13
|
+
class HostNeverBecameReadyError extends Error {
|
|
14
|
+
/** Creates an error whose message is exactly "host never became ready". The wait that ended is kept as `cause`. */
|
|
15
|
+
constructor(cause) {
|
|
16
|
+
super("host never became ready", cause === undefined ? undefined : { cause });
|
|
17
|
+
this.name = "HostNeverBecameReadyError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.HostNeverBecameReadyError = HostNeverBecameReadyError;
|
|
21
|
+
/**
|
|
22
|
+
* True only when `NX_FEATURE_BRANCH_BASE_URL` is a non-empty string.
|
|
23
|
+
*
|
|
24
|
+
* Unset or `""` leaves the gate off. Staging and production do not set the variable.
|
|
25
|
+
*/
|
|
26
|
+
const isFeatureBranchHost = () => (process.env.NX_FEATURE_BRANCH_BASE_URL ?? "") !== "";
|
|
27
|
+
exports.isFeatureBranchHost = isFeatureBranchHost;
|
|
28
|
+
//# sourceMappingURL=hostReadiness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hostReadiness.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/fixtures/hostReadiness.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;GAQG;AACH,MAAa,yBAA0B,SAAQ,KAAK;IAClD,mHAAmH;IACnH,YAAmB,KAAe;QAChC,KAAK,CAAC,yBAAyB,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9E,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;IAC1C,CAAC;CACF;AAND,8DAMC;AAED;;;;GAIG;AACI,MAAM,mBAAmB,GAAG,GAAY,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC;AAA3F,QAAA,mBAAmB,uBAAwE","sourcesContent":["/**\n * Thrown when a feature-branch host never becomes ready.\n *\n * Root cause (GLU-2631): feature-branch login had no shared error whose\n * message is exactly \"host never became ready\", and no gate on a non-empty\n * `NX_FEATURE_BRANCH_BASE_URL`. A host that never served `/env` or a stuck\n * session handoff therefore failed as a page error. Later phases import this\n * error and {@link isFeatureBranchHost}.\n */\nexport class HostNeverBecameReadyError extends Error {\n /** Creates an error whose message is exactly \"host never became ready\". The wait that ended is kept as `cause`. */\n public constructor(cause?: unknown) {\n super(\"host never became ready\", cause === undefined ? undefined : { cause });\n this.name = \"HostNeverBecameReadyError\";\n }\n}\n\n/**\n * True only when `NX_FEATURE_BRANCH_BASE_URL` is a non-empty string.\n *\n * Unset or `\"\"` leaves the gate off. Staging and production do not set the variable.\n */\nexport const isFeatureBranchHost = (): boolean => (process.env.NX_FEATURE_BRANCH_BASE_URL ?? \"\") !== \"\";\n"]}
|
|
@@ -20,6 +20,8 @@ export declare const ENV_READY_TIMEOUT_MS = 300000;
|
|
|
20
20
|
*/
|
|
21
21
|
export declare const ENV_READY_TIMEOUT_ENV_VAR = "NX_E2E_ENV_READY_TIMEOUT_MS";
|
|
22
22
|
export declare const ENV_READY_POLL_INTERVAL_MS = 5000;
|
|
23
|
+
/** Gap between reloads of a manager-classic session document that did not load. */
|
|
24
|
+
export declare const SESSION_DOCUMENT_RETRY_DELAY_MS = 1000;
|
|
23
25
|
export declare const MAIN_MENU_READY_SELECTOR = "[data-testid=\"mainMenu\"]:not(:has([data-testid=\"mainMenuItems-skeleton\"], [data-testid=\"menu-items-skeleton\"]))";
|
|
24
26
|
type MainMenuReadinessInput = {
|
|
25
27
|
readonly selector: string;
|
|
@@ -40,6 +42,17 @@ type TestTimeoutInfo = {
|
|
|
40
42
|
* Playwright and is left untouched.
|
|
41
43
|
*/
|
|
42
44
|
export declare const extendTestTimeout: (testInfo: TestTimeoutInfo | undefined, budgetMs: number) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Gives the feature-branch session handoff its own readiness budget.
|
|
47
|
+
*
|
|
48
|
+
* Root cause (GLU-2631): after `/env` answers, a feature-branch host can still
|
|
49
|
+
* serve a failed session document or stay on `manager-classic` for a while.
|
|
50
|
+
* Those waits share {@link MAIN_MENU_READY_TIMEOUT_MS} after `/env` is ready,
|
|
51
|
+
* so the test timeout is extended by that budget before the slow path. The
|
|
52
|
+
* `/env` wait extends it by its own budget. A host that is not a feature
|
|
53
|
+
* branch is unchanged.
|
|
54
|
+
*/
|
|
55
|
+
export declare const reserveFeatureBranchLoginBudget: (testInfo?: TestTimeoutInfo) => void;
|
|
43
56
|
/** Returns whether the main menu is ready or intentionally hidden for the current route. */
|
|
44
57
|
export declare const isMainMenuReady: ({ selector, fragments }: MainMenuReadinessInput) => boolean;
|
|
45
58
|
/**
|
|
@@ -56,6 +69,11 @@ type EnvReadyRequestContext<TResponse extends EnvReadyResponse> = {
|
|
|
56
69
|
timeout?: number;
|
|
57
70
|
}) => Promise<TResponse>;
|
|
58
71
|
};
|
|
72
|
+
/** Thrown by {@link waitForEnvReady} when `/env` did not answer 200 inside its budget. */
|
|
73
|
+
export declare class EnvReadyTimeoutError extends Error {
|
|
74
|
+
/** @param message the budget, host and last status/error, as reported by the poller */
|
|
75
|
+
constructor(message: string);
|
|
76
|
+
}
|
|
59
77
|
/**
|
|
60
78
|
* Polls `GET /env` on the branch host until it returns 200 and resolves with
|
|
61
79
|
* that response, untouched. Non-200 responses (404 while Envoy has no route
|
|
@@ -85,4 +103,73 @@ export interface LoginCacheFixtures {
|
|
|
85
103
|
* always starts with empty caches.
|
|
86
104
|
*/
|
|
87
105
|
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & TrackunitBaseFixtures, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions & LoginCacheFixtures>;
|
|
106
|
+
type LoginJsonBody = {
|
|
107
|
+
auth?: {
|
|
108
|
+
url?: unknown;
|
|
109
|
+
};
|
|
110
|
+
sessionToken?: string;
|
|
111
|
+
};
|
|
112
|
+
type LoginEnvResponse = EnvReadyResponse & {
|
|
113
|
+
json: () => Promise<LoginJsonBody>;
|
|
114
|
+
};
|
|
115
|
+
type LoginRequestContext = {
|
|
116
|
+
get: (url: string, options?: {
|
|
117
|
+
timeout?: number;
|
|
118
|
+
}) => Promise<LoginEnvResponse>;
|
|
119
|
+
post: (url: string, options: {
|
|
120
|
+
data: {
|
|
121
|
+
username: string;
|
|
122
|
+
password: string;
|
|
123
|
+
options: {
|
|
124
|
+
warnBeforePasswordExpired: boolean;
|
|
125
|
+
multiOptionalFactorEnroll: boolean;
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
headers: {
|
|
129
|
+
"Content-Type": "application/json";
|
|
130
|
+
};
|
|
131
|
+
timeout: number;
|
|
132
|
+
}) => Promise<{
|
|
133
|
+
ok: () => boolean;
|
|
134
|
+
json: () => Promise<LoginJsonBody>;
|
|
135
|
+
}>;
|
|
136
|
+
dispose: () => Promise<void>;
|
|
137
|
+
};
|
|
138
|
+
type SessionDocument = {
|
|
139
|
+
ok: () => boolean;
|
|
140
|
+
};
|
|
141
|
+
type OktaLoginPage = {
|
|
142
|
+
goto: (url: string, options?: {
|
|
143
|
+
timeout?: number;
|
|
144
|
+
}) => Promise<SessionDocument | null | undefined>;
|
|
145
|
+
waitForURL: (url: (url: URL) => boolean, options?: {
|
|
146
|
+
timeout?: number;
|
|
147
|
+
}) => Promise<unknown>;
|
|
148
|
+
locator: (selector: string) => {
|
|
149
|
+
waitFor: (options: {
|
|
150
|
+
state: "visible";
|
|
151
|
+
timeout: number;
|
|
152
|
+
}) => Promise<unknown>;
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Performs the slow path: wait for the branch host to serve `/env`, hit Okta's
|
|
157
|
+
* `/api/v1/authn`, redeem the `sessionToken` via the manager-classic redirect,
|
|
158
|
+
* and wait for the host layout to render. Factored out so the cache hit/miss
|
|
159
|
+
* branches read clearly.
|
|
160
|
+
*
|
|
161
|
+
* `createRequestContext`, `now`, and `sleep` are injectable so a unit test can
|
|
162
|
+
* drive the session handoff without a browser or a real three-minute wait.
|
|
163
|
+
*/
|
|
164
|
+
export declare const performOktaLogin: ({ page, baseURL, credentials, testInfo, createRequestContext, now, sleep: pause, }: {
|
|
165
|
+
page: OktaLoginPage;
|
|
166
|
+
baseURL: string | undefined;
|
|
167
|
+
credentials: AuthCredentials;
|
|
168
|
+
testInfo?: TestTimeoutInfo;
|
|
169
|
+
createRequestContext?: (baseURL: string | undefined) => Promise<LoginRequestContext>;
|
|
170
|
+
/** Injectable clock. Production uses `Date.now`. The handoff budget is {@link MAIN_MENU_READY_TIMEOUT_MS}. */
|
|
171
|
+
now?: () => number;
|
|
172
|
+
/** Injectable pause between session document reloads. Production waits on the event loop. */
|
|
173
|
+
sleep?: (ms: number) => Promise<void>;
|
|
174
|
+
}) => Promise<void>;
|
|
88
175
|
export { expect } from "@playwright/test";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.expect = exports.test = exports.waitForEnvReady = exports.waitForMainMenuReady = exports.isMainMenuReady = exports.extendTestTimeout = exports.MAIN_MENU_READY_SELECTOR = 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 hostReadiness_1 = require("./hostReadiness");
|
|
5
6
|
const hydrateStorageState_1 = require("./hydrateStorageState");
|
|
6
7
|
const loginCacheKey_1 = require("./loginCacheKey");
|
|
7
8
|
const resolveCredentials_1 = require("./resolveCredentials");
|
|
@@ -25,6 +26,12 @@ exports.ENV_READY_TIMEOUT_ENV_VAR = "NX_E2E_ENV_READY_TIMEOUT_MS";
|
|
|
25
26
|
exports.ENV_READY_POLL_INTERVAL_MS = 5000;
|
|
26
27
|
/** Floor for a single `/env` request so the last attempt before the deadline is still meaningful. */
|
|
27
28
|
const ENV_READY_MIN_REQUEST_TIMEOUT_MS = 1000;
|
|
29
|
+
/** Fixed timeout for Okta's `/api/v1/authn` call; it is not retried against the login budget. */
|
|
30
|
+
const OKTA_AUTH_TIMEOUT_MS = 20000;
|
|
31
|
+
/** Single wait for the manager-classic session handoff to leave that page. */
|
|
32
|
+
const SESSION_HANDOFF_TIMEOUT_MS = 20000;
|
|
33
|
+
/** Gap between reloads of a manager-classic session document that did not load. */
|
|
34
|
+
exports.SESSION_DOCUMENT_RETRY_DELAY_MS = 1000;
|
|
28
35
|
const resolveEnvReadyTimeoutMs = () => {
|
|
29
36
|
const raw = process.env[exports.ENV_READY_TIMEOUT_ENV_VAR];
|
|
30
37
|
if (raw === undefined || raw.trim() === "") {
|
|
@@ -45,6 +52,23 @@ const extendTestTimeout = (testInfo, budgetMs) => {
|
|
|
45
52
|
}
|
|
46
53
|
};
|
|
47
54
|
exports.extendTestTimeout = extendTestTimeout;
|
|
55
|
+
/**
|
|
56
|
+
* Gives the feature-branch session handoff its own readiness budget.
|
|
57
|
+
*
|
|
58
|
+
* Root cause (GLU-2631): after `/env` answers, a feature-branch host can still
|
|
59
|
+
* serve a failed session document or stay on `manager-classic` for a while.
|
|
60
|
+
* Those waits share {@link MAIN_MENU_READY_TIMEOUT_MS} after `/env` is ready,
|
|
61
|
+
* so the test timeout is extended by that budget before the slow path. The
|
|
62
|
+
* `/env` wait extends it by its own budget. A host that is not a feature
|
|
63
|
+
* branch is unchanged.
|
|
64
|
+
*/
|
|
65
|
+
const reserveFeatureBranchLoginBudget = (testInfo) => {
|
|
66
|
+
if (!(0, hostReadiness_1.isFeatureBranchHost)()) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
(0, exports.extendTestTimeout)(testInfo, exports.MAIN_MENU_READY_TIMEOUT_MS);
|
|
70
|
+
};
|
|
71
|
+
exports.reserveFeatureBranchLoginBudget = reserveFeatureBranchLoginBudget;
|
|
48
72
|
/** Returns whether the main menu is ready or intentionally hidden for the current route. */
|
|
49
73
|
const isMainMenuReady = ({ selector, fragments }) => {
|
|
50
74
|
if (document.querySelector(selector) !== null) {
|
|
@@ -73,6 +97,15 @@ const describeError = (error) => (error instanceof Error ? error.message : Strin
|
|
|
73
97
|
* branch resolves immediately and `ENOTFOUND` is never a transient state.
|
|
74
98
|
*/
|
|
75
99
|
const isHostUnreachableError = (error) => /\b(ECONNREFUSED|ENOTFOUND)\b/.test(describeError(error));
|
|
100
|
+
/** Thrown by {@link waitForEnvReady} when `/env` did not answer 200 inside its budget. */
|
|
101
|
+
class EnvReadyTimeoutError extends Error {
|
|
102
|
+
/** @param message the budget, host and last status/error, as reported by the poller */
|
|
103
|
+
constructor(message) {
|
|
104
|
+
super(message);
|
|
105
|
+
this.name = "EnvReadyTimeoutError";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.EnvReadyTimeoutError = EnvReadyTimeoutError;
|
|
76
109
|
/**
|
|
77
110
|
* Polls `GET /env` on the branch host until it returns 200 and resolves with
|
|
78
111
|
* that response, untouched. Non-200 responses (404 while Envoy has no route
|
|
@@ -94,7 +127,7 @@ const waitForEnvReady = async (context, baseURL, testInfo) => {
|
|
|
94
127
|
// Always make one attempt; after that stop as soon as the budget is spent
|
|
95
128
|
// so the error reports the last real outcome rather than a rushed retry.
|
|
96
129
|
if (attempted && remainingMs <= 0) {
|
|
97
|
-
throw new
|
|
130
|
+
throw new EnvReadyTimeoutError(`Timed out after ${timeoutMs} ms waiting for ${envUrl} to return 200 (${lastOutcome})`);
|
|
98
131
|
}
|
|
99
132
|
// Cap each request so a hanging connection cannot run far past the deadline.
|
|
100
133
|
const requestTimeout = Math.min(exports.ENV_READY_POLL_INTERVAL_MS, Math.max(remainingMs, ENV_READY_MIN_REQUEST_TIMEOUT_MS));
|
|
@@ -151,7 +184,8 @@ exports.test = test_1.test.extend({
|
|
|
151
184
|
return;
|
|
152
185
|
}
|
|
153
186
|
}
|
|
154
|
-
|
|
187
|
+
(0, exports.reserveFeatureBranchLoginBudget)(testInfo);
|
|
188
|
+
await (0, exports.performOktaLogin)({ page, baseURL, credentials, testInfo });
|
|
155
189
|
// Capture the fully-authenticated state for reuse by later tests in
|
|
156
190
|
// this worker. Errors here would mask the actual login success, so we
|
|
157
191
|
// never let snapshot capture throw — at worst we'll re-hit Okta next
|
|
@@ -168,23 +202,141 @@ exports.test = test_1.test.extend({
|
|
|
168
202
|
await provide(loginFn);
|
|
169
203
|
},
|
|
170
204
|
});
|
|
205
|
+
const openDefaultRequestContext = async (baseURL) => {
|
|
206
|
+
const context = await test_1.request.newContext({ baseURL });
|
|
207
|
+
return {
|
|
208
|
+
// Playwright's `get<T>` types the JSON body; without the pin it is `any`.
|
|
209
|
+
get: (url, options) => context.get(url, options),
|
|
210
|
+
post: (url, options) => context.post(url, options),
|
|
211
|
+
dispose: () => context.dispose(),
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Reads `GET /env` through {@link waitForEnvReady}.
|
|
216
|
+
*
|
|
217
|
+
* On a feature-branch host, spending that budget is the host never becoming
|
|
218
|
+
* ready, so the timeout is rethrown as {@link HostNeverBecameReadyError} with
|
|
219
|
+
* the original error as `cause`. An unreachable host still fails fast, and a
|
|
220
|
+
* host that is not a feature branch keeps the `/env` timeout message.
|
|
221
|
+
*/
|
|
222
|
+
const readHostEnv = async (context, baseURL, testInfo) => {
|
|
223
|
+
try {
|
|
224
|
+
const envResponse = await (0, exports.waitForEnvReady)(context, baseURL, testInfo);
|
|
225
|
+
return await envResponse.json();
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
if ((0, hostReadiness_1.isFeatureBranchHost)() && error instanceof EnvReadyTimeoutError) {
|
|
229
|
+
throw new hostReadiness_1.HostNeverBecameReadyError(error);
|
|
230
|
+
}
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const leftManagerClassic = (url) => !url.href.includes("manager-classic");
|
|
235
|
+
const isPlaywrightTimeout = (error) => error instanceof test_1.errors.TimeoutError;
|
|
236
|
+
/**
|
|
237
|
+
* Waits until the manager-classic session handoff leaves that page.
|
|
238
|
+
*
|
|
239
|
+
* Root cause (GLU-2631): feature-branch login waited once for 20s. While that
|
|
240
|
+
* host is still booting, the URL stays on manager-classic and `waitForURL`
|
|
241
|
+
* throws a navigation timeout, so the rest of the readiness budget was never
|
|
242
|
+
* used. When the feature-branch gate is on, retry until the URL leaves that
|
|
243
|
+
* page or the deadline ends, then throw {@link HostNeverBecameReadyError}.
|
|
244
|
+
* Each attempt waits up to 20s, capped by the time left, so the loop does not
|
|
245
|
+
* busy-spin. An unset or empty `NX_FEATURE_BRANCH_BASE_URL` keeps the single
|
|
246
|
+
* 20s wait.
|
|
247
|
+
*/
|
|
248
|
+
const waitForSessionHandoff = async (page, now, deadline) => {
|
|
249
|
+
if (deadline === undefined) {
|
|
250
|
+
await page.waitForURL(leftManagerClassic, { timeout: SESSION_HANDOFF_TIMEOUT_MS });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
let current = now();
|
|
254
|
+
let lastTimeout;
|
|
255
|
+
while (current < deadline) {
|
|
256
|
+
const timeout = Math.min(SESSION_HANDOFF_TIMEOUT_MS, deadline - current);
|
|
257
|
+
try {
|
|
258
|
+
await page.waitForURL(leftManagerClassic, { timeout });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (!isPlaywrightTimeout(error)) {
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
lastTimeout = error;
|
|
266
|
+
}
|
|
267
|
+
current = now();
|
|
268
|
+
}
|
|
269
|
+
throw new hostReadiness_1.HostNeverBecameReadyError(lastTimeout);
|
|
270
|
+
};
|
|
271
|
+
const documentLoaded = (response) => {
|
|
272
|
+
if (response === null || response === undefined) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
return response.ok();
|
|
276
|
+
};
|
|
277
|
+
/**
|
|
278
|
+
* Opens the manager-classic session URL.
|
|
279
|
+
*
|
|
280
|
+
* A feature-branch host that is still booting can answer `page.goto` with a
|
|
281
|
+
* 502 or an empty document. The URL stays on that page, so waiting for it to
|
|
282
|
+
* change never sees a later healthy load. Retry the navigation until the
|
|
283
|
+
* document loads or the deadline ends. A navigation that uses up the time
|
|
284
|
+
* left throws {@link HostNeverBecameReadyError}. The same session URL is
|
|
285
|
+
* reused: a failed document does not redeem the hash token. With the gate off,
|
|
286
|
+
* this is one `page.goto` and the browser's own navigation timeout.
|
|
287
|
+
*/
|
|
288
|
+
const gotoSessionDocument = async (page, url, now, deadline, pause) => {
|
|
289
|
+
if (deadline === undefined) {
|
|
290
|
+
await page.goto(url);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
while (now() < deadline) {
|
|
294
|
+
const remaining = deadline - now();
|
|
295
|
+
if (remaining <= 0) {
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const response = await page.goto(url, { timeout: remaining });
|
|
300
|
+
if (documentLoaded(response)) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
if (!isPlaywrightTimeout(error)) {
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
throw new hostReadiness_1.HostNeverBecameReadyError(error);
|
|
309
|
+
}
|
|
310
|
+
const remainingAfter = deadline - now();
|
|
311
|
+
if (remainingAfter <= 0) {
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
await pause(Math.min(exports.SESSION_DOCUMENT_RETRY_DELAY_MS, remainingAfter));
|
|
315
|
+
}
|
|
316
|
+
throw new hostReadiness_1.HostNeverBecameReadyError();
|
|
317
|
+
};
|
|
171
318
|
/**
|
|
172
319
|
* Performs the slow path: wait for the branch host to serve `/env`, hit Okta's
|
|
173
320
|
* `/api/v1/authn`, redeem the `sessionToken` via the manager-classic redirect,
|
|
174
321
|
* and wait for the host layout to render. Factored out so the cache hit/miss
|
|
175
322
|
* branches read clearly.
|
|
323
|
+
*
|
|
324
|
+
* `createRequestContext`, `now`, and `sleep` are injectable so a unit test can
|
|
325
|
+
* drive the session handoff without a browser or a real three-minute wait.
|
|
176
326
|
*/
|
|
177
|
-
const performOktaLogin = async ({ page, baseURL, credentials, testInfo, }) => {
|
|
178
|
-
const context = await
|
|
327
|
+
const performOktaLogin = async ({ page, baseURL, credentials, testInfo, createRequestContext = openDefaultRequestContext, now = Date.now, sleep: pause = sleep, }) => {
|
|
328
|
+
const context = await createRequestContext(baseURL);
|
|
179
329
|
try {
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
const env = await envResponse.json();
|
|
183
|
-
const domainValue = env?.auth?.url;
|
|
330
|
+
const env = await readHostEnv(context, baseURL, testInfo);
|
|
331
|
+
const domainValue = env.auth?.url;
|
|
184
332
|
if (typeof domainValue !== "string" || domainValue === "") {
|
|
185
333
|
throw new Error(`No domain found from servers /env found env: ${JSON.stringify(env)}`);
|
|
186
334
|
}
|
|
187
335
|
const domain = domainValue;
|
|
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).
|
|
339
|
+
const readyDeadline = (0, hostReadiness_1.isFeatureBranchHost)() ? now() + exports.MAIN_MENU_READY_TIMEOUT_MS : undefined;
|
|
188
340
|
const authResponse = await context.post(`${domain}/api/v1/authn`, {
|
|
189
341
|
data: {
|
|
190
342
|
username: credentials.username,
|
|
@@ -195,21 +347,22 @@ const performOktaLogin = async ({ page, baseURL, credentials, testInfo, }) => {
|
|
|
195
347
|
},
|
|
196
348
|
},
|
|
197
349
|
headers: { "Content-Type": "application/json" },
|
|
198
|
-
timeout:
|
|
350
|
+
timeout: OKTA_AUTH_TIMEOUT_MS,
|
|
199
351
|
});
|
|
200
352
|
if (!authResponse.ok()) {
|
|
201
353
|
const body = await authResponse.json().catch(() => ({}));
|
|
202
354
|
throw new Error(`Could not get a session token for user: ${credentials.username}, ${JSON.stringify(body)}`);
|
|
203
355
|
}
|
|
204
356
|
const { sessionToken } = await authResponse.json();
|
|
205
|
-
await page
|
|
206
|
-
await page
|
|
357
|
+
await gotoSessionDocument(page, `/auth/manager-classic#session_token=${sessionToken}&userProfile=true`, now, readyDeadline, pause);
|
|
358
|
+
await waitForSessionHandoff(page, now, readyDeadline);
|
|
207
359
|
await page.locator("#host-layout-content").waitFor({ state: "visible", timeout: 15000 });
|
|
208
360
|
}
|
|
209
361
|
finally {
|
|
210
362
|
await context.dispose();
|
|
211
363
|
}
|
|
212
364
|
};
|
|
365
|
+
exports.performOktaLogin = performOktaLogin;
|
|
213
366
|
var test_2 = require("@playwright/test");
|
|
214
367
|
Object.defineProperty(exports, "expect", { enumerable: true, get: function () { return test_2.expect; } });
|
|
215
368
|
//# sourceMappingURL=login.fixture.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.fixture.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/fixtures/login.fixture.ts"],"names":[],"mappings":"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","sourcesContent":["import { test as base, request, type APIResponse, type Page } from \"@playwright/test\";\nimport { hydrateStorageState, type CachedStorageState } from \"./hydrateStorageState\";\nimport { buildLoginCacheKey } from \"./loginCacheKey\";\nimport { resolveCredentials, type AuthCredentials, type ResolveCredentialsOptions } from \"./resolveCredentials\";\n\nexport type { AuthCredentials };\n\nexport type LoginOptions = ResolveCredentialsOptions;\n\n/** Matches HostLayout's `hideMenuForUrls` — routes where MainMenu is not rendered. */\nexport const HIDE_MENU_URL_FRAGMENTS = [\"/marketplace\", \"/auth/\", \"/terms\"] as const;\n\nexport const MAIN_MENU_READY_TIMEOUT_MS = 180_000;\n\n/**\n * Default budget for the first login on a worker to wait for the branch host\n * to answer `GET /env` with 200. The deploy job only waits for ArgoCD health,\n * which does not cover the Emissary/Envoy route for a freshly created feature\n * branch, so the host can return 404 for several minutes after the deploy job\n * is green. Override per run with {@link ENV_READY_TIMEOUT_ENV_VAR}.\n */\nexport const ENV_READY_TIMEOUT_MS = 300_000;\n\n/**\n * Environment variable that overrides {@link ENV_READY_TIMEOUT_MS} (in\n * milliseconds). Useful for local runs against a fixed host where a long wait\n * only delays the inevitable failure.\n */\nexport const ENV_READY_TIMEOUT_ENV_VAR = \"NX_E2E_ENV_READY_TIMEOUT_MS\";\n\nexport const ENV_READY_POLL_INTERVAL_MS = 5_000;\n\n/** Floor for a single `/env` request so the last attempt before the deadline is still meaningful. */\nconst ENV_READY_MIN_REQUEST_TIMEOUT_MS = 1_000;\n\nconst resolveEnvReadyTimeoutMs = (): number => {\n const raw = process.env[ENV_READY_TIMEOUT_ENV_VAR];\n if (raw === undefined || raw.trim() === \"\") {\n return ENV_READY_TIMEOUT_MS;\n }\n const parsed = Number(raw);\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : ENV_READY_TIMEOUT_MS;\n};\n\nexport const MAIN_MENU_READY_SELECTOR =\n '[data-testid=\"mainMenu\"]:not(:has([data-testid=\"mainMenuItems-skeleton\"], [data-testid=\"menu-items-skeleton\"]))';\n\ntype MainMenuReadinessInput = {\n readonly selector: string;\n readonly fragments: ReadonlyArray<string>;\n};\n\ntype MainMenuWaitPage = {\n waitForFunction: (\n pageFunction: (input: MainMenuReadinessInput) => boolean,\n input: MainMenuReadinessInput,\n options?: { timeout?: number }\n ) => Promise<unknown>;\n};\n\ntype TestTimeoutInfo = {\n timeout: number;\n setTimeout: (timeout: number) => void;\n};\n\n/**\n * Grows the running test's timeout by `budgetMs` so a readiness wait cannot\n * eat into the test's own budget. A timeout of 0 means \"unlimited\" in\n * Playwright and is left untouched.\n */\nexport const extendTestTimeout = (testInfo: TestTimeoutInfo | undefined, budgetMs: number): void => {\n if (testInfo !== undefined && testInfo.timeout !== 0) {\n testInfo.setTimeout(testInfo.timeout + budgetMs);\n }\n};\n\n/** Returns whether the main menu is ready or intentionally hidden for the current route. */\nexport const isMainMenuReady = ({ selector, fragments }: MainMenuReadinessInput): boolean => {\n if (document.querySelector(selector) !== null) {\n return true;\n }\n return fragments.some(fragment => window.location.pathname.includes(fragment));\n};\n\n/**\n * Waits until Manager's main menu is rendered without a loading skeleton, or\n * until the browser is on a HostLayout hide-menu route (marketplace / auth /\n * terms) where the menu is intentionally absent.\n */\nexport const waitForMainMenuReady = async (page: MainMenuWaitPage, testInfo?: TestTimeoutInfo): Promise<void> => {\n extendTestTimeout(testInfo, MAIN_MENU_READY_TIMEOUT_MS);\n\n await page.waitForFunction(\n isMainMenuReady,\n { selector: MAIN_MENU_READY_SELECTOR, fragments: HIDE_MENU_URL_FRAGMENTS },\n { timeout: MAIN_MENU_READY_TIMEOUT_MS }\n );\n};\n\ntype EnvReadyResponse = {\n status: () => number;\n};\n\ntype EnvReadyRequestContext<TResponse extends EnvReadyResponse> = {\n get: (url: string, options?: { timeout?: number }) => Promise<TResponse>;\n};\n\nconst sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));\n\nconst describeError = (error: unknown): string => (error instanceof Error ? error.message : String(error));\n\n/**\n * Nothing is listening on the host (local dev server not started, typo in\n * `baseURL`). Waiting will not change that, so these fail immediately instead\n * of burning the readiness budget. This relies on feature-branch hosts living\n * under a wildcard DNS record (`*.dev.manager.trackunit.com`), so a brand-new\n * branch resolves immediately and `ENOTFOUND` is never a transient state.\n */\nconst isHostUnreachableError = (error: unknown): boolean => /\\b(ECONNREFUSED|ENOTFOUND)\\b/.test(describeError(error));\n\n/**\n * Polls `GET /env` on the branch host until it returns 200 and resolves with\n * that response, untouched. Non-200 responses (404 while Envoy has no route\n * yet, 503 while pods start) and timed-out requests are retried every\n * {@link ENV_READY_POLL_INTERVAL_MS} until the budget ({@link ENV_READY_TIMEOUT_MS},\n * or {@link ENV_READY_TIMEOUT_ENV_VAR}) is spent. The resulting error names the\n * host and the last status/error so the failure is attributable to the\n * environment rather than to the test. Connection-level failures fail fast.\n */\nexport const waitForEnvReady = async <TResponse extends EnvReadyResponse>(\n context: EnvReadyRequestContext<TResponse>,\n baseURL: string | undefined,\n testInfo?: TestTimeoutInfo\n): Promise<TResponse> => {\n const timeoutMs = resolveEnvReadyTimeoutMs();\n extendTestTimeout(testInfo, timeoutMs);\n\n const envUrl = `${baseURL ?? \"\"}/env`;\n const deadline = Date.now() + timeoutMs;\n let lastOutcome = \"no response\";\n let attempted = false;\n\n for (;;) {\n const remainingMs = deadline - Date.now();\n // Always make one attempt; after that stop as soon as the budget is spent\n // so the error reports the last real outcome rather than a rushed retry.\n if (attempted && remainingMs <= 0) {\n throw new Error(`Timed out after ${timeoutMs} ms waiting for ${envUrl} to return 200 (${lastOutcome})`);\n }\n\n // Cap each request so a hanging connection cannot run far past the deadline.\n const requestTimeout = Math.min(\n ENV_READY_POLL_INTERVAL_MS,\n Math.max(remainingMs, ENV_READY_MIN_REQUEST_TIMEOUT_MS)\n );\n attempted = true;\n try {\n const response = await context.get(\"/env\", { timeout: requestTimeout });\n const status = response.status();\n if (status === 200) {\n return response;\n }\n lastOutcome = `last status: ${status}`;\n } catch (error) {\n if (isHostUnreachableError(error)) {\n throw new Error(`Could not reach ${envUrl}, is the server running? (${describeError(error)})`);\n }\n lastOutcome = `last error: ${describeError(error)}`;\n }\n\n await sleep(Math.max(0, Math.min(ENV_READY_POLL_INTERVAL_MS, deadline - Date.now())));\n }\n};\n\nexport interface TrackunitBaseFixtures {\n login: (options?: LoginOptions) => Promise<void>;\n}\n\n/**\n * Worker-scoped, in-memory cache of authenticated `storageState` snapshots,\n * keyed by `(baseURL, username)`. Implementation detail of the `login`\n * fixture — exposed on the fixtures object so Playwright can manage its\n * lifecycle, but prefixed with `_` to signal \"don't touch from tests\".\n */\nexport interface LoginCacheFixtures {\n _loginCache: Map<string, CachedStorageState>;\n}\n\n/**\n * Each worker is its own Node process, so a `Map` is sufficient — no\n * cross-worker sharing concerns, no disk I/O, no cleanup needed. The cache\n * dies with the worker, which is the desired behaviour: a fresh `nx run`\n * always starts with empty caches.\n */\nexport const test = base.extend<TrackunitBaseFixtures, LoginCacheFixtures>({\n _loginCache: [\n async ({}, provide) => {\n await provide(new Map<string, CachedStorageState>());\n },\n { scope: \"worker\" },\n ],\n\n login: async ({ page, baseURL, _loginCache }, provide, testInfo) => {\n const loginFn = async (options?: LoginOptions) => {\n const credentials = await resolveCredentials(options);\n const cacheKey = buildLoginCacheKey(baseURL, credentials);\n const cached = _loginCache.get(cacheKey);\n\n if (cached !== undefined) {\n let cachedSessionRestored = false;\n try {\n await hydrateStorageState(page.context(), cached);\n await page.goto(\"/\");\n await page.locator(\"#host-layout-content\").waitFor({ state: \"visible\", timeout: 15000 });\n cachedSessionRestored = true;\n } catch {\n _loginCache.delete(cacheKey);\n }\n\n if (cachedSessionRestored) {\n await waitForMainMenuReady(page, testInfo);\n return;\n }\n }\n\n await performOktaLogin({ page, baseURL, credentials, testInfo });\n\n // Capture the fully-authenticated state for reuse by later tests in\n // this worker. Errors here would mask the actual login success, so we\n // never let snapshot capture throw — at worst we'll re-hit Okta next\n // time, which is the pre-cache behaviour.\n try {\n const snapshot = await page.context().storageState();\n _loginCache.set(cacheKey, snapshot);\n } catch {\n /* Snapshot capture is best-effort; failing here is non-fatal. */\n }\n\n await waitForMainMenuReady(page, testInfo);\n };\n\n await provide(loginFn);\n },\n});\n\n/**\n * Performs the slow path: wait for the branch host to serve `/env`, hit Okta's\n * `/api/v1/authn`, redeem the `sessionToken` via the manager-classic redirect,\n * and wait for the host layout to render. Factored out so the cache hit/miss\n * branches read clearly.\n */\nconst performOktaLogin = async ({\n page,\n baseURL,\n credentials,\n testInfo,\n}: {\n page: Page;\n baseURL: string | undefined;\n credentials: AuthCredentials;\n testInfo?: TestTimeoutInfo;\n}): Promise<void> => {\n const context = await request.newContext({ baseURL });\n try {\n // Pin the response type: Playwright's `get<T = any>` would otherwise be inferred as `APIResponse<unknown>`.\n const envResponse = await waitForEnvReady<APIResponse>(context, baseURL, testInfo);\n const env = await envResponse.json();\n const domainValue: unknown = env?.auth?.url;\n if (typeof domainValue !== \"string\" || domainValue === \"\") {\n throw new Error(`No domain found from servers /env found env: ${JSON.stringify(env)}`);\n }\n const domain = domainValue;\n\n const authResponse = await context.post(`${domain}/api/v1/authn`, {\n data: {\n username: credentials.username,\n password: credentials.password,\n options: {\n warnBeforePasswordExpired: true,\n multiOptionalFactorEnroll: false,\n },\n },\n headers: { \"Content-Type\": \"application/json\" },\n timeout: 20000,\n });\n\n if (!authResponse.ok()) {\n const body = await authResponse.json().catch(() => ({}));\n throw new Error(`Could not get a session token for user: ${credentials.username}, ${JSON.stringify(body)}`);\n }\n\n const { sessionToken } = await authResponse.json();\n await page.goto(`/auth/manager-classic#session_token=${sessionToken}&userProfile=true`);\n await page.waitForURL(url => !url.href.includes(\"manager-classic\"), { timeout: 20000 });\n await page.locator(\"#host-layout-content\").waitFor({ state: \"visible\", timeout: 15000 });\n } finally {\n await context.dispose();\n }\n};\n\nexport { expect } from \"@playwright/test\";\n"]}
|
|
1
|
+
{"version":3,"file":"login.fixture.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/fixtures/login.fixture.ts"],"names":[],"mappings":"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","sourcesContent":["import { errors, request, test as base } from \"@playwright/test\";\nimport { HostNeverBecameReadyError, isFeatureBranchHost } from \"./hostReadiness\";\nimport { hydrateStorageState, type CachedStorageState } from \"./hydrateStorageState\";\nimport { buildLoginCacheKey } from \"./loginCacheKey\";\nimport { resolveCredentials, type AuthCredentials, type ResolveCredentialsOptions } from \"./resolveCredentials\";\n\nexport type { AuthCredentials };\n\nexport type LoginOptions = ResolveCredentialsOptions;\n\n/** Matches HostLayout's `hideMenuForUrls` — routes where MainMenu is not rendered. */\nexport const HIDE_MENU_URL_FRAGMENTS = [\"/marketplace\", \"/auth/\", \"/terms\"] as const;\n\nexport const MAIN_MENU_READY_TIMEOUT_MS = 180_000;\n\n/**\n * Default budget for the first login on a worker to wait for the branch host\n * to answer `GET /env` with 200. The deploy job only waits for ArgoCD health,\n * which does not cover the Emissary/Envoy route for a freshly created feature\n * branch, so the host can return 404 for several minutes after the deploy job\n * is green. Override per run with {@link ENV_READY_TIMEOUT_ENV_VAR}.\n */\nexport const ENV_READY_TIMEOUT_MS = 300_000;\n\n/**\n * Environment variable that overrides {@link ENV_READY_TIMEOUT_MS} (in\n * milliseconds). Useful for local runs against a fixed host where a long wait\n * only delays the inevitable failure.\n */\nexport const ENV_READY_TIMEOUT_ENV_VAR = \"NX_E2E_ENV_READY_TIMEOUT_MS\";\n\nexport const ENV_READY_POLL_INTERVAL_MS = 5_000;\n\n/** Floor for a single `/env` request so the last attempt before the deadline is still meaningful. */\nconst ENV_READY_MIN_REQUEST_TIMEOUT_MS = 1_000;\n\n/** Fixed timeout for Okta's `/api/v1/authn` call; it is not retried against the login budget. */\nconst OKTA_AUTH_TIMEOUT_MS = 20_000;\n\n/** Single wait for the manager-classic session handoff to leave that page. */\nconst SESSION_HANDOFF_TIMEOUT_MS = 20_000;\n\n/** Gap between reloads of a manager-classic session document that did not load. */\nexport const SESSION_DOCUMENT_RETRY_DELAY_MS = 1_000;\n\nconst resolveEnvReadyTimeoutMs = (): number => {\n const raw = process.env[ENV_READY_TIMEOUT_ENV_VAR];\n if (raw === undefined || raw.trim() === \"\") {\n return ENV_READY_TIMEOUT_MS;\n }\n const parsed = Number(raw);\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : ENV_READY_TIMEOUT_MS;\n};\n\nexport const MAIN_MENU_READY_SELECTOR =\n '[data-testid=\"mainMenu\"]:not(:has([data-testid=\"mainMenuItems-skeleton\"], [data-testid=\"menu-items-skeleton\"]))';\n\ntype MainMenuReadinessInput = {\n readonly selector: string;\n readonly fragments: ReadonlyArray<string>;\n};\n\ntype MainMenuWaitPage = {\n waitForFunction: (\n pageFunction: (input: MainMenuReadinessInput) => boolean,\n input: MainMenuReadinessInput,\n options?: { timeout?: number }\n ) => Promise<unknown>;\n};\n\ntype TestTimeoutInfo = {\n timeout: number;\n setTimeout: (timeout: number) => void;\n};\n\n/**\n * Grows the running test's timeout by `budgetMs` so a readiness wait cannot\n * eat into the test's own budget. A timeout of 0 means \"unlimited\" in\n * Playwright and is left untouched.\n */\nexport const extendTestTimeout = (testInfo: TestTimeoutInfo | undefined, budgetMs: number): void => {\n if (testInfo !== undefined && testInfo.timeout !== 0) {\n testInfo.setTimeout(testInfo.timeout + budgetMs);\n }\n};\n\n/**\n * Gives the feature-branch session handoff its own readiness budget.\n *\n * Root cause (GLU-2631): after `/env` answers, a feature-branch host can still\n * serve a failed session document or stay on `manager-classic` for a while.\n * Those waits share {@link MAIN_MENU_READY_TIMEOUT_MS} after `/env` is ready,\n * so the test timeout is extended by that budget before the slow path. The\n * `/env` wait extends it by its own budget. A host that is not a feature\n * branch is unchanged.\n */\nexport const reserveFeatureBranchLoginBudget = (testInfo?: TestTimeoutInfo): void => {\n if (!isFeatureBranchHost()) {\n return;\n }\n extendTestTimeout(testInfo, MAIN_MENU_READY_TIMEOUT_MS);\n};\n\n/** Returns whether the main menu is ready or intentionally hidden for the current route. */\nexport const isMainMenuReady = ({ selector, fragments }: MainMenuReadinessInput): boolean => {\n if (document.querySelector(selector) !== null) {\n return true;\n }\n return fragments.some(fragment => window.location.pathname.includes(fragment));\n};\n\n/**\n * Waits until Manager's main menu is rendered without a loading skeleton, or\n * until the browser is on a HostLayout hide-menu route (marketplace / auth /\n * terms) where the menu is intentionally absent.\n */\nexport const waitForMainMenuReady = async (page: MainMenuWaitPage, testInfo?: TestTimeoutInfo): Promise<void> => {\n extendTestTimeout(testInfo, MAIN_MENU_READY_TIMEOUT_MS);\n\n await page.waitForFunction(\n isMainMenuReady,\n { selector: MAIN_MENU_READY_SELECTOR, fragments: HIDE_MENU_URL_FRAGMENTS },\n { timeout: MAIN_MENU_READY_TIMEOUT_MS }\n );\n};\n\ntype EnvReadyResponse = {\n status: () => number;\n};\n\ntype EnvReadyRequestContext<TResponse extends EnvReadyResponse> = {\n get: (url: string, options?: { timeout?: number }) => Promise<TResponse>;\n};\n\nconst sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));\n\nconst describeError = (error: unknown): string => (error instanceof Error ? error.message : String(error));\n\n/**\n * Nothing is listening on the host (local dev server not started, typo in\n * `baseURL`). Waiting will not change that, so these fail immediately instead\n * of burning the readiness budget. This relies on feature-branch hosts living\n * under a wildcard DNS record (`*.dev.manager.trackunit.com`), so a brand-new\n * branch resolves immediately and `ENOTFOUND` is never a transient state.\n */\nconst isHostUnreachableError = (error: unknown): boolean => /\\b(ECONNREFUSED|ENOTFOUND)\\b/.test(describeError(error));\n\n/** Thrown by {@link waitForEnvReady} when `/env` did not answer 200 inside its budget. */\nexport class EnvReadyTimeoutError extends Error {\n /** @param message the budget, host and last status/error, as reported by the poller */\n public constructor(message: string) {\n super(message);\n this.name = \"EnvReadyTimeoutError\";\n }\n}\n\n/**\n * Polls `GET /env` on the branch host until it returns 200 and resolves with\n * that response, untouched. Non-200 responses (404 while Envoy has no route\n * yet, 503 while pods start) and timed-out requests are retried every\n * {@link ENV_READY_POLL_INTERVAL_MS} until the budget ({@link ENV_READY_TIMEOUT_MS},\n * or {@link ENV_READY_TIMEOUT_ENV_VAR}) is spent. The resulting error names the\n * host and the last status/error so the failure is attributable to the\n * environment rather than to the test. Connection-level failures fail fast.\n */\nexport const waitForEnvReady = async <TResponse extends EnvReadyResponse>(\n context: EnvReadyRequestContext<TResponse>,\n baseURL: string | undefined,\n testInfo?: TestTimeoutInfo\n): Promise<TResponse> => {\n const timeoutMs = resolveEnvReadyTimeoutMs();\n extendTestTimeout(testInfo, timeoutMs);\n\n const envUrl = `${baseURL ?? \"\"}/env`;\n const deadline = Date.now() + timeoutMs;\n let lastOutcome = \"no response\";\n let attempted = false;\n\n for (;;) {\n const remainingMs = deadline - Date.now();\n // Always make one attempt; after that stop as soon as the budget is spent\n // so the error reports the last real outcome rather than a rushed retry.\n if (attempted && remainingMs <= 0) {\n throw new EnvReadyTimeoutError(\n `Timed out after ${timeoutMs} ms waiting for ${envUrl} to return 200 (${lastOutcome})`\n );\n }\n\n // Cap each request so a hanging connection cannot run far past the deadline.\n const requestTimeout = Math.min(\n ENV_READY_POLL_INTERVAL_MS,\n Math.max(remainingMs, ENV_READY_MIN_REQUEST_TIMEOUT_MS)\n );\n attempted = true;\n try {\n const response = await context.get(\"/env\", { timeout: requestTimeout });\n const status = response.status();\n if (status === 200) {\n return response;\n }\n lastOutcome = `last status: ${status}`;\n } catch (error) {\n if (isHostUnreachableError(error)) {\n throw new Error(`Could not reach ${envUrl}, is the server running? (${describeError(error)})`);\n }\n lastOutcome = `last error: ${describeError(error)}`;\n }\n\n await sleep(Math.max(0, Math.min(ENV_READY_POLL_INTERVAL_MS, deadline - Date.now())));\n }\n};\n\nexport interface TrackunitBaseFixtures {\n login: (options?: LoginOptions) => Promise<void>;\n}\n\n/**\n * Worker-scoped, in-memory cache of authenticated `storageState` snapshots,\n * keyed by `(baseURL, username)`. Implementation detail of the `login`\n * fixture — exposed on the fixtures object so Playwright can manage its\n * lifecycle, but prefixed with `_` to signal \"don't touch from tests\".\n */\nexport interface LoginCacheFixtures {\n _loginCache: Map<string, CachedStorageState>;\n}\n\n/**\n * Each worker is its own Node process, so a `Map` is sufficient — no\n * cross-worker sharing concerns, no disk I/O, no cleanup needed. The cache\n * dies with the worker, which is the desired behaviour: a fresh `nx run`\n * always starts with empty caches.\n */\nexport const test = base.extend<TrackunitBaseFixtures, LoginCacheFixtures>({\n _loginCache: [\n async ({}, provide) => {\n await provide(new Map<string, CachedStorageState>());\n },\n { scope: \"worker\" },\n ],\n\n login: async ({ page, baseURL, _loginCache }, provide, testInfo) => {\n const loginFn = async (options?: LoginOptions) => {\n const credentials = await resolveCredentials(options);\n const cacheKey = buildLoginCacheKey(baseURL, credentials);\n const cached = _loginCache.get(cacheKey);\n\n if (cached !== undefined) {\n let cachedSessionRestored = false;\n try {\n await hydrateStorageState(page.context(), cached);\n await page.goto(\"/\");\n await page.locator(\"#host-layout-content\").waitFor({ state: \"visible\", timeout: 15000 });\n cachedSessionRestored = true;\n } catch {\n _loginCache.delete(cacheKey);\n }\n\n if (cachedSessionRestored) {\n await waitForMainMenuReady(page, testInfo);\n return;\n }\n }\n\n reserveFeatureBranchLoginBudget(testInfo);\n await performOktaLogin({ page, baseURL, credentials, testInfo });\n\n // Capture the fully-authenticated state for reuse by later tests in\n // this worker. Errors here would mask the actual login success, so we\n // never let snapshot capture throw — at worst we'll re-hit Okta next\n // time, which is the pre-cache behaviour.\n try {\n const snapshot = await page.context().storageState();\n _loginCache.set(cacheKey, snapshot);\n } catch {\n /* Snapshot capture is best-effort; failing here is non-fatal. */\n }\n\n await waitForMainMenuReady(page, testInfo);\n };\n\n await provide(loginFn);\n },\n});\n\ntype LoginJsonBody = { auth?: { url?: unknown }; sessionToken?: string };\n\ntype LoginEnvResponse = EnvReadyResponse & { json: () => Promise<LoginJsonBody> };\n\ntype LoginRequestContext = {\n get: (url: string, options?: { timeout?: number }) => Promise<LoginEnvResponse>;\n post: (\n url: string,\n options: {\n data: {\n username: string;\n password: string;\n options: {\n warnBeforePasswordExpired: boolean;\n multiOptionalFactorEnroll: boolean;\n };\n };\n headers: { \"Content-Type\": \"application/json\" };\n timeout: number;\n }\n ) => Promise<{ ok: () => boolean; json: () => Promise<LoginJsonBody> }>;\n dispose: () => Promise<void>;\n};\n\ntype SessionDocument = { ok: () => boolean };\n\ntype OktaLoginPage = {\n goto: (url: string, options?: { timeout?: number }) => Promise<SessionDocument | null | undefined>;\n waitForURL: (url: (url: URL) => boolean, options?: { timeout?: number }) => Promise<unknown>;\n locator: (selector: string) => {\n waitFor: (options: { state: \"visible\"; timeout: number }) => Promise<unknown>;\n };\n};\n\nconst openDefaultRequestContext = async (baseURL: string | undefined): Promise<LoginRequestContext> => {\n const context = await request.newContext({ baseURL });\n return {\n // Playwright's `get<T>` types the JSON body; without the pin it is `any`.\n get: (url, options) => context.get<LoginJsonBody>(url, options),\n post: (url, options) => context.post(url, options),\n dispose: () => context.dispose(),\n };\n};\n\n/**\n * Reads `GET /env` through {@link waitForEnvReady}.\n *\n * On a feature-branch host, spending that budget is the host never becoming\n * ready, so the timeout is rethrown as {@link HostNeverBecameReadyError} with\n * the original error as `cause`. An unreachable host still fails fast, and a\n * host that is not a feature branch keeps the `/env` timeout message.\n */\nconst readHostEnv = async (\n context: LoginRequestContext,\n baseURL: string | undefined,\n testInfo: TestTimeoutInfo | undefined\n): Promise<LoginJsonBody> => {\n try {\n const envResponse = await waitForEnvReady(context, baseURL, testInfo);\n return await envResponse.json();\n } catch (error) {\n if (isFeatureBranchHost() && error instanceof EnvReadyTimeoutError) {\n throw new HostNeverBecameReadyError(error);\n }\n throw error;\n }\n};\n\nconst leftManagerClassic = (url: URL): boolean => !url.href.includes(\"manager-classic\");\n\nconst isPlaywrightTimeout = (error: unknown): boolean => error instanceof errors.TimeoutError;\n\n/**\n * Waits until the manager-classic session handoff leaves that page.\n *\n * Root cause (GLU-2631): feature-branch login waited once for 20s. While that\n * host is still booting, the URL stays on manager-classic and `waitForURL`\n * throws a navigation timeout, so the rest of the readiness budget was never\n * used. When the feature-branch gate is on, retry until the URL leaves that\n * page or the deadline ends, then throw {@link HostNeverBecameReadyError}.\n * Each attempt waits up to 20s, capped by the time left, so the loop does not\n * busy-spin. An unset or empty `NX_FEATURE_BRANCH_BASE_URL` keeps the single\n * 20s wait.\n */\nconst waitForSessionHandoff = async (\n page: OktaLoginPage,\n now: () => number,\n deadline: number | undefined\n): Promise<void> => {\n if (deadline === undefined) {\n await page.waitForURL(leftManagerClassic, { timeout: SESSION_HANDOFF_TIMEOUT_MS });\n return;\n }\n\n let current = now();\n let lastTimeout: unknown;\n while (current < deadline) {\n const timeout = Math.min(SESSION_HANDOFF_TIMEOUT_MS, deadline - current);\n try {\n await page.waitForURL(leftManagerClassic, { timeout });\n return;\n } catch (error) {\n if (!isPlaywrightTimeout(error)) {\n throw error;\n }\n lastTimeout = error;\n }\n current = now();\n }\n\n throw new HostNeverBecameReadyError(lastTimeout);\n};\n\nconst documentLoaded = (response: SessionDocument | null | undefined): boolean => {\n if (response === null || response === undefined) {\n return false;\n }\n return response.ok();\n};\n\n/**\n * Opens the manager-classic session URL.\n *\n * A feature-branch host that is still booting can answer `page.goto` with a\n * 502 or an empty document. The URL stays on that page, so waiting for it to\n * change never sees a later healthy load. Retry the navigation until the\n * document loads or the deadline ends. A navigation that uses up the time\n * left throws {@link HostNeverBecameReadyError}. The same session URL is\n * reused: a failed document does not redeem the hash token. With the gate off,\n * this is one `page.goto` and the browser's own navigation timeout.\n */\nconst gotoSessionDocument = async (\n page: OktaLoginPage,\n url: string,\n now: () => number,\n deadline: number | undefined,\n pause: (ms: number) => Promise<void>\n): Promise<void> => {\n if (deadline === undefined) {\n await page.goto(url);\n return;\n }\n\n while (now() < deadline) {\n const remaining = deadline - now();\n if (remaining <= 0) {\n break;\n }\n try {\n const response = await page.goto(url, { timeout: remaining });\n if (documentLoaded(response)) {\n return;\n }\n } catch (error) {\n if (!isPlaywrightTimeout(error)) {\n throw error;\n }\n throw new HostNeverBecameReadyError(error);\n }\n const remainingAfter = deadline - now();\n if (remainingAfter <= 0) {\n break;\n }\n await pause(Math.min(SESSION_DOCUMENT_RETRY_DELAY_MS, remainingAfter));\n }\n\n throw new HostNeverBecameReadyError();\n};\n\n/**\n * Performs the slow path: wait for the branch host to serve `/env`, hit Okta's\n * `/api/v1/authn`, redeem the `sessionToken` via the manager-classic redirect,\n * and wait for the host layout to render. Factored out so the cache hit/miss\n * branches read clearly.\n *\n * `createRequestContext`, `now`, and `sleep` are injectable so a unit test can\n * drive the session handoff without a browser or a real three-minute wait.\n */\nexport const performOktaLogin = async ({\n page,\n baseURL,\n credentials,\n testInfo,\n createRequestContext = openDefaultRequestContext,\n now = Date.now,\n sleep: pause = sleep,\n}: {\n page: OktaLoginPage;\n baseURL: string | undefined;\n credentials: AuthCredentials;\n testInfo?: TestTimeoutInfo;\n createRequestContext?: (baseURL: string | undefined) => Promise<LoginRequestContext>;\n /** Injectable clock. Production uses `Date.now`. The handoff budget is {@link MAIN_MENU_READY_TIMEOUT_MS}. */\n now?: () => number;\n /** Injectable pause between session document reloads. Production waits on the event loop. */\n sleep?: (ms: number) => Promise<void>;\n}): Promise<void> => {\n const context = await createRequestContext(baseURL);\n try {\n const env = await readHostEnv(context, baseURL, testInfo);\n const domainValue: unknown = env.auth?.url;\n if (typeof domainValue !== \"string\" || domainValue === \"\") {\n throw new Error(`No domain found from servers /env found env: ${JSON.stringify(env)}`);\n }\n const domain = domainValue;\n\n // Started once `/env` is ready. The Okta post, the session document and the\n // handoff all run inside this window; only the last two are retried against\n // it, the post keeps its own fixed timeout (GLU-2631).\n const readyDeadline = isFeatureBranchHost() ? now() + MAIN_MENU_READY_TIMEOUT_MS : undefined;\n\n const authResponse = await context.post(`${domain}/api/v1/authn`, {\n data: {\n username: credentials.username,\n password: credentials.password,\n options: {\n warnBeforePasswordExpired: true,\n multiOptionalFactorEnroll: false,\n },\n },\n headers: { \"Content-Type\": \"application/json\" },\n timeout: OKTA_AUTH_TIMEOUT_MS,\n });\n\n if (!authResponse.ok()) {\n const body = await authResponse.json().catch(() => ({}));\n throw new Error(`Could not get a session token for user: ${credentials.username}, ${JSON.stringify(body)}`);\n }\n\n const { sessionToken } = await authResponse.json();\n await gotoSessionDocument(\n page,\n `/auth/manager-classic#session_token=${sessionToken}&userProfile=true`,\n now,\n readyDeadline,\n pause\n );\n await waitForSessionHandoff(page, now, readyDeadline);\n await page.locator(\"#host-layout-content\").waitFor({ state: \"visible\", timeout: 15000 });\n } finally {\n await context.dispose();\n }\n};\n\nexport { expect } from \"@playwright/test\";\n"]}
|