mirror of
https://github.com/Kong/insomnia.git
synced 2026-11-07 13:34:09 -05:00
572 lines
20 KiB
TypeScript
572 lines
20 KiB
TypeScript
import { ToggleButtonComponent } from "@package/components/toggle.component";
|
|
import { AuthType } from "@package/enums/auth-type";
|
|
import { ACTION_TIMEOUT, DEFAULT_TIMEOUT } from "@package/misc/constants";
|
|
import type { Locator } from "@playwright/test";
|
|
import { expect } from "@playwright/test";
|
|
import type {
|
|
AuthTypeAPIKey,
|
|
AuthTypeBasic,
|
|
AuthTypeBearer,
|
|
AuthTypeOAuth1,
|
|
AuthTypeOAuth2,
|
|
AuthTypeSingleToken,
|
|
} from "insomnia-data";
|
|
import type { OAuth1SignatureMethod } from "insomnia-data/common";
|
|
|
|
import { TabPanelComponent } from "./tab-panel.component";
|
|
|
|
type OAuth1TextField = keyof Omit<
|
|
AuthTypeOAuth1,
|
|
"type" | "disabled" | "signatureMethod" | "privateKey" | "includeBodyHash"
|
|
>;
|
|
|
|
const OAUTH1_FIELD_LABELS: Record<OAuth1TextField, string> = {
|
|
consumerKey: "Consumer Key",
|
|
consumerSecret: "Consumer Secret",
|
|
tokenKey: "Token Key",
|
|
tokenSecret: "Token Secret",
|
|
callback: "Callback URL",
|
|
version: "Version",
|
|
timestamp: "Timestamp",
|
|
realm: "Realm",
|
|
nonce: "Nonce",
|
|
verifier: "Verifier",
|
|
};
|
|
|
|
type OAuth2TextField = keyof Pick<
|
|
AuthTypeOAuth2,
|
|
| "authorizationUrl"
|
|
| "accessTokenUrl"
|
|
| "clientId"
|
|
| "clientSecret"
|
|
| "redirectUrl"
|
|
| "username"
|
|
| "password"
|
|
| "scope"
|
|
| "state"
|
|
| "tokenPrefix"
|
|
| "audience"
|
|
| "resource"
|
|
| "origin"
|
|
>;
|
|
|
|
const OAUTH2_FIELD_LABELS: Record<OAuth2TextField, string> = {
|
|
authorizationUrl: "Authorization URL",
|
|
accessTokenUrl: "Access Token URL",
|
|
clientId: "Client ID",
|
|
clientSecret: "Client Secret",
|
|
redirectUrl: "Redirect URL",
|
|
username: "Username",
|
|
password: "Password",
|
|
scope: "Scope",
|
|
state: "State",
|
|
tokenPrefix: "Header Prefix",
|
|
audience: "Audience",
|
|
resource: "Resource",
|
|
origin: "Origin",
|
|
};
|
|
|
|
// These 6 fields (plus the "Credentials" dropdown) only render once the
|
|
// Auth tab's "Advanced Options" accordion is expanded — see
|
|
// expandOAuth2AdvancedOptions().
|
|
const OAUTH2_ADVANCED_FIELDS: OAuth2TextField[] = [
|
|
"scope",
|
|
"state",
|
|
"tokenPrefix",
|
|
"audience",
|
|
"resource",
|
|
"origin",
|
|
];
|
|
|
|
export interface OAuth2Tokens {
|
|
refreshToken: string;
|
|
identityToken: string;
|
|
accessToken: string;
|
|
}
|
|
|
|
/**
|
|
* The tokens display/actions block ("Fetch Tokens"/"Refresh Token", "Clear
|
|
* OAuth 2 session") can take a few seconds to settle: Authorization Code
|
|
* and Implicit grants round-trip through a popup authorization window
|
|
* before the Access Token field populates.
|
|
*/
|
|
const OAUTH2_FETCH_TIMEOUT = ACTION_TIMEOUT;
|
|
|
|
// A request's auth lives on the Request document; a folder's own auth
|
|
// (inherited by its children) on the RequestGroup document.
|
|
const AUTH_DB_FILES = [
|
|
"insomnia.Request.db",
|
|
"insomnia.RequestGroup.db",
|
|
"insomnia.WebSocketRequest.db",
|
|
"insomnia.SocketIORequest.db",
|
|
"insomnia.McpRequest.db",
|
|
];
|
|
|
|
// How long an edit gets to land on its own before it's re-applied: the
|
|
// Auth editors save through a debounce, so a just-made edit legitimately
|
|
// isn't on disk yet.
|
|
const AUTH_REAPPLY_AFTER_MS = 2000;
|
|
|
|
// How long the full set of edits must keep holding on disk before the
|
|
// form counts as done — long enough for any still-pending debounced save
|
|
// built from a stale auth config to have fired and landed.
|
|
const AUTH_SETTLE_MS = 2000;
|
|
|
|
/** One edited `authentication` field, plus how to repeat the edit if it gets lost. */
|
|
interface AuthFieldEdit {
|
|
field: string;
|
|
value: unknown;
|
|
reapply?: () => Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* The Auth tab component, shared by any page that renders it — a
|
|
* request's own pane (`RequestPage`, composing an instance as
|
|
* `this.auth`) and a Folder's own settings tab (`FolderPage`, same).
|
|
* Constructed with the owning page's `pane` selector (its tab-bar's outer
|
|
* container), since that's the only thing that differs between them.
|
|
*/
|
|
export class AuthTabComponent extends TabPanelComponent {
|
|
protected get AUTH_TYPE_BUTTON(): string {
|
|
return `${this.PANE} [aria-label="Change Authentication type"]`;
|
|
}
|
|
|
|
/**
|
|
* Reads back the Refresh Token, Identity Token, and Access Token
|
|
* fields shown below the Auth tab's OAuth 2.0 form. Empty strings mean
|
|
* no token has been fetched yet.
|
|
*/
|
|
async getOAuth2Tokens(): Promise<OAuth2Tokens> {
|
|
await this.switchTab("auth");
|
|
return {
|
|
refreshToken: await this.oauth2TokenInput("Refresh-Token").inputValue(),
|
|
identityToken: await this.oauth2TokenInput("Identity-Token").inputValue(),
|
|
accessToken: await this.oauth2TokenInput("Access-Token").inputValue(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Expands the OAuth 2.0 Auth tab's "Advanced Options" accordion
|
|
* (Scope/State/Credentials/Header Prefix/Audience/Resource/Origin,
|
|
* plus the "Clear OAuth 2 session" button) if it isn't already open.
|
|
* A no-op if some other auth type is selected.
|
|
*/
|
|
private async expandOAuth2AdvancedOptions(): Promise<void> {
|
|
const panel = this.page.locator(this.TABPANEL);
|
|
if (await this.fieldByLabel("Scope").count()) return;
|
|
const button = panel.getByRole("button", { name: "Advanced Options" });
|
|
if ((await button.count()) === 0) return;
|
|
await button.click();
|
|
await expect(
|
|
panel.getByTestId("auth-field-Scope").locator(".CodeMirror"),
|
|
).toBeVisible({
|
|
timeout: DEFAULT_TIMEOUT,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clicks the "Clear" button next to "Refresh Token"/"Fetch Tokens" —
|
|
* only rendered once at least one token has been fetched — wiping the
|
|
* Refresh/Identity/Access Token fields. Not to be confused with
|
|
* "Advanced Options"' own "Clear OAuth 2 session" button, which resets
|
|
* the embedded authorization popup's own browser session (cookies),
|
|
* not the app's stored tokens.
|
|
*/
|
|
async clearOAuth2Tokens(): Promise<void> {
|
|
await this.switchTab("auth");
|
|
const clearButton = this.page
|
|
.locator(this.TABPANEL)
|
|
.getByRole("button", { name: "Clear", exact: true });
|
|
await clearButton.click();
|
|
await expect(clearButton).toBeHidden({ timeout: DEFAULT_TIMEOUT });
|
|
}
|
|
|
|
/**
|
|
* Clicks the Auth tab's token-fetch button — labeled "Fetch Tokens"
|
|
* before any token exists, "Refresh Token" once one does — and waits
|
|
* for the Access Token field to be populated. For the Authorization
|
|
* Code and Implicit grants this drives a real popup authorization
|
|
* window (auto-approved by `misc/oauth2-server.ts`, no interaction
|
|
* needed here); for Client Credentials/Resource Owner Password/an
|
|
* existing Refresh Token it resolves directly against the token
|
|
* endpoint with no popup at all.
|
|
*/
|
|
async fetchOAuth2Tokens(): Promise<void> {
|
|
await this.switchTab("auth");
|
|
const panel = this.page.locator(this.TABPANEL);
|
|
// The button acts on the auth config in the request's loader data,
|
|
// which refreshes a moment after the last edit lands on disk — drain
|
|
// the renderer first so a fetch can't go out with the previous config.
|
|
await this.waitForRendererIdle();
|
|
await panel
|
|
.getByRole("button", { name: /^(Fetch Tokens|Refresh Token)$/ })
|
|
.click();
|
|
await expect(this.oauth2TokenInput("Access-Token")).not.toHaveValue("", {
|
|
timeout: OAUTH2_FETCH_TIMEOUT,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Switches to the Auth tab, opens the auth-type dropdown, and selects
|
|
* the given type. Waits for the Auth Type button to reflect the
|
|
* selection before returning, so a dropdown option that renders slow
|
|
* (e.g. under heavy parallel load) fails fast here instead of a much
|
|
* later, confusing timeout on a field the new auth type never
|
|
* rendered.
|
|
*
|
|
* Retries the whole open-dropdown/click-option sequence (not just the
|
|
* read-back check) on each attempt: under heavy parallel load the
|
|
* option click can land while the listbox is present in the DOM but
|
|
* not yet actually wired up, so it's silently swallowed and the button
|
|
* text never changes no matter how long we wait on that same click -
|
|
* only redoing the click fixes it.
|
|
*/
|
|
async setAuthType(type: AuthType): Promise<void> {
|
|
await this.switchTab("auth");
|
|
const button = this.page.locator(this.AUTH_TYPE_BUTTON);
|
|
|
|
await expect(async () => {
|
|
await this.page.keyboard.press("Escape");
|
|
await button.click();
|
|
await this.page.locator(`[role="option"][aria-label="${type}"]`).click();
|
|
await expect(button).toContainText(type, { timeout: 2000 });
|
|
}).toPass({ timeout: DEFAULT_TIMEOUT });
|
|
}
|
|
|
|
/**
|
|
* Sets the Bearer Token's token, switching the auth type to Bearer
|
|
* Token first.
|
|
*/
|
|
async setBearerFields(fields: Partial<AuthTypeBearer>): Promise<void> {
|
|
await this.setAuthType(AuthType.Bearer);
|
|
await this.setTokenField(fields.token);
|
|
}
|
|
|
|
/**
|
|
* Sets the single token's value, switching the auth type to the
|
|
* single-token type (Socket.IO's only auth option) first.
|
|
*/
|
|
async setSingleTokenFields(
|
|
fields: Partial<AuthTypeSingleToken>,
|
|
): Promise<void> {
|
|
await this.setAuthType(AuthType.Token);
|
|
await this.setTokenField(fields.token);
|
|
}
|
|
|
|
/**
|
|
* Switches the auth type to None and waits for it to be saved.
|
|
*/
|
|
async setNoAuthentication(): Promise<void> {
|
|
await this.setAuthType(AuthType.None);
|
|
await this.waitForAuthFieldsPersisted(
|
|
[{ field: "type", value: "none" }],
|
|
AUTH_SETTLE_MS,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sets the Basic auth username and password, switching the auth type
|
|
* to Basic first.
|
|
*/
|
|
async setBasicFields(fields: Partial<AuthTypeBasic>): Promise<void> {
|
|
await this.setAuthType(AuthType.Basic);
|
|
await this.setAuthTextFields([
|
|
{ field: "username", label: "Username", value: fields.username },
|
|
{ field: "password", label: "Password", value: fields.password },
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Sets the API Key auth key and value, switching the auth type to
|
|
* API Key first. The "Add to" select is left at its default (Header).
|
|
*/
|
|
async setApiKeyFields(fields: Partial<AuthTypeAPIKey>): Promise<void> {
|
|
await this.setAuthType(AuthType.ApiKey);
|
|
await this.setAuthTextFields([
|
|
{ field: "key", label: "Key", value: fields.key },
|
|
{ field: "value", label: "Value", value: fields.value },
|
|
]);
|
|
}
|
|
|
|
private async setTokenField(token: string | undefined): Promise<void> {
|
|
await this.setAuthTextFields([
|
|
{ field: "token", label: "Token", value: token },
|
|
]);
|
|
}
|
|
|
|
private async setAuthTextFields(
|
|
fields: { field: string; label: string; value: string | undefined }[],
|
|
): Promise<void> {
|
|
const edits: AuthFieldEdit[] = [];
|
|
for (const { field, label, value } of fields) {
|
|
if (value === undefined) continue;
|
|
const editor = this.authFieldEditor(label);
|
|
await expect(editor).toBeVisible({ timeout: DEFAULT_TIMEOUT });
|
|
const edit: AuthFieldEdit = {
|
|
field,
|
|
value,
|
|
reapply: () => this.setCodeMirrorValue(editor, value),
|
|
};
|
|
await edit.reapply!();
|
|
await this.waitForAuthFieldsPersisted([...edits, edit]);
|
|
edits.push(edit);
|
|
}
|
|
await this.waitForAuthFieldsPersisted(edits, AUTH_SETTLE_MS);
|
|
}
|
|
|
|
/**
|
|
* Sets the given OAuth 1.0 fields, switching the auth type to OAuth
|
|
* 1.0 first. Fields left undefined are not written, leaving whatever
|
|
* value is already there.
|
|
*/
|
|
async setOAuth1Fields(fields: Partial<AuthTypeOAuth1>): Promise<void> {
|
|
await this.setAuthType(AuthType.OAuth1);
|
|
await expect(
|
|
this.oAuth1FieldEditor(OAUTH1_FIELD_LABELS.consumerKey),
|
|
).toBeVisible({ timeout: DEFAULT_TIMEOUT });
|
|
const edits: AuthFieldEdit[] = [];
|
|
for (const [field, label] of Object.entries(OAUTH1_FIELD_LABELS)) {
|
|
const value = fields[field as OAuth1TextField];
|
|
if (value === undefined) continue;
|
|
const editor = this.oAuth1FieldEditor(label);
|
|
const edit: AuthFieldEdit = {
|
|
field,
|
|
value,
|
|
reapply: () => this.setCodeMirrorValue(editor, value),
|
|
};
|
|
await edit.reapply!();
|
|
// Verify every field written so far, not just this one: its save can
|
|
// overwrite an earlier field from a stale copy of the auth config.
|
|
await this.waitForAuthFieldsPersisted([...edits, edit]);
|
|
edits.push(edit);
|
|
}
|
|
if (fields.signatureMethod) {
|
|
const signatureMethod = fields.signatureMethod;
|
|
const selectMethod = async () => {
|
|
await this.fieldByLabel("Signature-Method").selectOption(signatureMethod);
|
|
};
|
|
await selectMethod();
|
|
const edit: AuthFieldEdit = {
|
|
field: "signatureMethod",
|
|
value: signatureMethod,
|
|
reapply: selectMethod,
|
|
};
|
|
await this.waitForAuthFieldsPersisted([...edits, edit]);
|
|
edits.push(edit);
|
|
}
|
|
// A later field's debounced save can overwrite an earlier one from a
|
|
// stale copy of the auth config, so confirm they all hold together.
|
|
await this.waitForAuthFieldsPersisted(edits, AUTH_SETTLE_MS);
|
|
}
|
|
|
|
/**
|
|
* Sets the given OAuth 2.0 fields, switching the auth type to OAuth
|
|
* 2.0 first (and the grant type, if given, before anything else since
|
|
* it controls which other rows even render). Fields left undefined
|
|
* are not written. Advanced-Options-only fields (`scope`/`state`/
|
|
* `tokenPrefix`/`audience`/`resource`/`origin`/`credentialsInBody`/
|
|
* `responseType`) automatically expand that accordion first. A field
|
|
* with no matching row for the currently selected grant type (e.g.
|
|
* `username` under Client Credentials) is silently skipped rather
|
|
* than hanging.
|
|
*/
|
|
async setOAuth2Fields(fields: Partial<AuthTypeOAuth2>): Promise<void> {
|
|
await this.setAuthType(AuthType.OAuth2);
|
|
await expect(this.fieldByLabel("Grant-Type")).toBeVisible({
|
|
timeout: DEFAULT_TIMEOUT,
|
|
});
|
|
|
|
const edits: AuthFieldEdit[] = [];
|
|
const selectOnTab = async (id: string, value: string) => {
|
|
await this.fieldByLabel(id).selectOption(value);
|
|
};
|
|
const applySelect = async (
|
|
field: string,
|
|
id: string,
|
|
value: string,
|
|
stored: unknown = value,
|
|
) => {
|
|
const edit: AuthFieldEdit = {
|
|
field,
|
|
value: stored,
|
|
reapply: () => selectOnTab(id, value),
|
|
};
|
|
await edit.reapply!();
|
|
await this.waitForAuthFieldsPersisted([edit]);
|
|
edits.push(edit);
|
|
};
|
|
|
|
if (fields.grantType) {
|
|
await applySelect("grantType", "Grant-Type", fields.grantType);
|
|
// Changing the grant type re-renders the form's rows and refreshes
|
|
// loader data; let any stale debounced save land and the editors
|
|
// settle first, or the text fields written next get reverted by it.
|
|
await this.waitForAuthFieldsPersisted(edits, AUTH_SETTLE_MS);
|
|
await this.waitForRendererIdle();
|
|
}
|
|
|
|
const needsAdvanced =
|
|
fields.credentialsInBody !== undefined ||
|
|
fields.responseType !== undefined ||
|
|
OAUTH2_ADVANCED_FIELDS.some((field) => fields[field] !== undefined);
|
|
if (needsAdvanced) await this.expandOAuth2AdvancedOptions();
|
|
|
|
for (const [field, label] of Object.entries(OAUTH2_FIELD_LABELS)) {
|
|
const value = fields[field as OAuth2TextField];
|
|
if (value === undefined) continue;
|
|
const editor = this.oauth2FieldEditor(label);
|
|
if ((await editor.count()) === 0) continue;
|
|
const edit: AuthFieldEdit = {
|
|
field,
|
|
value,
|
|
reapply: () => this.setCodeMirrorValue(editor, value),
|
|
};
|
|
await edit.reapply!();
|
|
await this.waitForAuthFieldsPersisted([...edits, edit]);
|
|
edits.push(edit);
|
|
}
|
|
|
|
if (fields.usePkce !== undefined) {
|
|
await this.setToggle("Use-PKCE", fields.usePkce);
|
|
if (fields.usePkce && fields.pkceMethod) {
|
|
await applySelect("pkceMethod", "Code-Challenge-Method", fields.pkceMethod);
|
|
}
|
|
}
|
|
if (fields.useDefaultBrowser !== undefined) {
|
|
await this.setToggle("Using-default-browser", fields.useDefaultBrowser);
|
|
}
|
|
if (
|
|
fields.responseType &&
|
|
(await this.fieldByLabel("Response-Type").count())
|
|
) {
|
|
await applySelect("responseType", "Response-Type", fields.responseType);
|
|
}
|
|
if (fields.credentialsInBody !== undefined) {
|
|
await applySelect(
|
|
"credentialsInBody",
|
|
"Credentials",
|
|
fields.credentialsInBody ? "true" : "false",
|
|
fields.credentialsInBody,
|
|
);
|
|
}
|
|
|
|
// A later edit's debounced save can be built from a stale copy of the
|
|
// auth config and silently drop an earlier field — each per-field wait
|
|
// above passed at the time — so confirm every field holds together.
|
|
await this.waitForAuthFieldsPersisted(edits, AUTH_SETTLE_MS);
|
|
}
|
|
|
|
private toggle(id: string): ToggleButtonComponent {
|
|
return new ToggleButtonComponent(this.page, this.fieldByLabel(id));
|
|
}
|
|
|
|
private async setToggle(id: string, enabled: boolean): Promise<void> {
|
|
await this.toggle(id).set(enabled);
|
|
}
|
|
|
|
/**
|
|
* Polls the on-disk request/folder documents until one carries
|
|
* `authentication[field] === value`, confirming the edit has actually
|
|
* been saved (the editors autosave through a debounce) rather than just
|
|
* shown in the UI.
|
|
*/
|
|
private async waitForAuthFieldPersisted(
|
|
field: string,
|
|
value: unknown,
|
|
): Promise<void> {
|
|
await this.waitForAuthFieldsPersisted([{ field, value }]);
|
|
}
|
|
|
|
/**
|
|
* Polls the on-disk request/folder documents until a single one holds
|
|
* every edit in `edits` at once. The app's debounced editors can save
|
|
* from a stale copy of the auth config, so a write may never land, or
|
|
* may drop a field persisted earlier; once an edit has had
|
|
* `AUTH_REAPPLY_AFTER_MS` to land on its own, the edits still missing
|
|
* from the closest-matching document are re-applied.
|
|
*/
|
|
private async waitForAuthFieldsPersisted(
|
|
edits: AuthFieldEdit[],
|
|
settleMs = 0,
|
|
): Promise<void> {
|
|
const start = Date.now();
|
|
let lastReapply = start;
|
|
let heldSince = 0;
|
|
await expect(async () => {
|
|
const docs = (
|
|
await Promise.all(AUTH_DB_FILES.map((file) => this.readPersistedDocs(file)))
|
|
).flat();
|
|
let missing = edits;
|
|
for (const doc of docs) {
|
|
const docMissing = edits.filter(
|
|
(e) => String(doc.authentication?.[e.field]) !== String(e.value),
|
|
);
|
|
if (docMissing.length < missing.length) missing = docMissing;
|
|
}
|
|
if (missing.length === 0) {
|
|
heldSince ||= Date.now();
|
|
expect(Date.now() - heldSince).toBeGreaterThanOrEqual(settleMs);
|
|
return;
|
|
}
|
|
heldSince = 0;
|
|
if (Date.now() - lastReapply >= AUTH_REAPPLY_AFTER_MS) {
|
|
lastReapply = Date.now();
|
|
for (const edit of missing) await edit.reapply?.();
|
|
}
|
|
expect(missing.map((e) => e.field)).toEqual([]);
|
|
}).toPass({ timeout: DEFAULT_TIMEOUT + settleMs, intervals: [250, 500] });
|
|
}
|
|
|
|
/**
|
|
* Builds the locator for a labelled Auth form control (a select or a
|
|
* toggle button) from its row's label, given in the row's id form
|
|
* (`Grant-Type` -> the "Grant Type" label). The app links each row's
|
|
* `<label>` to its control via `for`/`id`, so no `#id` selector is needed.
|
|
*/
|
|
private fieldByLabel(id: string): Locator {
|
|
return this.page
|
|
.locator(this.TABPANEL)
|
|
.getByLabel(id.replace(/-/g, " "), { exact: true });
|
|
}
|
|
|
|
/**
|
|
* Builds the locator for an OAuth 1.0 field's CodeMirror editor,
|
|
* identified by the row's visible label (e.g. "Consumer Key" -> the
|
|
* "consumer-key"-id textarea CodeMirror replaces).
|
|
*/
|
|
private oAuth1FieldEditor(label: string): Locator {
|
|
return this.authFieldEditor(label);
|
|
}
|
|
|
|
/**
|
|
* Builds the locator for an OAuth 2.0 field's CodeMirror editor,
|
|
* identified by the row's visible label (e.g. "Client ID" -> the
|
|
* "Client-ID"-id textarea CodeMirror replaces). Resolves to an empty
|
|
* locator (`.count()` is 0) when the field's row isn't rendered for
|
|
* the currently selected grant type or accordion state.
|
|
*/
|
|
private oauth2FieldEditor(label: string): Locator {
|
|
return this.authFieldEditor(label);
|
|
}
|
|
|
|
/**
|
|
* Builds the locator for the CodeMirror editor inside an Auth form row,
|
|
* given the row's visible label (e.g. "Client ID" -> the row with
|
|
* `data-testid="auth-field-Client-ID"`).
|
|
*/
|
|
private authFieldEditor(label: string): Locator {
|
|
return this.page
|
|
.locator(this.TABPANEL)
|
|
.getByTestId(`auth-field-${label.replace(/ /g, "-")}`)
|
|
.locator(".CodeMirror");
|
|
}
|
|
|
|
/**
|
|
* Builds the locator for one of the read-only Refresh/Identity/Access
|
|
* Token inputs (`data-testid="auth-field-Access-Token"` and so on).
|
|
*/
|
|
private oauth2TokenInput(id: string): Locator {
|
|
return this.page.locator(this.TABPANEL).getByTestId(`auth-field-${id}`);
|
|
}
|
|
}
|