648 lines
24 KiB
JavaScript
648 lines
24 KiB
JavaScript
/**
|
|
* Playwright script to capture authenticated layout screenshots of StellaOps.
|
|
*
|
|
* Strategy:
|
|
* 1. Navigate to stella-ops.local
|
|
* 2. Try the real OAuth flow first (click Sign in, fill credentials)
|
|
* 3. If OAuth fails (Authority 500), inject mock auth state into Angular
|
|
* to force the authenticated shell layout with sidebar
|
|
* 4. Capture screenshots of various pages and states
|
|
*/
|
|
import { chromium } from 'playwright';
|
|
import { existsSync, mkdirSync } from 'fs';
|
|
import { join, dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
const SCREENSHOT_DIR = __dirname;
|
|
|
|
const BASE_URL = 'http://stella-ops.local';
|
|
const VIEWPORT_DESKTOP = { width: 1440, height: 900 };
|
|
const VIEWPORT_MOBILE = { width: 390, height: 844 };
|
|
|
|
// Mock session data to inject into Angular's AuthSessionStore
|
|
const MOCK_SESSION = {
|
|
tokens: {
|
|
accessToken: 'mock-access-token-for-screenshot',
|
|
expiresAtEpochMs: Date.now() + 3600000, // 1 hour from now
|
|
refreshToken: 'mock-refresh-token',
|
|
tokenType: 'Bearer',
|
|
scope: 'openid profile email ui.read ui.admin authority:tenants.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve orch:read analytics.read'
|
|
},
|
|
identity: {
|
|
subject: 'admin-user-001',
|
|
name: 'Admin',
|
|
email: 'admin@stella-ops.local',
|
|
roles: ['admin', 'security-analyst'],
|
|
},
|
|
dpopKeyThumbprint: 'mock-dpop-thumbprint-sha256',
|
|
issuedAtEpochMs: Date.now(),
|
|
tenantId: 'tenant-default',
|
|
scopes: [
|
|
'openid', 'profile', 'email',
|
|
'ui.read', 'ui.admin',
|
|
'authority:tenants.read', 'authority:users.read', 'authority:roles.read',
|
|
'authority:clients.read', 'authority:tokens.read', 'authority:audit.read',
|
|
'graph:read', 'graph:write', 'graph:simulate', 'graph:export',
|
|
'sbom:read',
|
|
'policy:read', 'policy:evaluate', 'policy:simulate', 'policy:author',
|
|
'policy:edit', 'policy:review', 'policy:submit', 'policy:approve',
|
|
'policy:operate', 'policy:activate', 'policy:run', 'policy:audit',
|
|
'scanner:read',
|
|
'exception:read', 'exception:write',
|
|
'release:read',
|
|
'aoc:verify',
|
|
'orch:read',
|
|
'analytics.read',
|
|
'findings:read',
|
|
],
|
|
audiences: ['stella-ops-api'],
|
|
authenticationTimeEpochMs: Date.now(),
|
|
freshAuthActive: true,
|
|
freshAuthExpiresAtEpochMs: Date.now() + 300000,
|
|
};
|
|
|
|
const PERSISTED_METADATA = {
|
|
subject: MOCK_SESSION.identity.subject,
|
|
expiresAtEpochMs: MOCK_SESSION.tokens.expiresAtEpochMs,
|
|
issuedAtEpochMs: MOCK_SESSION.issuedAtEpochMs,
|
|
dpopKeyThumbprint: MOCK_SESSION.dpopKeyThumbprint,
|
|
tenantId: MOCK_SESSION.tenantId,
|
|
};
|
|
|
|
async function sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function screenshot(page, name, description) {
|
|
const filePath = join(SCREENSHOT_DIR, `${name}.png`);
|
|
await page.screenshot({ path: filePath, fullPage: false });
|
|
console.log(` [SCREENSHOT] ${name}.png - ${description}`);
|
|
return filePath;
|
|
}
|
|
|
|
async function tryRealLogin(page) {
|
|
console.log('\n[STEP] Attempting real OAuth login flow...');
|
|
|
|
// Navigate to homepage
|
|
await page.goto(BASE_URL, { waitUntil: 'networkidle', timeout: 15000 });
|
|
await sleep(2000);
|
|
|
|
// Look for Sign in button
|
|
const signInButton = page.locator('button.app-auth__signin, button:has-text("Sign in")');
|
|
const hasSignIn = await signInButton.count() > 0;
|
|
|
|
if (!hasSignIn) {
|
|
console.log(' No Sign in button found (may already be authenticated or shell layout active)');
|
|
// Check if already in shell layout
|
|
const shell = page.locator('app-shell, .shell');
|
|
if (await shell.count() > 0) {
|
|
console.log(' Shell layout detected - already authenticated!');
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
console.log(' Sign in button found, clicking...');
|
|
await screenshot(page, '01-landing-unauthenticated', 'Landing page before sign-in');
|
|
|
|
// Click sign in - this will redirect to Authority
|
|
try {
|
|
// Listen for navigation to authority
|
|
const [response] = await Promise.all([
|
|
page.waitForNavigation({ timeout: 10000 }).catch(() => null),
|
|
signInButton.click(),
|
|
]);
|
|
|
|
await sleep(2000);
|
|
|
|
// Check if we're on an authority login page
|
|
const currentUrl = page.url();
|
|
console.log(` Redirected to: ${currentUrl}`);
|
|
|
|
if (currentUrl.includes('authority') || currentUrl.includes('authorize') || currentUrl.includes('login')) {
|
|
await screenshot(page, '02-authority-login-page', 'Authority login page');
|
|
|
|
// Try to find username/password fields
|
|
const usernameField = page.locator('input[name="username"], input[type="email"], input[name="login"], #username, #email');
|
|
const passwordField = page.locator('input[type="password"], input[name="password"], #password');
|
|
|
|
if (await usernameField.count() > 0 && await passwordField.count() > 0) {
|
|
console.log(' Login form found, entering credentials...');
|
|
await usernameField.first().fill('admin');
|
|
await passwordField.first().fill('Admin@Stella2026!');
|
|
|
|
const submitButton = page.locator('button[type="submit"], input[type="submit"], button:has-text("Login"), button:has-text("Sign in"), button:has-text("Log in")');
|
|
if (await submitButton.count() > 0) {
|
|
await submitButton.first().click();
|
|
await sleep(3000);
|
|
|
|
// Check if login was successful
|
|
if (page.url().includes(BASE_URL) || page.url().includes('callback')) {
|
|
console.log(' Login successful!');
|
|
await page.waitForLoadState('networkidle');
|
|
await sleep(2000);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we ended up with an error
|
|
if (currentUrl.includes('error') || page.url().includes('error')) {
|
|
console.log(' OAuth flow resulted in error page');
|
|
await screenshot(page, '02-oauth-error', 'OAuth error page');
|
|
}
|
|
} catch (error) {
|
|
console.log(` OAuth flow navigation failed: ${error.message}`);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async function injectMockAuth(page) {
|
|
console.log('\n[STEP] Injecting mock authentication state...');
|
|
|
|
// Navigate to base URL first
|
|
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
await sleep(1000);
|
|
|
|
// Inject session storage and manipulate Angular internals
|
|
const injected = await page.evaluate((mockData) => {
|
|
const { session, persisted } = mockData;
|
|
|
|
// Set session storage for persistence
|
|
sessionStorage.setItem('stellaops.auth.session.info', JSON.stringify(persisted));
|
|
|
|
// Try to access Angular's dependency injection to set the session store
|
|
// Angular stores component references in debug elements
|
|
try {
|
|
const appRoot = document.querySelector('app-root');
|
|
if (!appRoot) return { success: false, reason: 'app-root not found' };
|
|
|
|
// Access Angular's internal component instance
|
|
const ngContext = appRoot['__ngContext__'];
|
|
if (!ngContext) return { success: false, reason: 'no Angular context' };
|
|
|
|
return { success: true, reason: 'session storage set, need reload' };
|
|
} catch (e) {
|
|
return { success: false, reason: e.message };
|
|
}
|
|
}, { session: MOCK_SESSION, persisted: PERSISTED_METADATA });
|
|
|
|
console.log(` Injection result: ${JSON.stringify(injected)}`);
|
|
|
|
// The most reliable approach: intercept the silent-refresh to return a successful response,
|
|
// and intercept API calls to prevent errors. Then reload.
|
|
// Actually, let's take a simpler approach: route intercept to mock the OIDC flow.
|
|
|
|
// Set up route interception
|
|
await page.route('**/connect/authorize**', async (route) => {
|
|
// Redirect back to callback with a mock code
|
|
const url = new URL(route.request().url());
|
|
const state = url.searchParams.get('state') || 'mock-state';
|
|
const redirectUri = url.searchParams.get('redirect_uri') || `${BASE_URL}/callback`;
|
|
const callbackUrl = `${redirectUri}?code=mock-auth-code-12345&state=${state}`;
|
|
await route.fulfill({
|
|
status: 302,
|
|
headers: { 'Location': callbackUrl },
|
|
});
|
|
});
|
|
|
|
await page.route('**/token', async (route) => {
|
|
// Return a mock token response
|
|
const accessPayload = btoa(JSON.stringify({ alg: 'RS256', typ: 'at+jwt' }))
|
|
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
const accessClaims = btoa(JSON.stringify({
|
|
sub: 'admin-user-001',
|
|
name: 'Admin',
|
|
email: 'admin@stella-ops.local',
|
|
'stellaops:tenant': 'tenant-default',
|
|
role: ['admin', 'security-analyst'],
|
|
scp: MOCK_SESSION.scopes,
|
|
aud: ['stella-ops-api'],
|
|
auth_time: Math.floor(Date.now() / 1000),
|
|
'stellaops:fresh_auth': true,
|
|
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
iat: Math.floor(Date.now() / 1000),
|
|
iss: 'http://authority.stella-ops.local',
|
|
})).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
const mockSig = 'mock-signature';
|
|
const accessToken = `${accessPayload}.${accessClaims}.${mockSig}`;
|
|
|
|
const idPayload = btoa(JSON.stringify({ alg: 'RS256', typ: 'JWT' }))
|
|
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
const idClaims = btoa(JSON.stringify({
|
|
sub: 'admin-user-001',
|
|
name: 'Admin',
|
|
email: 'admin@stella-ops.local',
|
|
role: ['admin', 'security-analyst'],
|
|
nonce: 'mock-nonce',
|
|
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
iat: Math.floor(Date.now() / 1000),
|
|
iss: 'http://authority.stella-ops.local',
|
|
})).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
const idToken = `${idPayload}.${idClaims}.${mockSig}`;
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
headers: { 'DPoP-Nonce': 'mock-dpop-nonce' },
|
|
body: JSON.stringify({
|
|
access_token: accessToken,
|
|
token_type: 'DPoP',
|
|
expires_in: 3600,
|
|
scope: MOCK_SESSION.scopes.join(' '),
|
|
refresh_token: 'mock-refresh-token',
|
|
id_token: idToken,
|
|
}),
|
|
});
|
|
});
|
|
|
|
// Intercept console-context API calls
|
|
await page.route('**/console/context**', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
tenants: [{
|
|
tenantId: 'tenant-default',
|
|
displayName: 'Default Tenant',
|
|
role: 'admin'
|
|
}],
|
|
selectedTenant: 'tenant-default',
|
|
features: {},
|
|
}),
|
|
});
|
|
});
|
|
|
|
// Intercept any API calls that would fail without auth
|
|
await page.route('**/api/**', async (route) => {
|
|
// Let it pass through but don't fail
|
|
try {
|
|
await route.continue();
|
|
} catch {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ data: [], message: 'mock response' }),
|
|
});
|
|
}
|
|
});
|
|
|
|
// Intercept branding
|
|
await page.route('**/console/branding**', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
logoUrl: null,
|
|
title: 'Stella Ops',
|
|
theme: 'dark',
|
|
}),
|
|
});
|
|
});
|
|
|
|
// Now navigate fresh and click Sign in to trigger the mocked OAuth flow
|
|
await page.goto(BASE_URL, { waitUntil: 'networkidle', timeout: 15000 });
|
|
await sleep(2000);
|
|
|
|
// Click Sign in
|
|
const signInButton = page.locator('button.app-auth__signin, button:has-text("Sign in")');
|
|
if (await signInButton.count() > 0) {
|
|
console.log(' Clicking Sign in with intercepted OAuth flow...');
|
|
|
|
// The sign in click will call beginLogin() which calls window.location.assign(authorizeUrl)
|
|
// Our route intercept will redirect back to /callback with a mock code
|
|
// Then completeLoginFromRedirect will exchange the code at /token (also intercepted)
|
|
|
|
// But beginLogin uses window.location.assign which navigates away,
|
|
// and our route intercept handles the authorize redirect.
|
|
// However, the DPoP proof generation might fail...
|
|
|
|
// Let's try a different approach: directly manipulate the Angular service
|
|
const authInjected = await page.evaluate((sessionData) => {
|
|
try {
|
|
// Get Angular's injector from the app root
|
|
const appRoot = document.querySelector('app-root');
|
|
if (!appRoot) return { success: false, reason: 'no app-root' };
|
|
|
|
// Use ng.getComponent and ng.getInjector for debugging
|
|
const ng = (window).__ng_debug__ || (window).ng;
|
|
|
|
// Try getAllAngularRootElements approach
|
|
const rootElements = (window).getAllAngularRootElements?.() ?? [appRoot];
|
|
|
|
// Access __ngContext__ to find the component
|
|
const ctx = appRoot.__ngContext__;
|
|
if (!ctx) return { success: false, reason: 'no __ngContext__' };
|
|
|
|
// Walk the injector tree to find AuthSessionStore
|
|
// In Angular 19+, the LView is the context array
|
|
// The injector is typically at index 9 or via the directive flags
|
|
|
|
// Alternative approach: use the debugging API if available
|
|
if (typeof (window).ng !== 'undefined') {
|
|
const component = (window).ng.getComponent(appRoot);
|
|
if (component) {
|
|
// Access sessionStore through the component's injected dependencies
|
|
const sessionStore = component.sessionStore;
|
|
if (sessionStore && typeof sessionStore.setSession === 'function') {
|
|
sessionStore.setSession(sessionData);
|
|
return { success: true, method: 'ng.getComponent -> sessionStore.setSession' };
|
|
}
|
|
// Try via the auth property path
|
|
if (component.auth && component.sessionStore) {
|
|
component.sessionStore.setSession(sessionData);
|
|
return { success: true, method: 'component.sessionStore' };
|
|
}
|
|
}
|
|
}
|
|
|
|
return { success: false, reason: 'could not access Angular internals' };
|
|
} catch (e) {
|
|
return { success: false, reason: e.message };
|
|
}
|
|
}, MOCK_SESSION);
|
|
|
|
console.log(` Direct Angular injection result: ${JSON.stringify(authInjected)}`);
|
|
|
|
if (!authInjected.success) {
|
|
// Last resort: navigate with mocked OAuth - click Sign in and let interceptors handle it
|
|
// The DPoP createProof will be called which uses WebCrypto - should work in Playwright
|
|
console.log(' Attempting OAuth flow with route interception...');
|
|
|
|
try {
|
|
await Promise.all([
|
|
page.waitForURL('**/callback**', { timeout: 10000 }).catch(() => null),
|
|
signInButton.click(),
|
|
]);
|
|
|
|
await sleep(3000);
|
|
console.log(` After sign-in click, URL: ${page.url()}`);
|
|
|
|
// Wait for the app to process the callback
|
|
await page.waitForLoadState('networkidle').catch(() => {});
|
|
await sleep(2000);
|
|
} catch (e) {
|
|
console.log(` OAuth interception flow error: ${e.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if we're authenticated now
|
|
const isAuthenticated = await page.evaluate(() => {
|
|
const appRoot = document.querySelector('app-root');
|
|
const shell = document.querySelector('app-shell, .shell');
|
|
const sidebar = document.querySelector('app-sidebar, .sidebar');
|
|
return {
|
|
hasShell: !!shell,
|
|
hasSidebar: !!sidebar,
|
|
hasAppRoot: !!appRoot,
|
|
url: window.location.href,
|
|
sessionStorage: sessionStorage.getItem('stellaops.auth.session.info'),
|
|
};
|
|
});
|
|
|
|
console.log(` Auth check: ${JSON.stringify(isAuthenticated)}`);
|
|
return isAuthenticated.hasShell || isAuthenticated.hasSidebar;
|
|
}
|
|
|
|
async function captureAuthenticatedScreenshots(page) {
|
|
console.log('\n[STEP] Capturing authenticated layout screenshots...');
|
|
|
|
await sleep(2000);
|
|
|
|
// 1. Main dashboard with sidebar
|
|
await screenshot(page, '03-dashboard-sidebar-expanded', 'Dashboard with sidebar visible (1440x900)');
|
|
|
|
// 2. Wait for sidebar to be visible and capture it expanded
|
|
const sidebar = page.locator('app-sidebar, .sidebar');
|
|
if (await sidebar.count() > 0) {
|
|
console.log(' Sidebar component found');
|
|
|
|
// Try to expand nav groups
|
|
const navGroups = page.locator('.sidebar__group-header, .nav-group__header, [class*="group-toggle"], [class*="nav-group"]');
|
|
const groupCount = await navGroups.count();
|
|
console.log(` Found ${groupCount} nav group elements`);
|
|
|
|
// Click to expand some groups
|
|
for (let i = 0; i < Math.min(groupCount, 5); i++) {
|
|
try {
|
|
await navGroups.nth(i).click();
|
|
await sleep(300);
|
|
} catch (e) {
|
|
// Some may not be clickable
|
|
}
|
|
}
|
|
|
|
await sleep(500);
|
|
await screenshot(page, '04-sidebar-groups-expanded', 'Sidebar with navigation groups expanded');
|
|
}
|
|
|
|
// 3. Navigate to different pages
|
|
const pages = [
|
|
{ route: '/findings', name: '05-findings-page', desc: 'Scans & Findings page' },
|
|
{ route: '/vulnerabilities', name: '06-vulnerabilities-page', desc: 'Vulnerabilities page' },
|
|
{ route: '/triage/artifacts', name: '07-triage-page', desc: 'Triage page' },
|
|
{ route: '/policy-studio/packs', name: '08-policy-studio', desc: 'Policy Studio page' },
|
|
{ route: '/evidence', name: '09-evidence-page', desc: 'Evidence page' },
|
|
{ route: '/ops/health', name: '10-operations-health', desc: 'Operations health page' },
|
|
{ route: '/settings', name: '11-settings-page', desc: 'Settings page' },
|
|
{ route: '/console/admin/tenants', name: '12-admin-tenants', desc: 'Admin tenants page' },
|
|
];
|
|
|
|
for (const pg of pages) {
|
|
try {
|
|
await page.goto(`${BASE_URL}${pg.route}`, { waitUntil: 'domcontentloaded', timeout: 10000 });
|
|
await sleep(1500);
|
|
await page.waitForLoadState('networkidle').catch(() => {});
|
|
await screenshot(page, pg.name, pg.desc);
|
|
} catch (e) {
|
|
console.log(` Failed to capture ${pg.name}: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
// 4. Collapsed sidebar
|
|
console.log('\n Toggling sidebar to collapsed state...');
|
|
const collapseToggle = page.locator('[class*="collapse-toggle"], [class*="sidebar-toggle"], button[aria-label*="collapse"], button[aria-label*="toggle"]');
|
|
if (await collapseToggle.count() > 0) {
|
|
await collapseToggle.first().click();
|
|
await sleep(500);
|
|
await screenshot(page, '13-sidebar-collapsed', 'Sidebar in collapsed state');
|
|
} else {
|
|
// Try the sidebar component's collapse button
|
|
const sidebarButtons = page.locator('app-sidebar button, .sidebar button');
|
|
const btnCount = await sidebarButtons.count();
|
|
for (let i = 0; i < btnCount; i++) {
|
|
const text = await sidebarButtons.nth(i).textContent().catch(() => '');
|
|
const ariaLabel = await sidebarButtons.nth(i).getAttribute('aria-label').catch(() => '');
|
|
if (text?.includes('collapse') || text?.includes('Collapse') ||
|
|
ariaLabel?.includes('collapse') || ariaLabel?.includes('Collapse') ||
|
|
text?.includes('<<') || text?.includes('toggle')) {
|
|
await sidebarButtons.nth(i).click();
|
|
await sleep(500);
|
|
break;
|
|
}
|
|
}
|
|
await screenshot(page, '13-sidebar-collapsed', 'Sidebar collapsed (or toggle not found)');
|
|
}
|
|
|
|
// 5. Mobile viewport
|
|
console.log('\n Switching to mobile viewport...');
|
|
await page.setViewportSize(VIEWPORT_MOBILE);
|
|
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded', timeout: 10000 });
|
|
await sleep(2000);
|
|
await screenshot(page, '14-mobile-viewport', 'Mobile viewport (390px)');
|
|
|
|
// Try to open mobile menu
|
|
const menuToggle = page.locator('[class*="menu-toggle"], [class*="hamburger"], button[aria-label*="menu"], button[aria-label*="Menu"]');
|
|
if (await menuToggle.count() > 0) {
|
|
await menuToggle.first().click();
|
|
await sleep(500);
|
|
await screenshot(page, '15-mobile-menu-open', 'Mobile viewport with menu open');
|
|
}
|
|
}
|
|
|
|
async function captureUnauthenticatedScreenshots(page) {
|
|
console.log('\n[STEP] Capturing what is visible on the live site...');
|
|
|
|
await page.goto(BASE_URL, { waitUntil: 'networkidle', timeout: 15000 });
|
|
await sleep(3000);
|
|
|
|
// Check what's actually rendered
|
|
const pageState = await page.evaluate(() => {
|
|
const elements = {
|
|
appRoot: !!document.querySelector('app-root'),
|
|
appShell: !!document.querySelector('app-shell'),
|
|
sidebar: !!document.querySelector('app-sidebar'),
|
|
topbar: !!document.querySelector('app-topbar'),
|
|
header: !!document.querySelector('.app-header'),
|
|
signIn: !!document.querySelector('.app-auth__signin'),
|
|
splash: !!document.querySelector('#stella-splash'),
|
|
shell: !!document.querySelector('.shell'),
|
|
navigation: !!document.querySelector('app-navigation-menu'),
|
|
breadcrumb: !!document.querySelector('app-breadcrumb'),
|
|
};
|
|
return elements;
|
|
});
|
|
|
|
console.log(` Page element state: ${JSON.stringify(pageState, null, 2)}`);
|
|
|
|
await screenshot(page, '01-landing-page', 'Landing page state');
|
|
}
|
|
|
|
async function main() {
|
|
console.log('=== StellaOps Authenticated Layout Screenshot Capture ===\n');
|
|
console.log(`Target: ${BASE_URL}`);
|
|
console.log(`Output: ${SCREENSHOT_DIR}\n`);
|
|
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
args: ['--ignore-certificate-errors', '--allow-insecure-localhost'],
|
|
});
|
|
|
|
const context = await browser.newContext({
|
|
viewport: VIEWPORT_DESKTOP,
|
|
ignoreHTTPSErrors: true,
|
|
bypassCSP: true,
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
|
|
// Enable console log forwarding
|
|
page.on('console', msg => {
|
|
if (msg.type() === 'error') {
|
|
console.log(` [BROWSER ERROR] ${msg.text()}`);
|
|
}
|
|
});
|
|
|
|
try {
|
|
// Step 1: Capture current state
|
|
await captureUnauthenticatedScreenshots(page);
|
|
|
|
// Step 2: Try real login
|
|
const realLoginSuccess = await tryRealLogin(page);
|
|
|
|
if (realLoginSuccess) {
|
|
console.log('\n Real login succeeded!');
|
|
await captureAuthenticatedScreenshots(page);
|
|
} else {
|
|
console.log('\n Real login failed. Attempting mock auth injection...');
|
|
|
|
// Close and create new context for clean state
|
|
await page.close();
|
|
const newPage = await context.newPage();
|
|
|
|
page.on('console', msg => {
|
|
if (msg.type() === 'error') {
|
|
console.log(` [BROWSER ERROR] ${msg.text()}`);
|
|
}
|
|
});
|
|
|
|
const mockSuccess = await injectMockAuth(newPage);
|
|
|
|
if (mockSuccess) {
|
|
console.log('\n Mock auth injection succeeded!');
|
|
await captureAuthenticatedScreenshots(newPage);
|
|
} else {
|
|
console.log('\n Mock auth injection did not produce shell layout.');
|
|
console.log(' Capturing available state with additional details...');
|
|
|
|
// Capture whatever we can see
|
|
await newPage.goto(BASE_URL, { waitUntil: 'networkidle', timeout: 15000 });
|
|
await sleep(3000);
|
|
|
|
// Get detailed DOM info
|
|
const domInfo = await newPage.evaluate(() => {
|
|
const getTextContent = (selector) => {
|
|
const el = document.querySelector(selector);
|
|
return el ? el.textContent?.trim()?.substring(0, 200) : null;
|
|
};
|
|
const getInnerHTML = (selector) => {
|
|
const el = document.querySelector(selector);
|
|
return el ? el.innerHTML?.substring(0, 500) : null;
|
|
};
|
|
|
|
return {
|
|
title: document.title,
|
|
bodyClasses: document.body.className,
|
|
appRootHTML: getInnerHTML('app-root'),
|
|
mainContent: getTextContent('.app-content') || getTextContent('main'),
|
|
allComponents: Array.from(document.querySelectorAll('*'))
|
|
.filter(el => el.tagName.includes('-'))
|
|
.map(el => el.tagName.toLowerCase())
|
|
.filter((v, i, a) => a.indexOf(v) === i)
|
|
.slice(0, 30),
|
|
};
|
|
});
|
|
|
|
console.log('\n DOM Info:');
|
|
console.log(` Title: ${domInfo.title}`);
|
|
console.log(` Body classes: ${domInfo.bodyClasses}`);
|
|
console.log(` Custom elements: ${domInfo.allComponents.join(', ')}`);
|
|
if (domInfo.appRootHTML) {
|
|
console.log(` app-root HTML (first 500 chars): ${domInfo.appRootHTML}`);
|
|
}
|
|
|
|
await screenshot(newPage, '99-final-state', 'Final page state after all attempts');
|
|
await newPage.close();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(`\nFatal error: ${error.message}`);
|
|
console.error(error.stack);
|
|
|
|
try {
|
|
await screenshot(page, '99-error-state', 'Error state');
|
|
} catch {}
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
|
|
console.log('\n=== Screenshot capture complete ===');
|
|
}
|
|
|
|
main().catch(console.error);
|