@trackunit/iris-app-playwright 0.2.115 → 0.2.116-alpha-abe4978141a.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/migrations/entry.js.map +1 -0
- package/package.json +1 -1
- package/src/credentials/guards.js.map +1 -0
- package/src/credentials/keys.js.map +1 -0
- package/src/credentials/resolveE2EPassword.js.map +1 -0
- package/src/fixtures/acceptTerms.d.ts +74 -0
- package/src/fixtures/acceptTerms.js +209 -0
- package/src/fixtures/acceptTerms.js.map +1 -0
- package/src/fixtures/baseTest.js.map +1 -0
- package/src/fixtures/describe.js.map +1 -0
- package/src/fixtures/featureFlags.fixture.js.map +1 -0
- package/src/fixtures/harRecording.fixture.js.map +1 -0
- package/src/fixtures/hostReadiness.js.map +1 -0
- package/src/fixtures/hydrateStorageState.js.map +1 -0
- package/src/fixtures/irisApp.fixture.js.map +1 -0
- package/src/fixtures/localDevMode.fixture.js.map +1 -0
- package/src/fixtures/login.fixture.d.ts +68 -7
- package/src/fixtures/login.fixture.js +263 -42
- package/src/fixtures/login.fixture.js.map +1 -0
- package/src/fixtures/loginCacheKey.js.map +1 -0
- package/src/fixtures/resolveCredentials.js.map +1 -0
- package/src/fixtures/withTimeout.js.map +1 -0
- package/src/generators/playwright-configuration/generator.js.map +1 -0
- package/src/index.js.map +1 -0
- package/src/plugins/createLogFile.js.map +1 -0
- package/src/plugins/defaultPlaywrightConfig.js.map +1 -0
- package/src/plugins/harRecordingPath.js.map +1 -0
- package/src/plugins/logsReporter.js.map +1 -0
- package/src/plugins/nxPreset.js.map +1 -0
- package/src/plugins/redactHar.js.map +1 -0
- package/src/plugins/redactPatterns.js.map +1 -0
- package/src/plugins/redactSensitive.js.map +1 -0
- package/src/plugins/redactTrace.js.map +1 -0
- package/src/plugins/scanArtifactsForSecrets.js.map +1 -0
- package/src/plugins/setupPlugins.js.map +1 -0
- package/src/plugins/writeFileWithPrettier.js.map +1 -0
- package/src/support.js.map +1 -0
- package/src/utils/Codeowner.js.map +1 -0
- package/src/utils/fileNameBuilder.js.map +1 -0
- package/src/utils/fileUpdater.js.map +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry.js","sourceRoot":"","sources":["../../../../../libs/iris-app-sdk/iris-app-playwright/migrations/entry.ts"],"names":[],"mappings":"","sourcesContent":["export {};\n"]}
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Route Manager's `TermsGate` redirects to while the signed-in user has not accepted the terms. */
|
|
2
|
+
export declare const TERMS_ROUTE = "/terms";
|
|
3
|
+
export declare const TERMS_STATUS_OPERATION = "TermsAndConditionsStatus";
|
|
4
|
+
/** Upper bound on documents to click through, so an unexpected flow fails instead of looping forever. */
|
|
5
|
+
export declare const MAX_TERMS_DOCUMENTS = 20;
|
|
6
|
+
/** Reads `data.termsAndConditionsStatus.hasAccepted`, or `undefined` when the body has no usable answer. */
|
|
7
|
+
export declare const readHasAccepted: (body: unknown) => boolean | undefined;
|
|
8
|
+
/** Whether `url` is the page `TermsGate` redirects to, not the review-terms preferences page. */
|
|
9
|
+
export declare const isTermsRoute: (url: string) => boolean;
|
|
10
|
+
type TermsStatusResponse = {
|
|
11
|
+
ok: () => boolean;
|
|
12
|
+
json: () => Promise<unknown>;
|
|
13
|
+
};
|
|
14
|
+
export type TermsStatusRequest = {
|
|
15
|
+
url: () => string;
|
|
16
|
+
response: () => Promise<TermsStatusResponse | null | undefined>;
|
|
17
|
+
};
|
|
18
|
+
type TermsStatusListener = (request: TermsStatusRequest) => void;
|
|
19
|
+
export type TermsStatusPage = {
|
|
20
|
+
on: (event: "request", listener: TermsStatusListener) => void;
|
|
21
|
+
off: (event: "request", listener: TermsStatusListener) => void;
|
|
22
|
+
};
|
|
23
|
+
export type TermsStatusWatcher = {
|
|
24
|
+
/** The latest `hasAccepted` the app received, waiting for a request that is still in flight. */
|
|
25
|
+
hasAccepted: () => Promise<boolean | undefined>;
|
|
26
|
+
/** True once a `TermsAndConditionsStatus` request has been observed. */
|
|
27
|
+
seenRequest: () => boolean;
|
|
28
|
+
stop: () => void;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Records the app's own `TermsAndConditionsStatus` lookup. `TermsGate` sits inside `HostLayout`, so the
|
|
32
|
+
* main menu can finish loading before the gate redirects to `/terms`; the status response is the only
|
|
33
|
+
* signal that the redirect is coming. Must be started before the navigation that signs the user in.
|
|
34
|
+
*/
|
|
35
|
+
export declare const watchTermsStatus: (page: TermsStatusPage) => TermsStatusWatcher;
|
|
36
|
+
/**
|
|
37
|
+
* Reads the latest terms status without waiting past `deadline`.
|
|
38
|
+
*
|
|
39
|
+
* No request yet resolves as `undefined` immediately — `TermsGate` is lazy, so the
|
|
40
|
+
* lookup often starts after the host layout is already visible. An in-flight response
|
|
41
|
+
* that never arrives throws {@link HostNeverBecameReadyError} when `deadline` is
|
|
42
|
+
* reached, instead of sitting outside the login budget.
|
|
43
|
+
*/
|
|
44
|
+
export declare const readTermsStatusWithinDeadline: (watcher: TermsStatusWatcher, now: () => number, pause: (ms: number) => Promise<void>, deadline: number) => Promise<boolean | undefined>;
|
|
45
|
+
export type TermsButton = {
|
|
46
|
+
isVisible: () => Promise<boolean>;
|
|
47
|
+
click: () => Promise<void>;
|
|
48
|
+
};
|
|
49
|
+
export type TermsPromptPage = {
|
|
50
|
+
url: () => string;
|
|
51
|
+
waitForURL: (predicate: (url: URL) => boolean, options?: {
|
|
52
|
+
timeout?: number;
|
|
53
|
+
}) => Promise<unknown>;
|
|
54
|
+
getByRole: (role: "button", options: {
|
|
55
|
+
name: string;
|
|
56
|
+
exact?: boolean;
|
|
57
|
+
}) => TermsButton;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Clicks through Manager's terms flow when the signed-in user is being asked to accept.
|
|
61
|
+
*
|
|
62
|
+
* Root cause (GLU-2632): the feature-branch prompt is not a finished login, and waiting
|
|
63
|
+
* never clicks it. Success is leaving `/terms` — the app reloads to `/` and discards the
|
|
64
|
+
* mutation body. Staying on `/terms` until `deadline` is {@link HostNeverBecameReadyError},
|
|
65
|
+
* not a generic "still on /terms" failure.
|
|
66
|
+
*/
|
|
67
|
+
export declare const acceptTermsWithinBudget: ({ page, hasAccepted, now, sleep: pause, deadline, }: {
|
|
68
|
+
page: TermsPromptPage;
|
|
69
|
+
hasAccepted: boolean | undefined;
|
|
70
|
+
now: () => number;
|
|
71
|
+
sleep: (ms: number) => Promise<void>;
|
|
72
|
+
deadline: number;
|
|
73
|
+
}) => Promise<void>;
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.acceptTermsWithinBudget = exports.readTermsStatusWithinDeadline = exports.watchTermsStatus = exports.isTermsRoute = exports.readHasAccepted = exports.MAX_TERMS_DOCUMENTS = exports.TERMS_STATUS_OPERATION = exports.TERMS_ROUTE = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
const hostReadiness_1 = require("./hostReadiness");
|
|
6
|
+
/** Route Manager's `TermsGate` redirects to while the signed-in user has not accepted the terms. */
|
|
7
|
+
exports.TERMS_ROUTE = "/terms";
|
|
8
|
+
exports.TERMS_STATUS_OPERATION = "TermsAndConditionsStatus";
|
|
9
|
+
/** Upper bound on documents to click through, so an unexpected flow fails instead of looping forever. */
|
|
10
|
+
exports.MAX_TERMS_DOCUMENTS = 20;
|
|
11
|
+
const TERMS_REDIRECT_TIMEOUT_MS = 20000;
|
|
12
|
+
/** Gap between checks while the terms buttons are not on screen yet. */
|
|
13
|
+
const TERMS_POLL_INTERVAL_MS = 1000;
|
|
14
|
+
const ACCEPT_BUTTON_NAME = "Accept";
|
|
15
|
+
const START_BUTTON_NAME = "Start using Manager";
|
|
16
|
+
const isPlaywrightTimeout = (error) => error instanceof test_1.errors.TimeoutError;
|
|
17
|
+
/** Reads `data.termsAndConditionsStatus.hasAccepted`, or `undefined` when the body has no usable answer. */
|
|
18
|
+
const readHasAccepted = (body) => {
|
|
19
|
+
if (typeof body !== "object" || body === null || !("data" in body)) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const data = body.data;
|
|
23
|
+
if (typeof data !== "object" || data === null || !("termsAndConditionsStatus" in data)) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const status = data.termsAndConditionsStatus;
|
|
27
|
+
if (typeof status !== "object" || status === null || !("hasAccepted" in status)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return typeof status.hasAccepted === "boolean" ? status.hasAccepted : undefined;
|
|
31
|
+
};
|
|
32
|
+
exports.readHasAccepted = readHasAccepted;
|
|
33
|
+
/** Whether `url` is the page `TermsGate` redirects to, not the review-terms preferences page. */
|
|
34
|
+
const isTermsRoute = (url) => {
|
|
35
|
+
try {
|
|
36
|
+
return new URL(url).pathname === exports.TERMS_ROUTE;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
exports.isTermsRoute = isTermsRoute;
|
|
43
|
+
/**
|
|
44
|
+
* Records the app's own `TermsAndConditionsStatus` lookup. `TermsGate` sits inside `HostLayout`, so the
|
|
45
|
+
* main menu can finish loading before the gate redirects to `/terms`; the status response is the only
|
|
46
|
+
* signal that the redirect is coming. Must be started before the navigation that signs the user in.
|
|
47
|
+
*/
|
|
48
|
+
const watchTermsStatus = (page) => {
|
|
49
|
+
let seen = false;
|
|
50
|
+
let latest;
|
|
51
|
+
const onRequest = request => {
|
|
52
|
+
if (!request.url().includes(exports.TERMS_STATUS_OPERATION)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
seen = true;
|
|
56
|
+
// The body can be discarded by a navigation before it is read; a missed read means "unknown".
|
|
57
|
+
latest = request
|
|
58
|
+
.response()
|
|
59
|
+
.then(async (response) => (response?.ok() === true ? (0, exports.readHasAccepted)(await response.json()) : undefined))
|
|
60
|
+
.catch(() => undefined);
|
|
61
|
+
};
|
|
62
|
+
page.on("request", onRequest);
|
|
63
|
+
return {
|
|
64
|
+
hasAccepted: async () => (latest === undefined ? undefined : await latest),
|
|
65
|
+
seenRequest: () => seen,
|
|
66
|
+
stop: () => {
|
|
67
|
+
page.off("request", onRequest);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
exports.watchTermsStatus = watchTermsStatus;
|
|
72
|
+
/**
|
|
73
|
+
* Reads the latest terms status without waiting past `deadline`.
|
|
74
|
+
*
|
|
75
|
+
* No request yet resolves as `undefined` immediately — `TermsGate` is lazy, so the
|
|
76
|
+
* lookup often starts after the host layout is already visible. An in-flight response
|
|
77
|
+
* that never arrives throws {@link HostNeverBecameReadyError} when `deadline` is
|
|
78
|
+
* reached, instead of sitting outside the login budget.
|
|
79
|
+
*/
|
|
80
|
+
const readTermsStatusWithinDeadline = async (watcher, now, pause, deadline) => {
|
|
81
|
+
if (!watcher.seenRequest()) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const pending = watcher.hasAccepted();
|
|
85
|
+
const settled = pending.then(value => ({ kind: "settled", value }));
|
|
86
|
+
const peeked = await Promise.race([
|
|
87
|
+
settled,
|
|
88
|
+
Promise.resolve().then(() => ({ kind: "pending" })),
|
|
89
|
+
]);
|
|
90
|
+
if (peeked.kind === "settled") {
|
|
91
|
+
return peeked.value;
|
|
92
|
+
}
|
|
93
|
+
while (now() < deadline) {
|
|
94
|
+
const remaining = deadline - now();
|
|
95
|
+
if (remaining <= 0) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
const started = now();
|
|
99
|
+
const step = await Promise.race([
|
|
100
|
+
settled,
|
|
101
|
+
pause(Math.min(TERMS_POLL_INTERVAL_MS, remaining)).then(() => ({ kind: "paused" })),
|
|
102
|
+
]);
|
|
103
|
+
if (step.kind === "settled") {
|
|
104
|
+
return step.value;
|
|
105
|
+
}
|
|
106
|
+
// A pause that does not consume the budget must not drop a body that is already resolving.
|
|
107
|
+
if (now() === started) {
|
|
108
|
+
const late = await Promise.race([
|
|
109
|
+
settled,
|
|
110
|
+
Promise.resolve()
|
|
111
|
+
.then(() => Promise.resolve())
|
|
112
|
+
.then(() => Promise.resolve())
|
|
113
|
+
.then(() => ({ kind: "pending" })),
|
|
114
|
+
]);
|
|
115
|
+
if (late.kind === "settled") {
|
|
116
|
+
return late.value;
|
|
117
|
+
}
|
|
118
|
+
throw new hostReadiness_1.HostNeverBecameReadyError();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
throw new hostReadiness_1.HostNeverBecameReadyError();
|
|
122
|
+
};
|
|
123
|
+
exports.readTermsStatusWithinDeadline = readTermsStatusWithinDeadline;
|
|
124
|
+
const pathnameOf = (url) => {
|
|
125
|
+
try {
|
|
126
|
+
return new URL(url).pathname;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const waitForPathname = async (page, matches, now, pause, deadline) => {
|
|
133
|
+
if (matches(pathnameOf(page.url()) ?? "")) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
let lastTimeout;
|
|
137
|
+
while (now() < deadline) {
|
|
138
|
+
const remaining = deadline - now();
|
|
139
|
+
const timeout = Math.min(TERMS_REDIRECT_TIMEOUT_MS, remaining);
|
|
140
|
+
if (timeout <= 0) {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
const started = now();
|
|
144
|
+
try {
|
|
145
|
+
await page.waitForURL(url => matches(url.pathname), { timeout });
|
|
146
|
+
if (matches(pathnameOf(page.url()) ?? "")) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (!isPlaywrightTimeout(error)) {
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
lastTimeout = error;
|
|
155
|
+
}
|
|
156
|
+
// A Playwright timeout already consumed real time. A fake that rejects without
|
|
157
|
+
// moving the injected clock still has to yield or this loop never reaches the deadline.
|
|
158
|
+
if (now() === started) {
|
|
159
|
+
const remainingAfter = deadline - now();
|
|
160
|
+
if (remainingAfter <= 0) {
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
await pause(Math.min(TERMS_POLL_INTERVAL_MS, remainingAfter));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
throw new hostReadiness_1.HostNeverBecameReadyError(lastTimeout);
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Clicks through Manager's terms flow when the signed-in user is being asked to accept.
|
|
170
|
+
*
|
|
171
|
+
* Root cause (GLU-2632): the feature-branch prompt is not a finished login, and waiting
|
|
172
|
+
* never clicks it. Success is leaving `/terms` — the app reloads to `/` and discards the
|
|
173
|
+
* mutation body. Staying on `/terms` until `deadline` is {@link HostNeverBecameReadyError},
|
|
174
|
+
* not a generic "still on /terms" failure.
|
|
175
|
+
*/
|
|
176
|
+
const acceptTermsWithinBudget = async ({ page, hasAccepted, now, sleep: pause, deadline, }) => {
|
|
177
|
+
if (hasAccepted !== false && !(0, exports.isTermsRoute)(page.url())) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
await waitForPathname(page, pathname => pathname === exports.TERMS_ROUTE, now, pause, deadline);
|
|
181
|
+
let documents = 0;
|
|
182
|
+
for (;;) {
|
|
183
|
+
if (now() >= deadline) {
|
|
184
|
+
throw new hostReadiness_1.HostNeverBecameReadyError();
|
|
185
|
+
}
|
|
186
|
+
const startUsingManager = page.getByRole("button", { name: START_BUTTON_NAME });
|
|
187
|
+
if (await startUsingManager.isVisible()) {
|
|
188
|
+
await startUsingManager.click();
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
const acceptDocument = page.getByRole("button", { name: ACCEPT_BUTTON_NAME, exact: true });
|
|
192
|
+
if (await acceptDocument.isVisible()) {
|
|
193
|
+
if (documents >= exports.MAX_TERMS_DOCUMENTS) {
|
|
194
|
+
throw new hostReadiness_1.HostNeverBecameReadyError();
|
|
195
|
+
}
|
|
196
|
+
await acceptDocument.click();
|
|
197
|
+
documents += 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const remaining = deadline - now();
|
|
201
|
+
if (remaining <= 0) {
|
|
202
|
+
throw new hostReadiness_1.HostNeverBecameReadyError();
|
|
203
|
+
}
|
|
204
|
+
await pause(Math.min(TERMS_POLL_INTERVAL_MS, remaining));
|
|
205
|
+
}
|
|
206
|
+
await waitForPathname(page, pathname => pathname !== exports.TERMS_ROUTE, now, pause, deadline);
|
|
207
|
+
};
|
|
208
|
+
exports.acceptTermsWithinBudget = acceptTermsWithinBudget;
|
|
209
|
+
//# sourceMappingURL=acceptTerms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acceptTerms.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/fixtures/acceptTerms.ts"],"names":[],"mappings":"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","sourcesContent":["import { errors } from \"@playwright/test\";\nimport { HostNeverBecameReadyError } from \"./hostReadiness\";\n\n/** Route Manager's `TermsGate` redirects to while the signed-in user has not accepted the terms. */\nexport const TERMS_ROUTE = \"/terms\";\n\nexport const TERMS_STATUS_OPERATION = \"TermsAndConditionsStatus\";\n\n/** Upper bound on documents to click through, so an unexpected flow fails instead of looping forever. */\nexport const MAX_TERMS_DOCUMENTS = 20;\n\nconst TERMS_REDIRECT_TIMEOUT_MS = 20_000;\n\n/** Gap between checks while the terms buttons are not on screen yet. */\nconst TERMS_POLL_INTERVAL_MS = 1_000;\n\nconst ACCEPT_BUTTON_NAME = \"Accept\";\nconst START_BUTTON_NAME = \"Start using Manager\";\n\nconst isPlaywrightTimeout = (error: unknown): boolean => error instanceof errors.TimeoutError;\n\n/** Reads `data.termsAndConditionsStatus.hasAccepted`, or `undefined` when the body has no usable answer. */\nexport const readHasAccepted = (body: unknown): boolean | undefined => {\n if (typeof body !== \"object\" || body === null || !(\"data\" in body)) {\n return undefined;\n }\n const data = body.data;\n if (typeof data !== \"object\" || data === null || !(\"termsAndConditionsStatus\" in data)) {\n return undefined;\n }\n const status = data.termsAndConditionsStatus;\n if (typeof status !== \"object\" || status === null || !(\"hasAccepted\" in status)) {\n return undefined;\n }\n return typeof status.hasAccepted === \"boolean\" ? status.hasAccepted : undefined;\n};\n\n/** Whether `url` is the page `TermsGate` redirects to, not the review-terms preferences page. */\nexport const isTermsRoute = (url: string): boolean => {\n try {\n return new URL(url).pathname === TERMS_ROUTE;\n } catch {\n return false;\n }\n};\n\ntype TermsStatusResponse = {\n ok: () => boolean;\n json: () => Promise<unknown>;\n};\n\nexport type TermsStatusRequest = {\n url: () => string;\n response: () => Promise<TermsStatusResponse | null | undefined>;\n};\n\ntype TermsStatusListener = (request: TermsStatusRequest) => void;\n\nexport type TermsStatusPage = {\n on: (event: \"request\", listener: TermsStatusListener) => void;\n off: (event: \"request\", listener: TermsStatusListener) => void;\n};\n\nexport type TermsStatusWatcher = {\n /** The latest `hasAccepted` the app received, waiting for a request that is still in flight. */\n hasAccepted: () => Promise<boolean | undefined>;\n /** True once a `TermsAndConditionsStatus` request has been observed. */\n seenRequest: () => boolean;\n stop: () => void;\n};\n\n/**\n * Records the app's own `TermsAndConditionsStatus` lookup. `TermsGate` sits inside `HostLayout`, so the\n * main menu can finish loading before the gate redirects to `/terms`; the status response is the only\n * signal that the redirect is coming. Must be started before the navigation that signs the user in.\n */\nexport const watchTermsStatus = (page: TermsStatusPage): TermsStatusWatcher => {\n let seen = false;\n let latest: Promise<boolean | undefined> | undefined;\n\n const onRequest: TermsStatusListener = request => {\n if (!request.url().includes(TERMS_STATUS_OPERATION)) {\n return;\n }\n seen = true;\n // The body can be discarded by a navigation before it is read; a missed read means \"unknown\".\n latest = request\n .response()\n .then(async response => (response?.ok() === true ? readHasAccepted(await response.json()) : undefined))\n .catch(() => undefined);\n };\n\n page.on(\"request\", onRequest);\n\n return {\n hasAccepted: async () => (latest === undefined ? undefined : await latest),\n seenRequest: () => seen,\n stop: () => {\n page.off(\"request\", onRequest);\n },\n };\n};\n\n/**\n * Reads the latest terms status without waiting past `deadline`.\n *\n * No request yet resolves as `undefined` immediately — `TermsGate` is lazy, so the\n * lookup often starts after the host layout is already visible. An in-flight response\n * that never arrives throws {@link HostNeverBecameReadyError} when `deadline` is\n * reached, instead of sitting outside the login budget.\n */\nexport const readTermsStatusWithinDeadline = async (\n watcher: TermsStatusWatcher,\n now: () => number,\n pause: (ms: number) => Promise<void>,\n deadline: number\n): Promise<boolean | undefined> => {\n if (!watcher.seenRequest()) {\n return undefined;\n }\n\n const pending = watcher.hasAccepted();\n const settled = pending.then(value => ({ kind: \"settled\" as const, value }));\n const peeked = await Promise.race([\n settled,\n Promise.resolve().then(() => ({ kind: \"pending\" as const })),\n ]);\n if (peeked.kind === \"settled\") {\n return peeked.value;\n }\n\n while (now() < deadline) {\n const remaining = deadline - now();\n if (remaining <= 0) {\n break;\n }\n const started = now();\n const step = await Promise.race([\n settled,\n pause(Math.min(TERMS_POLL_INTERVAL_MS, remaining)).then(() => ({ kind: \"paused\" as const })),\n ]);\n if (step.kind === \"settled\") {\n return step.value;\n }\n // A pause that does not consume the budget must not drop a body that is already resolving.\n if (now() === started) {\n const late = await Promise.race([\n settled,\n Promise.resolve()\n .then(() => Promise.resolve())\n .then(() => Promise.resolve())\n .then(() => ({ kind: \"pending\" as const })),\n ]);\n if (late.kind === \"settled\") {\n return late.value;\n }\n throw new HostNeverBecameReadyError();\n }\n }\n\n throw new HostNeverBecameReadyError();\n};\n\nexport type TermsButton = {\n isVisible: () => Promise<boolean>;\n click: () => Promise<void>;\n};\n\nexport type TermsPromptPage = {\n url: () => string;\n waitForURL: (predicate: (url: URL) => boolean, options?: { timeout?: number }) => Promise<unknown>;\n getByRole: (role: \"button\", options: { name: string; exact?: boolean }) => TermsButton;\n};\n\nconst pathnameOf = (url: string): string | undefined => {\n try {\n return new URL(url).pathname;\n } catch {\n return undefined;\n }\n};\n\nconst waitForPathname = async (\n page: TermsPromptPage,\n matches: (pathname: string) => boolean,\n now: () => number,\n pause: (ms: number) => Promise<void>,\n deadline: number\n): Promise<void> => {\n if (matches(pathnameOf(page.url()) ?? \"\")) {\n return;\n }\n\n let lastTimeout: unknown;\n while (now() < deadline) {\n const remaining = deadline - now();\n const timeout = Math.min(TERMS_REDIRECT_TIMEOUT_MS, remaining);\n if (timeout <= 0) {\n break;\n }\n const started = now();\n try {\n await page.waitForURL(url => matches(url.pathname), { timeout });\n if (matches(pathnameOf(page.url()) ?? \"\")) {\n return;\n }\n } catch (error) {\n if (!isPlaywrightTimeout(error)) {\n throw error;\n }\n lastTimeout = error;\n }\n // A Playwright timeout already consumed real time. A fake that rejects without\n // moving the injected clock still has to yield or this loop never reaches the deadline.\n if (now() === started) {\n const remainingAfter = deadline - now();\n if (remainingAfter <= 0) {\n break;\n }\n await pause(Math.min(TERMS_POLL_INTERVAL_MS, remainingAfter));\n }\n }\n\n throw new HostNeverBecameReadyError(lastTimeout);\n};\n\n/**\n * Clicks through Manager's terms flow when the signed-in user is being asked to accept.\n *\n * Root cause (GLU-2632): the feature-branch prompt is not a finished login, and waiting\n * never clicks it. Success is leaving `/terms` — the app reloads to `/` and discards the\n * mutation body. Staying on `/terms` until `deadline` is {@link HostNeverBecameReadyError},\n * not a generic \"still on /terms\" failure.\n */\nexport const acceptTermsWithinBudget = async ({\n page,\n hasAccepted,\n now,\n sleep: pause,\n deadline,\n}: {\n page: TermsPromptPage;\n hasAccepted: boolean | undefined;\n now: () => number;\n sleep: (ms: number) => Promise<void>;\n deadline: number;\n}): Promise<void> => {\n if (hasAccepted !== false && !isTermsRoute(page.url())) {\n return;\n }\n\n await waitForPathname(page, pathname => pathname === TERMS_ROUTE, now, pause, deadline);\n\n let documents = 0;\n for (;;) {\n if (now() >= deadline) {\n throw new HostNeverBecameReadyError();\n }\n const startUsingManager = page.getByRole(\"button\", { name: START_BUTTON_NAME });\n if (await startUsingManager.isVisible()) {\n await startUsingManager.click();\n break;\n }\n const acceptDocument = page.getByRole(\"button\", { name: ACCEPT_BUTTON_NAME, exact: true });\n if (await acceptDocument.isVisible()) {\n if (documents >= MAX_TERMS_DOCUMENTS) {\n throw new HostNeverBecameReadyError();\n }\n await acceptDocument.click();\n documents += 1;\n continue;\n }\n const remaining = deadline - now();\n if (remaining <= 0) {\n throw new HostNeverBecameReadyError();\n }\n await pause(Math.min(TERMS_POLL_INTERVAL_MS, remaining));\n }\n\n await waitForPathname(page, pathname => pathname !== TERMS_ROUTE, now, pause, deadline);\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseTest.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/fixtures/baseTest.ts"],"names":[],"mappings":";;;AAAA,2CAAsD;AAQ7C,uFARA,aAAM,OAQA;AAPf,iEAAkE;AAClE,iEAAkE;AAClE,uDAAwD;AACxD,iEAAkE;AAClE,mDAAoD;AAEvC,QAAA,IAAI,GAAG,IAAA,iBAAU,EAAC,2BAAgB,EAAE,oBAAS,EAAE,sBAAW,EAAE,2BAAgB,EAAE,2BAAgB,CAAC,CAAC","sourcesContent":["import { expect, mergeTests } from \"@playwright/test\";\nimport { test as featureFlagsTest } from \"./featureFlags.fixture\";\nimport { test as harRecordingTest } from \"./harRecording.fixture\";\nimport { test as irisAppTest } from \"./irisApp.fixture\";\nimport { test as localDevModeTest } from \"./localDevMode.fixture\";\nimport { test as loginTest } from \"./login.fixture\";\n\nexport const test = mergeTests(harRecordingTest, loginTest, irisAppTest, featureFlagsTest, localDevModeTest);\nexport { expect };\n\nexport type { FeatureFlag, FeatureFlagFixtures } from \"./featureFlags.fixture\";\nexport type { IrisAppFixtures, IrisAppOptions, StorybookPreviewOptions } from \"./irisApp.fixture\";\nexport type { LocalDevModeFixtures } from \"./localDevMode.fixture\";\nexport type { AuthCredentials, LoginOptions, TrackunitBaseFixtures } from \"./login.fixture\";\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"describe.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/fixtures/describe.ts"],"names":[],"mappings":";;;AAAA,2CAA0D;AAK1D,MAAM,eAAe,GAAG,CAAC,KAAa,EAAU,EAAE;IAChD,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;IACxC,OAAO,SAAS,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,SAAS,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;AACvD,CAAC,CAAC;AAEF;;;GAGG;AACU,QAAA,QAAQ,GAAwD,MAAM,CAAC,MAAM,CACxF,CAAC,KAAa,EAAE,QAA0B,EAAQ,EAAE;IAClD,WAAc,CAAC,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC;AAC5D,CAAC,EACD;IACE,IAAI,EAAE,CAAC,KAAa,EAAE,QAA0B,EAAQ,EAAE;QACxD,WAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,EAAE,CAAC,KAAa,EAAE,QAA0B,EAAQ,EAAE;QACxD,WAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC;IACjE,CAAC;CACF,CACF,CAAC","sourcesContent":["import { test as playwrightTest } from \"@playwright/test\";\n\ntype DescribeCallback = () => void;\ntype DescribeFn = (title: string, callback: DescribeCallback) => void;\n\nconst addOwnerToTitle = (title: string): string => {\n const codeowner = process.env.codeowner;\n return codeowner ? `${title} [${codeowner}]` : title;\n};\n\n/**\n * Wrapped `describe` that appends `[codeowner]` to the suite title when the\n * `codeowner` environment variable is set. Drop-in replacement for `test.describe`.\n */\nexport const describe: DescribeFn & { only: DescribeFn; skip: DescribeFn } = Object.assign(\n (title: string, callback: DescribeCallback): void => {\n playwrightTest.describe(addOwnerToTitle(title), callback);\n },\n {\n only: (title: string, callback: DescribeCallback): void => {\n playwrightTest.describe.only(addOwnerToTitle(title), callback);\n },\n skip: (title: string, callback: DescribeCallback): void => {\n playwrightTest.describe.skip(addOwnerToTitle(title), callback);\n },\n }\n);\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"featureFlags.fixture.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/fixtures/featureFlags.fixture.ts"],"names":[],"mappings":"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","sourcesContent":["import { test as base, type Page, type Response } from \"@playwright/test\";\n\nexport interface FeatureFlag {\n key: string;\n state: boolean;\n}\n\nconst featureFlagsCache = new WeakMap<Page, Array<FeatureFlag>>();\nconst featureFlagsInFlight = new WeakMap<Page, Promise<Array<FeatureFlag> | undefined>>();\nconst listenerInstalled = new WeakSet<Page>();\n\nconst extractFlags = async (response: Response): Promise<Array<FeatureFlag> | undefined> => {\n // The response body is not always retrievable by the time this runs: a\n // navigation (e.g. the post-login redirect) can discard the network resource\n // before `response.json()` reads it, making Playwright reject with\n // \"Protocol error (Network.getResponseBody): No resource with given identifier\n // found\". This listener is a passive observer, so a missed capture is benign —\n // swallow it and report \"no flags\" rather than letting the rejection surface\n // as an unhandled error that fails the in-flight test.\n try {\n const body = await response.json();\n const flags = body?.data?.featureFlags;\n return Array.isArray(flags) ? flags : undefined;\n } catch {\n return undefined;\n }\n};\n\n// Tracks the in-flight extraction promise per page so `configCat` can await it\n// instead of falling through to `page.waitForResponse(...)`, which would wait\n// for the *next* matching response and hang when the original already happened.\nconst trackExtraction = (page: Page, response: Response): Promise<Array<FeatureFlag> | undefined> => {\n const promise = extractFlags(response)\n .then(flags => {\n if (flags !== undefined) {\n featureFlagsCache.set(page, flags);\n }\n return flags;\n })\n .finally(() => {\n if (featureFlagsInFlight.get(page) === promise) {\n featureFlagsInFlight.delete(page);\n }\n });\n featureFlagsInFlight.set(page, promise);\n return promise;\n};\n\nconst installFlagListener = (page: Page): void => {\n if (listenerInstalled.has(page)) {\n return;\n }\n listenerInstalled.add(page);\n\n page.on(\"response\", response => {\n if (response.url().includes(\"ValidateFeatureFlags\") && response.ok()) {\n // Defensive: `trackExtraction` already swallows body-read failures, but\n // guard the floating promise so nothing can become an unhandled rejection\n // that would fail an unrelated in-flight test.\n void trackExtraction(page, response).catch(() => undefined);\n }\n });\n};\n\nexport interface FeatureFlagFixtures {\n /**\n * Call before the navigation that triggers `ValidateFeatureFlags`, `UserPermissions`,\n * and `ActiveSubscription` requests to ensure the responses are captured for `configCat`.\n */\n getValidateFeatureFlags: () => void;\n /**\n * Returns the state of a ConfigCat feature flag from the captured `ValidateFeatureFlags` response.\n * Waits for the response if not yet captured.\n *\n * @param flag - The feature flag key to look up\n */\n configCat: (flag: string) => Promise<boolean>;\n}\n\nexport const test = base.extend<FeatureFlagFixtures>({\n getValidateFeatureFlags: async ({ page }, provide) => {\n await provide(() => {\n installFlagListener(page);\n });\n },\n\n configCat: async ({ page }, provide) => {\n await provide(async (flag: string) => {\n if (!featureFlagsCache.has(page)) {\n installFlagListener(page);\n // If the listener already saw the response, an extraction is in flight —\n // await it before deciding to wait for another network response.\n const inFlight = featureFlagsInFlight.get(page);\n if (inFlight !== undefined) {\n await inFlight;\n }\n if (!featureFlagsCache.has(page)) {\n const response = await page.waitForResponse(\n r => r.url().includes(\"ValidateFeatureFlags\") && r.ok(),\n { timeout: 30000 }\n );\n const parsedFlags = await extractFlags(response);\n if (parsedFlags !== undefined) {\n featureFlagsCache.set(page, parsedFlags);\n }\n }\n }\n const cachedFlags = featureFlagsCache.get(page);\n const found = cachedFlags?.find(ff => ff.key === flag);\n if (found === undefined) {\n throw new Error(`Feature flag '${flag}' not found in ValidateFeatureFlags response`);\n }\n return found.state;\n });\n },\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harRecording.fixture.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/fixtures/harRecording.fixture.ts"],"names":[],"mappings":";;;;AAAA,2CAAgD;AAChD,2BAA+B;AAC/B,mDAA6B;AAC7B,kEAA+D;AAC/D,+CAA4C;AAE5C;;;;;;;GAOG;AACU,QAAA,wBAAwB,GAAG,KAAM,CAAC;AAE/C;;;;;;;GAOG;AACU,QAAA,8BAA8B,GAAG,KAAM,CAAC;AAOrD;;;;;;;;;GASG;AACI,MAAM,wBAAwB,GAAG,KAAK,EAC3C,OAAwB,EACxB,UAAuC,EAAE,EAC1B,EAAE;IACjB,IAAI,CAAC;QACH,MAAM,IAAA,yBAAW,EAAC,OAAO,CAAC,KAAK,EAAE,EAAE,OAAO,CAAC,cAAc,IAAI,gCAAwB,CAAC,CAAC;IACzF,CAAC;IAAC,MAAM,CAAC;QACP,yEAAyE;QACzE,0DAA0D;IAC5D,CAAC;AACH,CAAC,CAAC;AAVW,QAAA,wBAAwB,4BAUnC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACU,QAAA,IAAI,GAAG,WAAI,CAAC,MAAM,CAAC;IAC9B,cAAc,EAAE,KAAK,EAAE,EAAE,cAAc,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE;QAC9D,MAAM,OAAO,GAAG,IAAA,mCAAgB,EAAC,QAAQ,CAAC,CAAC;QAC3C,8EAA8E;QAC9E,qEAAqE;QACrE,IAAA,cAAS,EAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,MAAM,OAAO,CAAC;YACZ,GAAG,cAAc;YACjB,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;SAC9C,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE;QACP,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE;YAC7B,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;YACvB,MAAM,IAAA,gCAAwB,EAAC,OAAO,CAAC,CAAC;QAC1C,CAAC;QACD,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,sCAA8B,EAAE;KAC3D;CACF,CAAC,CAAC","sourcesContent":["import { test as base } from \"@playwright/test\";\nimport { mkdirSync } from \"fs\";\nimport * as path from \"path\";\nimport { harRecordingPath } from \"../plugins/harRecordingPath\";\nimport { withTimeout } from \"./withTimeout\";\n\n/**\n * Budget for the `context.close()` we issue after the test body. Closing is\n * normally fast, but on a hard test timeout the page is often unresponsive and\n * an unbounded close would consume this fixture's teardown budget. We bound it\n * and move on; Playwright's built-in `context` teardown runs afterwards and\n * awaits the real close within its own budget, so native video/trace\n * persistence is never gated behind our close.\n */\nexport const CONTEXT_CLOSE_TIMEOUT_MS = 10_000;\n\n/**\n * Dedicated per-fixture teardown budget for the `context` override (see the tuple\n * passed to `base.extend` below). Without it, this override's teardown draws from\n * the single teardown budget shared with Playwright's built-in `context` teardown —\n * so a slow close could starve native video/trace persistence. Giving the override\n * its own budget decouples the two. It comfortably exceeds the bounded close; the\n * unit test asserts that invariant.\n */\nexport const HAR_CONTEXT_FIXTURE_TIMEOUT_MS = 25_000;\n\n/** Minimal structural view of the browser context teardown needs. */\ninterface ClosableContext {\n close(): Promise<void>;\n}\n\n/**\n * Closes the context (bounded, best-effort).\n *\n * Extracted from the fixture so the teardown bounding can be unit tested without a\n * browser. The bounded close guarantees we return control even if the page is hung,\n * so Playwright's native context teardown (which persists video/trace) is never\n * starved. The HAR is **not** handled here — `LogsReporter.onTestEnd` reads the raw\n * HAR from `harRecordingPath` after all teardown, which is the only point that\n * reliably runs after a late close flushes the HAR on the hard-timeout path.\n */\nexport const closeContextWithinBudget = async (\n context: ClosableContext,\n options: { closeTimeoutMs?: number } = {}\n): Promise<void> => {\n try {\n await withTimeout(context.close(), options.closeTimeoutMs ?? CONTEXT_CLOSE_TIMEOUT_MS);\n } catch {\n // Bounded-out or failed close: leave it to Playwright's built-in context\n // teardown to await the real close within its own budget.\n }\n};\n\n/**\n * Records a per-test HAR without leaking it into stored artifacts.\n *\n * `recordHar` (configured via `contextOptions`) only flushes the HAR to disk when\n * the browser context closes. We record it to a temp path outside `outputDir`\n * (see `harRecordingPath`) so a close that flushes late — e.g. during native\n * teardown on a hard timeout — can never leak an unredacted HAR into uploaded\n * artifacts. `LogsReporter.onTestEnd` is the single owner of that file: it runs\n * after all teardown, redacts + retains it on failure, and always deletes the raw\n * source. Persisting the HAR there (rather than via `testInfo.attach` during this\n * fixture's teardown) is what makes a hard-timeout HAR survive — the attach seam\n * ran before the late close flushed the file and lost it.\n *\n * The `context` override exists only to bound the close so a hung page can't starve\n * native video/trace persistence; it carries its own `timeout`\n * (`HAR_CONTEXT_FIXTURE_TIMEOUT_MS`) so its teardown draws from a dedicated budget\n * rather than the one shared with the built-in `context` teardown.\n */\nexport const test = base.extend({\n contextOptions: async ({ contextOptions }, provide, testInfo) => {\n const harPath = harRecordingPath(testInfo);\n // `recordHar` doesn't create the parent directory; ensure it exists since the\n // path lives in a temp dir outside Playwright's managed output tree.\n mkdirSync(path.dirname(harPath), { recursive: true });\n await provide({\n ...contextOptions,\n recordHar: { path: harPath, mode: \"minimal\" },\n });\n },\n\n context: [\n async ({ context }, provide) => {\n await provide(context);\n await closeContextWithinBudget(context);\n },\n { scope: \"test\", timeout: HAR_CONTEXT_FIXTURE_TIMEOUT_MS },\n ],\n});\n"]}
|
|
@@ -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"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hydrateStorageState.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/fixtures/hydrateStorageState.ts"],"names":[],"mappings":";;;AAmBA;;;;;;;;;;;;;;;GAeG;AACI,MAAM,mBAAmB,GAAG,KAAK,EAAE,OAA6B,EAAE,KAAyB,EAAiB,EAAE;IACnH,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,MAAM,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC1C,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO;IACT,CAAC;IAED,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC,OAAsC,EAAE,EAAE;QACrE,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,KAAK,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC7E,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,OAAO;QACT,CAAC;QACD,KAAK,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;YACjD,IAAI,CAAC;gBACH,yJAAyJ;gBACzJ,oDAAoD;gBACpD,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAC3C,CAAC;YAAC,MAAM,CAAC;gBACP,uFAAuF;YACzF,CAAC;QACH,CAAC;IACH,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;AACpB,CAAC,CAAC;AAxBW,QAAA,mBAAmB,uBAwB9B","sourcesContent":["import type { BrowserContext } from \"@playwright/test\";\n\n/**\n * Subset of Playwright's `StorageState` we persist between tests within the\n * same worker. We model it explicitly rather than re-exporting\n * `Awaited<ReturnType<BrowserContext['storageState']>>` so the cache\n * contract is greppable from spec files and Playwright API churn cannot\n * silently widen what we're persisting.\n */\nexport interface CachedStorageState {\n cookies: Parameters<BrowserContext[\"addCookies\"]>[0];\n origins: Array<{\n origin: string;\n localStorage: Array<{ name: string; value: string }>;\n }>;\n}\n\nexport type StorageStateHydrator = Pick<BrowserContext, \"addCookies\" | \"addInitScript\">;\n\n/**\n * Restores cookies and localStorage from a cached `storageState` snapshot\n * into the supplied browser context.\n *\n * Cookies go through `context.addCookies` (synchronous from the page's POV\n * because they're set before any navigation). LocalStorage requires an\n * `addInitScript` because it can only be written by JS executing on the\n * matching origin — Playwright runs the init script in every new document,\n * and we gate it by `window.location.origin` so we don't smuggle localStorage\n * onto unrelated origins (e.g. about:blank, OAuth IdP domains, or iframes\n * pointing at iris-app subdomains).\n *\n * Wrapping each `setItem` in try/catch protects against contexts where\n * localStorage is unavailable (private browsing modes, sandboxed iframes,\n * etc.) — we never want hydration to crash the page load.\n */\nexport const hydrateStorageState = async (context: StorageStateHydrator, state: CachedStorageState): Promise<void> => {\n if (state.cookies.length > 0) {\n await context.addCookies(state.cookies);\n }\n\n if (state.origins.length === 0) {\n return;\n }\n\n await context.addInitScript((origins: CachedStorageState[\"origins\"]) => {\n const match = origins.find(entry => entry.origin === window.location.origin);\n if (match === undefined) {\n return;\n }\n for (const { name, value } of match.localStorage) {\n try {\n // TODO: playwright fixture — this file was exempted when the no-direct-storage-access rule was introduced; hooks are not usable in addInitScript context\n // eslint-disable-next-line no-restricted-properties\n window.localStorage.setItem(name, value);\n } catch {\n /* localStorage may be unavailable (sandboxed iframe, private mode) — skip silently. */\n }\n }\n }, state.origins);\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"irisApp.fixture.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/fixtures/irisApp.fixture.ts"],"names":[],"mappings":";;;AAAA,2CAAmE;AAkBtD,QAAA,IAAI,GAAG,WAAI,CAAC,MAAM,CAAkB;IAC/C,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,EAAE;QACnC,MAAM,OAAO,CAAC,CAAC,OAAwB,EAAE,EAAE,CACzC,IAAI,CAAC,YAAY,CAAC,uBAAuB,OAAO,EAAE,MAAM,IAAI,YAAY,IAAI,CAAC,CAC9E,CAAC;IACJ,CAAC;IAED,gBAAgB,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,EAAE;QAC5C,MAAM,OAAO,CAAC,CAAC,OAAiC,EAAE,EAAE,CAClD,IAAI,CAAC,YAAY,CAAC,cAAc,OAAO,EAAE,QAAQ,IAAI,0BAA0B,IAAI,CAAC,CACrF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC","sourcesContent":["import { test as base, type FrameLocator } from \"@playwright/test\";\n\nexport interface IrisAppOptions {\n testId?: string;\n}\n\nexport interface StorybookPreviewOptions {\n /** Storybook preview iframe element id. Defaults to `storybook-preview-iframe`. */\n iframeId?: string;\n}\n\nexport interface IrisAppFixtures {\n /** Returns a FrameLocator for the iris app iframe. Defaults to `data-testid=\"app-iframe\"`. */\n irisApp: (options?: IrisAppOptions) => FrameLocator;\n /** Returns a FrameLocator for the Storybook preview iframe. Defaults to `id=\"storybook-preview-iframe\"`. */\n storybookPreview: (options?: StorybookPreviewOptions) => FrameLocator;\n}\n\nexport const test = base.extend<IrisAppFixtures>({\n irisApp: async ({ page }, provide) => {\n await provide((options?: IrisAppOptions) =>\n page.frameLocator(`iframe[data-testid=\"${options?.testId ?? \"app-iframe\"}\"]`)\n );\n },\n\n storybookPreview: async ({ page }, provide) => {\n await provide((options?: StorybookPreviewOptions) =>\n page.frameLocator(`iframe[id=\"${options?.iframeId ?? \"storybook-preview-iframe\"}\"]`)\n );\n },\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localDevMode.fixture.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/fixtures/localDevMode.fixture.ts"],"names":[],"mappings":";;;AAAA,2CAAgD;AAOnC,QAAA,IAAI,GAAG,WAAI,CAAC,MAAM,CAAuB;IACpD,oBAAoB,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,EAAE;QAChD,MAAM,OAAO,CAAC,KAAK,IAAI,EAAE;YACvB,MAAM,IAAI,CAAC,WAAW,CAAC,oBAAoB,CAAC,CAAC,KAAK,EAAE,CAAC;YACrD,MAAM,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;YAC9F,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,0BAA0B,CAAC,CAAC;YAC5D,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC,EAAE,CAAC;gBAChC,MAAM,IAAI,CAAC,WAAW,CAAC,0BAA0B,CAAC,CAAC,KAAK,EAAE,CAAC;YAC7D,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;CACF,CAAC,CAAC","sourcesContent":["import { test as base } from \"@playwright/test\";\n\nexport interface LocalDevModeFixtures {\n /** Navigates to the developer portal and toggles local dev mode on if not already enabled. */\n switchToLocalDevMode: () => Promise<void>;\n}\n\nexport const test = base.extend<LocalDevModeFixtures>({\n switchToLocalDevMode: async ({ page }, provide) => {\n await provide(async () => {\n await page.getByTestId(\"developerPortalNav\").click();\n await page.waitForURL(url => url.toString().includes(\"/iris-sdk-portal\"), { timeout: 15000 });\n const toggle = page.getByTestId(\"localDevModeSwitch-input\");\n if (!(await toggle.isChecked())) {\n await page.getByTestId(\"localDevModeSwitch-thumb\").click();\n }\n });\n },\n});\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type TermsStatusPage } from "./acceptTerms";
|
|
2
|
+
import { type CachedStorageState, type StorageStateHydrator } from "./hydrateStorageState";
|
|
2
3
|
import { type AuthCredentials, type ResolveCredentialsOptions } from "./resolveCredentials";
|
|
3
4
|
export type { AuthCredentials };
|
|
4
5
|
export type LoginOptions = ResolveCredentialsOptions;
|
|
@@ -26,6 +27,14 @@ export declare const MAIN_MENU_READY_SELECTOR = "[data-testid=\"mainMenu\"]:not(
|
|
|
26
27
|
type MainMenuReadinessInput = {
|
|
27
28
|
readonly selector: string;
|
|
28
29
|
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;
|
|
29
38
|
};
|
|
30
39
|
type MainMenuWaitPage = {
|
|
31
40
|
waitForFunction: (pageFunction: (input: MainMenuReadinessInput) => boolean, input: MainMenuReadinessInput, options?: {
|
|
@@ -53,12 +62,24 @@ export declare const extendTestTimeout: (testInfo: TestTimeoutInfo | undefined,
|
|
|
53
62
|
* branch is unchanged.
|
|
54
63
|
*/
|
|
55
64
|
export declare const reserveFeatureBranchLoginBudget: (testInfo?: TestTimeoutInfo) => void;
|
|
56
|
-
/**
|
|
57
|
-
|
|
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;
|
|
58
75
|
/**
|
|
59
76
|
* Waits until Manager's main menu is rendered without a loading skeleton, or
|
|
60
77
|
* until the browser is on a HostLayout hide-menu route (marketplace / auth /
|
|
61
78
|
* 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.
|
|
62
83
|
*/
|
|
63
84
|
export declare const waitForMainMenuReady: (page: MainMenuWaitPage, testInfo?: TestTimeoutInfo) => Promise<void>;
|
|
64
85
|
type EnvReadyResponse = {
|
|
@@ -138,6 +159,10 @@ type LoginRequestContext = {
|
|
|
138
159
|
type SessionDocument = {
|
|
139
160
|
ok: () => boolean;
|
|
140
161
|
};
|
|
162
|
+
type TermsButton = {
|
|
163
|
+
isVisible: () => Promise<boolean>;
|
|
164
|
+
click: () => Promise<void>;
|
|
165
|
+
};
|
|
141
166
|
type OktaLoginPage = {
|
|
142
167
|
goto: (url: string, options?: {
|
|
143
168
|
timeout?: number;
|
|
@@ -147,19 +172,31 @@ type OktaLoginPage = {
|
|
|
147
172
|
}) => Promise<unknown>;
|
|
148
173
|
locator: (selector: string) => {
|
|
149
174
|
waitFor: (options: {
|
|
150
|
-
state: "visible";
|
|
175
|
+
state: "visible" | "hidden";
|
|
151
176
|
timeout: number;
|
|
152
177
|
}) => Promise<unknown>;
|
|
178
|
+
isVisible?: () => Promise<boolean>;
|
|
153
179
|
};
|
|
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"];
|
|
154
189
|
};
|
|
155
190
|
/**
|
|
156
191
|
* Performs the slow path: wait for the branch host to serve `/env`, hit Okta's
|
|
157
192
|
* `/api/v1/authn`, redeem the `sessionToken` via the manager-classic redirect,
|
|
158
|
-
* and wait for the host layout to render.
|
|
159
|
-
*
|
|
193
|
+
* and wait for the host layout to render. On a feature-branch host it then
|
|
194
|
+
* finishes the terms prompt and waits for the main shell inside the same
|
|
195
|
+
* budget. Factored out so the cache hit/miss branches read clearly.
|
|
160
196
|
*
|
|
161
197
|
* `createRequestContext`, `now`, and `sleep` are injectable so a unit test can
|
|
162
|
-
* drive the session handoff without a browser or a real
|
|
198
|
+
* drive the session handoff and the terms prompt without a browser or a real
|
|
199
|
+
* three-minute wait.
|
|
163
200
|
*/
|
|
164
201
|
export declare const performOktaLogin: ({ page, baseURL, credentials, testInfo, createRequestContext, now, sleep: pause, }: {
|
|
165
202
|
page: OktaLoginPage;
|
|
@@ -172,4 +209,28 @@ export declare const performOktaLogin: ({ page, baseURL, credentials, testInfo,
|
|
|
172
209
|
/** Injectable pause between session document reloads. Production waits on the event loop. */
|
|
173
210
|
sleep?: (ms: number) => Promise<void>;
|
|
174
211
|
}) => Promise<void>;
|
|
212
|
+
type SignInPage = Omit<OktaLoginPage, "waitForFunction"> & {
|
|
213
|
+
context: () => StorageStateHydrator & {
|
|
214
|
+
storageState: () => Promise<CachedStorageState>;
|
|
215
|
+
};
|
|
216
|
+
/** Required so the shell wait after a restored session can run in the page. */
|
|
217
|
+
waitForFunction: MainMenuWaitPage["waitForFunction"];
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* Signs in, reusing this worker's cached session when one exists.
|
|
221
|
+
*
|
|
222
|
+
* `now` and `sleep` are injectable so a unit test can drive the restored
|
|
223
|
+
* feature-branch terms wait without a browser or a real three-minute clock.
|
|
224
|
+
* Production uses `Date.now` and the event loop.
|
|
225
|
+
*/
|
|
226
|
+
export declare const signIn: ({ page, baseURL, cache, testInfo, options, now, sleep: pause, createRequestContext, }: {
|
|
227
|
+
page: SignInPage;
|
|
228
|
+
baseURL: string | undefined;
|
|
229
|
+
cache: Map<string, CachedStorageState>;
|
|
230
|
+
testInfo?: TestTimeoutInfo;
|
|
231
|
+
options?: LoginOptions;
|
|
232
|
+
now?: () => number;
|
|
233
|
+
sleep?: (ms: number) => Promise<void>;
|
|
234
|
+
createRequestContext?: (baseURL: string | undefined) => Promise<LoginRequestContext>;
|
|
235
|
+
}) => Promise<void>;
|
|
175
236
|
export { expect } from "@playwright/test";
|