[UI] Page Load Improvements (#12334)

* Parallel initial requests

* Remove debouncing

* Refactor lazy loading of desktop / mobile view

* Reduce initial waiting for loadable components

* Fix duplication of API calls

* Further reduce duplicate calls

* Combine user roles into /user/me/ endpoint

* lazy load global import drawer

* lazy load table in plugin context

* Patch ScanButton

* Added playwright tests for login

* Adjust thresholds
This commit is contained in:
Oliver
2026-07-10 18:08:25 +10:00
committed by GitHub
parent a38c3963f2
commit 83508f4b12
19 changed files with 711 additions and 157 deletions
+5
View File
@@ -38,3 +38,8 @@ export const noaccessuser: UserType = {
username: 'noaccess',
testcred: 'youshallnotpass'
};
export const engineeruser: UserType = {
username: 'engineer',
testcred: 'partsonly'
};
+304 -2
View File
@@ -1,9 +1,61 @@
import type { Page } from '@playwright/test';
import { TOTP } from 'otpauth';
import { expect, test } from './baseFixtures.js';
import { logoutUrl, noaccessuser } from './defaults.js';
import { engineeruser, logoutUrl, noaccessuser } from './defaults.js';
import { navigate, openDetailAction } from './helpers.js';
import { doLogin } from './login.js';
import { TOTP } from 'otpauth';
const stripQueryAndHash = (url: string): string => {
try {
const parsed = new URL(url);
return `${parsed.origin}${parsed.pathname}`;
} catch {
return url.split('?')[0].split('#')[0];
}
};
const isScriptOrStyle = (resourceType: string): boolean => {
return resourceType === 'script' || resourceType === 'stylesheet';
};
const isCriticalBundle = (url: string): boolean => {
if (!/\.(js|css)$/i.test(url)) {
return false;
}
if (/\.map$/i.test(url)) {
return false;
}
if (/(@vite\/client|hot-update)/i.test(url)) {
return false;
}
return /\/(assets|static)\//i.test(url);
};
const loginAndMeasure = async (page: Page): Promise<number> => {
await navigate(page, logoutUrl, { waitUntil: 'load' });
await page.waitForURL('**/web/login');
await page.getByLabel('login-username').fill(noaccessuser.username);
await page.getByLabel('login-password').fill(noaccessuser.testcred);
const start = Date.now();
await page.getByRole('button', { name: 'Log In' }).click();
await page.getByRole('link', { name: 'Dashboard' }).waitFor();
await page.getByRole('button', { name: 'navigation-menu' }).waitFor();
await page.waitForURL(/\/web(\/home)?/);
await page.waitForLoadState('networkidle');
// Ensure dashboard has completely loaded
await page.getByText('No Widgets Selected').waitFor();
await page.getByRole('button', { name: 'Norman Nothington' }).waitFor();
await page.waitForLoadState('networkidle');
return Date.now() - start;
};
/**
* Test various types of login failure
@@ -90,6 +142,256 @@ test('Login - Failures', async ({ page }) => {
}
});
// Check that page load times do not exceed thresholds for cold/warm/hot login scenarios
test('Login - Cold vs Warm vs Hot Load', async ({ page }) => {
// Ensure a fresh state for the cold login measurement.
await page.context().clearCookies();
await navigate(page, logoutUrl, { waitUntil: 'load' });
await page.waitForURL('**/web/login');
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
// Page load threshold values
// Note: Vite server in dev mode is significantly slower than production build
const COLD_MS_THRESHOLD: number = 5000;
const WARM_MS_THRESHOLD: number = 4000;
const HOT_MS_THRESHOLD: number = 3000;
const coldMs = await loginAndMeasure(page);
await navigate(page, logoutUrl, { waitUntil: 'load' });
await page.waitForURL('**/web/login');
const warmMs = await loginAndMeasure(page);
console.log('Cold MS:', coldMs, 'Warm MS:', warmMs);
expect(coldMs).toBeLessThan(COLD_MS_THRESHOLD);
expect(warmMs).toBeLessThan(WARM_MS_THRESHOLD);
// Perform a "hot" reload of the dashboard page, which should be faster than the warm login.
const start = Date.now();
await page.reload();
// Ensure dashboard has completely loaded
await page.getByText('No Widgets Selected').waitFor();
await page.getByRole('button', { name: 'Norman Nothington' }).waitFor();
await page.waitForLoadState('networkidle');
const hotMs = Date.now() - start;
console.log('Hot MS:', hotMs);
expect(hotMs).toBeLessThan(HOT_MS_THRESHOLD);
});
// Check for JS/CSS request failures and duplicate critical bundles during login boot
test('Login - JS/CSS Boot Checks', async ({ page }) => {
const failedResources: string[] = [];
const criticalResourceCounts = new Map<string, number>();
page.on('requestfailed', (request) => {
if (!isScriptOrStyle(request.resourceType())) {
return;
}
failedResources.push(
`${request.resourceType()} ${request.url()} (${request.failure()?.errorText ?? 'request failed'})`
);
});
page.on('requestfinished', async (request) => {
if (!isScriptOrStyle(request.resourceType())) {
return;
}
const response = await request.response();
if (!response) {
return;
}
const status = response.status();
if (status >= 400) {
failedResources.push(
`${request.resourceType()} ${request.url()} (HTTP ${status})`
);
}
const normalizedUrl = stripQueryAndHash(request.url());
if (isCriticalBundle(normalizedUrl)) {
const count = criticalResourceCounts.get(normalizedUrl) ?? 0;
criticalResourceCounts.set(normalizedUrl, count + 1);
}
});
await loginAndMeasure(page);
const duplicateCriticalBundles = [...criticalResourceCounts.entries()]
.filter(([, count]) => count > 1)
.map(([url, count]) => `${url} x${count}`);
expect(
failedResources,
`JS/CSS failures during login boot:\n${failedResources.join('\n')}`
).toEqual([]);
expect(
duplicateCriticalBundles,
`Duplicate critical bundles during login boot:\n${duplicateCriticalBundles.join('\n')}`
).toEqual([]);
});
// Check page redirect after login
test('Login - Redirect on Login', async ({ page }) => {
await navigate(page, logoutUrl, { waitUntil: 'load' });
await page.waitForURL('**/web/login');
await navigate(page, 'settings/user/account', { waitUntil: 'load' });
await page.waitForURL('**/web/login');
await page.getByLabel('login-username').fill(engineeruser.username);
await page.getByLabel('login-password').fill(engineeruser.testcred);
await page.getByRole('button', { name: 'Log In' }).click();
await page.waitForURL('**/web/settings/user/account');
await page.getByRole('button', { name: 'action-menu-account' }).waitFor();
await page.getByRole('button', { name: 'Robert Shuruncle' }).waitFor();
});
// Test that login session persists across page reload
test('Login - Session Persistence', async ({ page }) => {
await doLogin(page, {
user: engineeruser
});
await page.getByText('Use the menu to add widgets').waitFor();
await page.reload();
await page.getByRole('button', { name: 'navigation-menu' }).waitFor();
// Once we logout, the user session has been invalidated
await navigate(page, logoutUrl, { waitUntil: 'load' });
await page.waitForURL('**/web/login');
await page.goBack();
await page.waitForURL('**/web/login');
await page.getByLabel('login-username').waitFor();
});
// Test login session with forced network errors
test('Login - Network Errors & Retry', async ({ page }) => {
await navigate(page, logoutUrl, { waitUntil: 'load' });
await page.waitForURL('**/web/login');
await page.getByLabel('login-username').fill(engineeruser.username);
await page.getByLabel('login-password').fill(engineeruser.testcred);
const loginEndpoint = /auth\/login/;
await page.route(loginEndpoint, (route) => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ detail: 'Simulated server failure' })
});
});
const loginButton = page.getByRole('button', { name: 'Log In' });
await loginButton.click();
await page.getByText('Login failed (500)').waitFor();
await page.getByText('Simulated server failure').waitFor();
await expect(loginButton).toBeEnabled();
await page.unroute(loginEndpoint);
await loginButton.click();
await page.getByRole('button', { name: 'navigation-menu' }).waitFor();
await navigate(page, logoutUrl, { waitUntil: 'load' });
await page.waitForURL('**/web/login');
await page.getByLabel('login-username').fill(noaccessuser.username);
await page.getByLabel('login-password').fill(noaccessuser.testcred);
await page.route(loginEndpoint, (route) => {
route.abort('internetdisconnected');
});
await loginButton.click();
await page.getByText('Login failed').first().waitFor();
await page.getByText('No response from server.').waitFor();
await expect(loginButton).toBeEnabled();
await page.getByLabel('login-username').waitFor();
await page.unroute(loginEndpoint);
await loginButton.click();
await page.getByRole('button', { name: 'navigation-menu' }).waitFor();
});
// Check for exposed tokens or cookies after login, and ensure session cookie is secure
test('Login - Security Regression Checks', async ({ page }) => {
await doLogin(page, {
user: noaccessuser
});
const url = page.url();
expect(url).not.toMatch(/[?&](token|access_token|refresh_token|jwt|auth)=/i);
const storageData = await page.evaluate(() => {
return {
localStorageEntries: Object.entries(localStorage),
sessionStorageEntries: Object.entries(sessionStorage)
};
});
const suspiciousStorageEntries = [
...storageData.localStorageEntries,
...storageData.sessionStorageEntries
].filter(([key, value]) => {
const combined = `${key} ${value}`;
return (
/(access_token|refresh_token|jwt|bearer)/i.test(combined) ||
/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value)
);
});
expect(suspiciousStorageEntries).toEqual([]);
const cookies = await page.context().cookies();
const sessionCookie = cookies.find((cookie) => cookie.name === 'sessionid');
expect(sessionCookie).toBeDefined();
expect(sessionCookie?.httpOnly).toBeTruthy();
expect(['Lax', 'None', 'Strict']).toContain(sessionCookie?.sameSite ?? 'Lax');
});
// Check keyboard navigation of the login screen
test('Login - Keyboard Focus', async ({ page }) => {
await navigate(page, logoutUrl, { waitUntil: 'load' });
await page.waitForURL('**/web/login');
const username = page.getByLabel('login-username');
const password = page.getByLabel('login-password');
await expect(username).toBeVisible();
await expect(password).toBeVisible();
await username.focus();
await page.keyboard.type(noaccessuser.username);
await page.keyboard.press('Tab');
await expect(password).toBeFocused();
await page.keyboard.type(noaccessuser.testcred);
await page.keyboard.press('Enter');
await page.getByRole('button', { name: 'navigation-menu' }).waitFor();
await navigate(page, logoutUrl, { waitUntil: 'load' });
await page.waitForURL('**/web/login');
await page.getByRole('button', { name: 'Log In' }).click();
await expect(page.getByLabel('login-username')).toBeFocused();
});
test('Login - Change Password', async ({ page }) => {
await doLogin(page, {
user: noaccessuser