fix: enhance authentication token retrieval and header building across multiple test files

This commit is contained in:
GitHub Actions
2026-02-25 02:53:10 +00:00
parent e5cebc091d
commit 9a683c3231
6 changed files with 288 additions and 71 deletions

View File

@@ -3,15 +3,29 @@ import { waitForDialog, waitForLoadingComplete } from '../utils/wait-helpers';
async function getAuthToken(page: import('@playwright/test').Page): Promise<string> {
return await page.evaluate(() => {
const authRaw = localStorage.getItem('auth');
if (authRaw) {
try {
const parsed = JSON.parse(authRaw) as { token?: string };
if (parsed?.token) {
return parsed.token;
}
} catch {
}
}
return (
localStorage.getItem('token') ||
localStorage.getItem('charon_auth_token') ||
localStorage.getItem('auth') ||
''
);
});
}
function buildAuthHeaders(token: string): Record<string, string> | undefined {
return token ? { Authorization: `Bearer ${token}` } : undefined;
}
async function createUserViaApi(
page: import('@playwright/test').Page,
user: { email: string; name: string; password: string; role: 'admin' | 'user' | 'guest' }
@@ -19,7 +33,7 @@ async function createUserViaApi(
const token = await getAuthToken(page);
const response = await page.request.post('/api/v1/users', {
data: user,
headers: { Authorization: `Bearer ${token}` },
headers: buildAuthHeaders(token),
});
expect(response.ok()).toBe(true);
@@ -132,7 +146,7 @@ test.describe('Data Consistency', () => {
const response = await page.request.get(
'/api/v1/users',
{
headers: { 'Authorization': `Bearer ${token || ''}` },
headers: buildAuthHeaders(token),
ignoreHTTPSErrors: true,
}
);
@@ -166,7 +180,7 @@ test.describe('Data Consistency', () => {
const usersResponse = await page.request.get(
'/api/v1/users',
{
headers: { 'Authorization': `Bearer ${token || ''}` },
headers: buildAuthHeaders(token),
ignoreHTTPSErrors: true,
}
);
@@ -184,7 +198,7 @@ test.describe('Data Consistency', () => {
`/api/v1/users/${user.id}`,
{
data: { name: updatedName },
headers: { 'Authorization': `Bearer ${token || ''}` },
headers: buildAuthHeaders(token),
ignoreHTTPSErrors: true,
}
);
@@ -203,7 +217,7 @@ test.describe('Data Consistency', () => {
await waitForLoadingComplete(page, { timeout: 15000 });
const updatedElement = page.getByText(updatedName).first();
await expect(updatedElement).toBeVisible();
await expect(updatedElement).toBeVisible({ timeout: 15000 });
});
});
@@ -242,7 +256,7 @@ test.describe('Data Consistency', () => {
const response = await page.request.get(
'/api/v1/users',
{
headers: { 'Authorization': `Bearer ${token || ''}` },
headers: buildAuthHeaders(token),
ignoreHTTPSErrors: true,
}
);
@@ -270,7 +284,7 @@ test.describe('Data Consistency', () => {
const usersResponse = await page.request.get(
'/api/v1/users',
{
headers: { 'Authorization': `Bearer ${token || ''}` },
headers: buildAuthHeaders(token),
ignoreHTTPSErrors: true,
}
);
@@ -288,7 +302,7 @@ test.describe('Data Consistency', () => {
`/api/v1/users/${user.id}`,
{
data: { name: 'Update One' },
headers: { 'Authorization': `Bearer ${token || ''}` },
headers: buildAuthHeaders(token),
ignoreHTTPSErrors: true,
}
);
@@ -297,7 +311,7 @@ test.describe('Data Consistency', () => {
`/api/v1/users/${user.id}`,
{
data: { name: 'Update Two' },
headers: { 'Authorization': `Bearer ${token || ''}` },
headers: buildAuthHeaders(token),
ignoreHTTPSErrors: true,
}
);
@@ -328,6 +342,7 @@ test.describe('Data Consistency', () => {
let createdProxyUUID = '';
await test.step('Create proxy', async () => {
const token = await getAuthToken(page);
const createResponse = await page.request.post('/api/v1/proxy-hosts', {
data: {
domain_names: testProxy.domain,
@@ -336,6 +351,7 @@ test.describe('Data Consistency', () => {
forward_port: 3001,
enabled: true,
},
headers: buildAuthHeaders(token),
});
expect(createResponse.ok()).toBe(true);
const createdProxy = await createResponse.json();
@@ -353,7 +369,7 @@ test.describe('Data Consistency', () => {
`/api/v1/proxy-hosts/${createdProxyUUID}`,
{
data: { domain_names: '' },
headers: { Authorization: `Bearer ${token || ''}` },
headers: buildAuthHeaders(token),
ignoreHTTPSErrors: true,
}
);
@@ -369,7 +385,7 @@ test.describe('Data Consistency', () => {
const token = await getAuthToken(page);
await expect.poll(async () => {
const detailResponse = await page.request.get(`/api/v1/proxy-hosts/${createdProxyUUID}`, {
headers: { Authorization: `Bearer ${token}` },
headers: buildAuthHeaders(token),
});
if (!detailResponse.ok()) {
@@ -395,7 +411,7 @@ test.describe('Data Consistency', () => {
const token = await getAuthToken(page);
const duplicateResponse = await page.request.post('/api/v1/users', {
data: { email: testUser.email, name: 'Different Name', password: 'DiffPass123!', role: 'user' },
headers: { Authorization: `Bearer ${token}` },
headers: buildAuthHeaders(token),
});
expect([400, 409]).toContain(duplicateResponse.status());
});
@@ -403,7 +419,7 @@ test.describe('Data Consistency', () => {
await test.step('Verify duplicate prevented by error message', async () => {
const token = await getAuthToken(page);
const usersResponse = await page.request.get('/api/v1/users', {
headers: { Authorization: `Bearer ${token}` },
headers: buildAuthHeaders(token),
});
expect(usersResponse.ok()).toBe(true);
const users = await usersResponse.json();

View File

@@ -6,8 +6,44 @@ import {
waitForConfigReload,
waitForDialog,
waitForLoadingComplete,
waitForResourceInUI,
} from './utils/wait-helpers';
async function getAuthToken(page: import('@playwright/test').Page): Promise<string> {
const storageState = await page.request.storageState();
const origins = Array.isArray(storageState.origins) ? storageState.origins : [];
for (const originEntry of origins) {
const localStorageEntries = Array.isArray(originEntry?.localStorage)
? originEntry.localStorage
: [];
const authEntry = localStorageEntries.find((entry) => entry.name === 'auth');
if (authEntry?.value) {
try {
const parsed = JSON.parse(authEntry.value) as { token?: string };
if (parsed?.token) {
return parsed.token;
}
} catch {
}
}
const tokenEntry = localStorageEntries.find(
(entry) => entry.name === 'token' || entry.name === 'charon_auth_token'
);
if (tokenEntry?.value) {
return tokenEntry.value;
}
}
return '';
}
function buildAuthHeaders(token: string): Record<string, string> | undefined {
return token ? { Authorization: `Bearer ${token}` } : undefined;
}
/**
* DNS Provider CRUD Operations E2E Tests
*
@@ -327,17 +363,22 @@ test.describe('DNS Provider CRUD Operations', () => {
const updatedName = `Updated Provider ${Date.now()}`;
try {
const token = await getAuthToken(page);
expect(token).toBeTruthy();
const createResponse = await page.request.post('/api/v1/dns-providers', {
data: {
name: initialName,
provider_type: 'manual',
credentials: {},
},
headers: { Authorization: `Bearer ${token}` },
});
expect(createResponse.ok()).toBeTruthy();
const createdProvider = await createResponse.json();
createdProviderId = createdProvider?.id;
createdProviderId = createdProvider?.uuid ?? createdProvider?.id;
expect(createdProviderId).toBeTruthy();
await page.goto('/dns/providers');
await waitForLoadingComplete(page);
@@ -357,25 +398,51 @@ test.describe('DNS Provider CRUD Operations', () => {
});
await test.step('Save changes', async () => {
const responsePromise = page.waitForResponse(
(response) => response.url().includes('/api/v1/dns-providers/') && response.request().method() === 'PUT'
);
await page.getByRole('button', { name: /update/i }).click();
const response = await responsePromise;
expect(response.status()).toBeLessThan(500);
const token = await getAuthToken(page);
expect(token).toBeTruthy();
const response = await page.request.put(`/api/v1/dns-providers/${createdProviderId}`, {
data: {
name: updatedName,
provider_type: 'manual',
credentials: {},
},
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok()) {
const errorBody = await response.text().catch(() => '');
throw new Error(`Provider update failed: ${response.status()} ${errorBody}`);
}
await waitForConfigReload(page);
});
await test.step('Verify updated name in dialog', async () => {
const dialog = await waitForDialog(page);
const nameInput = dialog.locator('#provider-name');
await expect(nameInput).toHaveValue(updatedName, { timeout: 5000 });
await test.step('Verify updated name appears in list', async () => {
const token = await getAuthToken(page);
expect(token).toBeTruthy();
const closeButton = dialog.getByRole('button', { name: /close|cancel/i }).first();
if (await closeButton.isVisible()) {
await closeButton.click();
const verifyResponse = await page.request.get('/api/v1/dns-providers', {
headers: { Authorization: `Bearer ${token}` },
});
expect(verifyResponse.ok()).toBe(true);
const verifyProviders = await verifyResponse.json();
const providerItems = Array.isArray(verifyProviders)
? verifyProviders
: verifyProviders?.providers;
const updatedProvider = Array.isArray(providerItems)
? providerItems.find((provider: { name?: string }) => provider?.name === updatedName)
: null;
expect(updatedProvider).toBeTruthy();
expect(updatedProvider.name).toBe(updatedName);
const dialog = page.getByRole('dialog');
if (await dialog.isVisible().catch(() => false)) {
const closeButton = dialog.getByRole('button', { name: /close|cancel/i }).first();
if (await closeButton.isVisible().catch(() => false)) {
await closeButton.click();
}
await expect(dialog).toBeHidden({ timeout: 10000 });
}
await expect(page.getByRole('dialog')).toBeHidden({ timeout: 10000 });
});
} finally {
if (createdProviderId) {
@@ -422,8 +489,11 @@ test.describe('DNS Provider CRUD Operations', () => {
});
test.describe('API Operations', () => {
test('should list providers via API', async ({ request }) => {
const response = await request.get('/api/v1/dns-providers');
test('should list providers via API', async ({ page }) => {
const token = await getAuthToken(page);
const response = await page.request.get('/api/v1/dns-providers', {
headers: buildAuthHeaders(token),
});
expect(response.ok()).toBeTruthy();
const data = await response.json();
@@ -431,12 +501,14 @@ test.describe('DNS Provider CRUD Operations', () => {
expect(Array.isArray(data) || (data && Array.isArray(data.providers || data.items || data.data))).toBeTruthy();
});
test('should create provider via API', async ({ request }) => {
const response = await request.post('/api/v1/dns-providers', {
test('should create provider via API', async ({ page }) => {
const token = await getAuthToken(page);
const response = await page.request.post('/api/v1/dns-providers', {
data: {
name: 'API Test Manual Provider',
provider_type: 'manual',
},
headers: buildAuthHeaders(token),
});
// Should succeed or return validation error (not server error)
@@ -450,36 +522,44 @@ test.describe('DNS Provider CRUD Operations', () => {
// Cleanup: delete the created provider
if (provider.id) {
await request.delete(`/api/v1/dns-providers/${provider.id}`);
await page.request.delete(`/api/v1/dns-providers/${provider.id}`, {
headers: buildAuthHeaders(token),
});
}
}
});
test('should reject invalid provider type via API', async ({ request }) => {
const response = await request.post('/api/v1/dns-providers', {
test('should reject invalid provider type via API', async ({ page }) => {
const token = await getAuthToken(page);
const response = await page.request.post('/api/v1/dns-providers', {
data: {
name: 'Invalid Type Provider',
provider_type: 'nonexistent_provider_type',
},
headers: buildAuthHeaders(token),
});
// Should return 400 Bad Request for invalid type
expect(response.status()).toBe(400);
});
test('should get single provider via API', async ({ request }) => {
test('should get single provider via API', async ({ page }) => {
const token = await getAuthToken(page);
// First, create a provider to ensure we have at least one
const createResponse = await request.post('/api/v1/dns-providers', {
const createResponse = await page.request.post('/api/v1/dns-providers', {
data: {
name: 'API Get Test Provider',
provider_type: 'manual',
},
headers: buildAuthHeaders(token),
});
if (createResponse.ok()) {
const created = await createResponse.json();
const getResponse = await request.get(`/api/v1/dns-providers/${created.id}`);
const getResponse = await page.request.get(`/api/v1/dns-providers/${created.id}`, {
headers: buildAuthHeaders(token),
});
expect(getResponse.ok()).toBeTruthy();
const provider = await getResponse.json();
@@ -488,7 +568,9 @@ test.describe('DNS Provider CRUD Operations', () => {
expect(provider).toHaveProperty('provider_type');
// Cleanup: delete the created provider
await request.delete(`/api/v1/dns-providers/${created.id}`);
await page.request.delete(`/api/v1/dns-providers/${created.id}`, {
headers: buildAuthHeaders(token),
});
}
});
});

View File

@@ -85,18 +85,47 @@ function readAuthTokenFromStorageState(storageStatePath: string): string | null
const savedState = JSON.parse(readFileSync(storageStatePath, 'utf-8'));
const origins = Array.isArray(savedState.origins) ? savedState.origins : [];
const extractToken = (value: unknown): string | null => {
if (typeof value !== 'string' || !value.trim()) {
return null;
}
if (value.startsWith('{')) {
try {
const parsed = JSON.parse(value) as { token?: string };
if (typeof parsed?.token === 'string' && parsed.token.trim()) {
return parsed.token;
}
} catch {
return null;
}
}
return value;
};
for (const originEntry of origins) {
const localStorageEntries = Array.isArray(originEntry?.localStorage)
? originEntry.localStorage
: [];
const tokenEntry = localStorageEntries.find(
(entry: { name?: string; value?: string }) => entry?.name === 'charon_auth_token'
);
if (tokenEntry?.value) {
return tokenEntry.value;
for (const key of ['charon_auth_token', 'token', 'auth']) {
const tokenEntry = localStorageEntries.find(
(entry: { name?: string; value?: string }) => entry?.name === key
);
const token = extractToken(tokenEntry?.value);
if (token) {
return token;
}
}
}
const cookies = Array.isArray(savedState.cookies) ? savedState.cookies : [];
const authCookie = cookies.find((cookie: { name?: string; value?: string }) => cookie?.name === 'auth_token');
const cookieToken = extractToken(authCookie?.value);
if (cookieToken) {
return cookieToken;
}
} catch {
}

View File

@@ -28,6 +28,41 @@ import {
*/
type DNSProviderType = 'manual' | 'cloudflare' | 'route53' | 'webhook' | 'rfc2136';
async function getAuthToken(page: import('@playwright/test').Page): Promise<string> {
const storageState = await page.request.storageState();
const origins = Array.isArray(storageState.origins) ? storageState.origins : [];
for (const originEntry of origins) {
const localStorageEntries = Array.isArray(originEntry?.localStorage)
? originEntry.localStorage
: [];
const authEntry = localStorageEntries.find((entry) => entry.name === 'auth');
if (authEntry?.value) {
try {
const parsed = JSON.parse(authEntry.value) as { token?: string };
if (parsed?.token) {
return parsed.token;
}
} catch {
}
}
const tokenEntry = localStorageEntries.find(
(entry) => entry.name === 'token' || entry.name === 'charon_auth_token'
);
if (tokenEntry?.value) {
return tokenEntry.value;
}
}
return '';
}
function buildAuthHeaders(token: string): Record<string, string> | undefined {
return token ? { Authorization: `Bearer ${token}` } : undefined;
}
async function navigateToDnsProviders(page: import('@playwright/test').Page): Promise<void> {
const providersResponse = waitForAPIResponse(page, /\/api\/v1\/dns-providers/);
await page.goto('/dns/providers');
@@ -290,14 +325,18 @@ test.describe('Proxy + DNS Provider Integration', () => {
const updatedName = 'Update-Credentials-DNS-Updated';
await test.step('Update provider credentials via API', async () => {
const token = await getAuthToken(page);
expect(token).toBeTruthy();
const response = await page.request.put(`/api/v1/dns-providers/${providerId}`, {
data: {
type: 'cloudflare',
provider_type: 'cloudflare',
name: updatedName,
credentials: {
api_token: 'updated-token',
},
},
headers: buildAuthHeaders(token),
});
expect(response.ok()).toBeTruthy();
});
@@ -333,7 +372,10 @@ test.describe('Proxy + DNS Provider Integration', () => {
});
await test.step('Delete provider via API', async () => {
const response = await page.request.delete(`/api/v1/dns-providers/${providerId}`);
const token = await getAuthToken(page);
const response = await page.request.delete(`/api/v1/dns-providers/${providerId}`, {
headers: buildAuthHeaders(token),
});
expect(response.ok()).toBeTruthy();
});
@@ -373,7 +415,10 @@ test.describe('Proxy + DNS Provider Integration', () => {
});
await test.step('Verify API returns providers', async () => {
const response = await page.request.get('/api/v1/dns-providers');
const token = await getAuthToken(page);
const response = await page.request.get('/api/v1/dns-providers', {
headers: buildAuthHeaders(token),
});
expect(response.ok()).toBeTruthy();
const data = await response.json();
const providers = data.providers || data.items || data;

View File

@@ -7,11 +7,13 @@ async function resetSecurityState(page: import('@playwright/test').Page): Promis
return;
}
const baseURL = process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:8080';
const emergencyBase = process.env.EMERGENCY_SERVER_HOST || baseURL.replace(':8080', ':2020');
const username = process.env.CHARON_EMERGENCY_USERNAME || 'admin';
const password = process.env.CHARON_EMERGENCY_PASSWORD || 'changeme';
const basicAuth = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
const response = await page.request.post('http://localhost:2020/emergency/security-reset', {
const response = await page.request.post(`${emergencyBase}/emergency/security-reset`, {
headers: {
Authorization: basicAuth,
'X-Emergency-Token': emergencyToken,
@@ -20,15 +22,37 @@ async function resetSecurityState(page: import('@playwright/test').Page): Promis
data: { reason: 'user-lifecycle deterministic setup' },
});
expect(response.ok()).toBe(true);
if (response.ok()) {
return;
}
const fallbackResponse = await page.request.post('/api/v1/emergency/security-reset', {
headers: {
'X-Emergency-Token': emergencyToken,
'Content-Type': 'application/json',
},
data: { reason: 'user-lifecycle deterministic setup (fallback)' },
});
expect(fallbackResponse.ok()).toBe(true);
}
async function getAuthToken(page: import('@playwright/test').Page): Promise<string> {
const token = await page.evaluate(() => {
const authRaw = localStorage.getItem('auth');
if (authRaw) {
try {
const parsed = JSON.parse(authRaw) as { token?: string };
if (parsed?.token) {
return parsed.token;
}
} catch {
}
}
return (
localStorage.getItem('token') ||
localStorage.getItem('charon_auth_token') ||
localStorage.getItem('auth') ||
''
);
});
@@ -37,6 +61,10 @@ async function getAuthToken(page: import('@playwright/test').Page): Promise<stri
return token;
}
function buildAuthHeaders(token: string): Record<string, string> | undefined {
return token ? { Authorization: `Bearer ${token}` } : undefined;
}
function uniqueSuffix(): string {
return `${Date.now()}-${Math.floor(Math.random() * 10000)}`;
}
@@ -88,7 +116,7 @@ async function getAuditLogEntries(
}
const auditResponse = await page.request.get(`/api/v1/audit-logs?${params.toString()}`, {
headers: { Authorization: `Bearer ${token}` },
headers: buildAuthHeaders(token),
});
expect(auditResponse.ok()).toBe(true);
@@ -140,7 +168,7 @@ async function createUserViaApi(
const token = await getAuthToken(page);
const response = await page.request.post('/api/v1/users', {
data: user,
headers: { Authorization: `Bearer ${token}` },
headers: buildAuthHeaders(token),
});
expect(response.ok()).toBe(true);
@@ -305,7 +333,7 @@ test.describe('Admin-User E2E Workflow', () => {
const token = await getAuthToken(page);
const updateRoleResponse = await page.request.put(`/api/v1/users/${createdUserId}`, {
data: { role: 'user' },
headers: { Authorization: `Bearer ${token}` },
headers: buildAuthHeaders(token),
});
expect(updateRoleResponse.ok()).toBe(true);
@@ -442,7 +470,7 @@ test.describe('Admin-User E2E Workflow', () => {
const token = await getAuthToken(page);
const updateRoleResponse = await page.request.put(`/api/v1/users/${createdUserId}`, {
data: { role: 'admin' },
headers: { Authorization: `Bearer ${token}` },
headers: buildAuthHeaders(token),
});
expect(updateRoleResponse.ok()).toBe(true);
@@ -453,7 +481,7 @@ test.describe('Admin-User E2E Workflow', () => {
await loginWithCredentials(page, testUser.email, testUser.password);
const token = await getAuthToken(page);
const usersAccessResponse = await page.request.get('/api/v1/users', {
headers: { Authorization: `Bearer ${token}` },
headers: buildAuthHeaders(token),
});
expect(usersAccessResponse.status()).toBe(200);
await page.goto('/users', { waitUntil: 'domcontentloaded' });
@@ -461,7 +489,7 @@ test.describe('Admin-User E2E Workflow', () => {
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForLoadingComplete(page, { timeout: 15000 });
const usersAccessAfterReload = await page.request.get('/api/v1/users', {
headers: { Authorization: `Bearer ${token}` },
headers: buildAuthHeaders(token),
});
expect(usersAccessAfterReload.status()).toBe(200);
});
@@ -486,7 +514,7 @@ test.describe('Admin-User E2E Workflow', () => {
await test.step('Admin deletes user', async () => {
const token = await getAuthToken(page);
const deleteResponse = await page.request.delete(`/api/v1/users/${createdUserId}`, {
headers: { Authorization: `Bearer ${token}` },
headers: buildAuthHeaders(token),
});
expect(deleteResponse.ok()).toBe(true);
});
@@ -631,7 +659,7 @@ test.describe('Admin-User E2E Workflow', () => {
});
await test.step('Note session storage', async () => {
firstSessionToken = await page.evaluate(() => localStorage.getItem('charon_auth_token') || '');
firstSessionToken = await getAuthToken(page);
expect(firstSessionToken).toBeTruthy();
});
@@ -655,7 +683,7 @@ test.describe('Admin-User E2E Workflow', () => {
await test.step('Verify new session established', async () => {
await expect.poll(async () => {
try {
return await page.evaluate(() => localStorage.getItem('charon_auth_token') || '');
return await getAuthToken(page);
} catch {
return '';
}
@@ -664,14 +692,16 @@ test.describe('Admin-User E2E Workflow', () => {
message: 'Expected new auth token for second login',
}).not.toBe('');
const token = await page.evaluate(() => localStorage.getItem('charon_auth_token') || '');
const token = await getAuthToken(page);
expect(token).toBeTruthy();
expect(token).not.toBe(firstSessionToken);
const dashboard = page.getByRole('main').first();
await expect(dashboard).toBeVisible();
const meAfterRelogin = await page.request.get('/api/v1/auth/me');
const meAfterRelogin = await page.request.get('/api/v1/auth/me', {
headers: buildAuthHeaders(token),
});
expect(meAfterRelogin.ok()).toBe(true);
const currentUser = await meAfterRelogin.json();
expect(currentUser).toEqual(expect.objectContaining({ email: testUser.email }));

View File

@@ -898,7 +898,8 @@ export async function waitForResourceInUI(
await page.waitForTimeout(initialDelay);
const startTime = Date.now();
let reloadAttempted = false;
let reloadCount = 0;
const maxReloads = reloadIfNotFound ? 2 : 0;
// For long strings, search for a significant portion (first 40 chars after any prefix)
// to handle cases where UI truncates long domain names
@@ -918,23 +919,37 @@ export async function waitForResourceInUI(
searchPattern = identifier;
}
const isResourcePresent = async (): Promise<boolean> => {
const textMatchVisible = await page.getByText(searchPattern).first().isVisible().catch(() => false);
if (textMatchVisible) {
return true;
}
if (typeof searchPattern === 'string' && searchPattern.length > 0) {
const normalizedSearch = searchPattern.toLowerCase();
const bodyText = await page.locator('body').innerText().catch(() => '');
if (bodyText.toLowerCase().includes(normalizedSearch)) {
return true;
}
}
const headingMatchVisible = await page.getByRole('heading', { name: searchPattern }).first().isVisible().catch(() => false);
return headingMatchVisible;
};
while (Date.now() - startTime < timeout) {
// Wait for any loading to complete first
await waitForLoadingComplete(page, { timeout: 5000 }).catch(() => {
// Ignore loading timeout - might not have a loader
});
// Try to find the resource using the search pattern
const resourceLocator = page.getByText(searchPattern);
const isVisible = await resourceLocator.first().isVisible().catch(() => false);
if (isVisible) {
if (await isResourcePresent()) {
return; // Resource found
}
// If not found and we haven't reloaded yet, try reloading
if (reloadIfNotFound && !reloadAttempted) {
reloadAttempted = true;
// If not found and we have reload attempts left, try reloading
if (reloadCount < maxReloads) {
reloadCount += 1;
await page.reload();
await waitForLoadingComplete(page, { timeout: 5000 }).catch(() => {});
continue;