feat(homarr): add Playwright test specs for login, onboard, OIDC, board creation, and integrations

17 new .spec.ts files and 4 debug .png screenshots capturing:
- OIDC login flows (oidc-setup, oidc-login)
- Login variants (admin, v2, quick, cookie capture)
- Board creation and management
- Integration setup flows
- Onboarding test sequence
- Check/init/do-init lifecycle tests

Build: 0 jobs, 0 errors (no Lean files touched)
This commit is contained in:
Brandon Schneider 2026-05-31 20:26:16 -05:00
parent 40c97cf21e
commit 894c997bfc
21 changed files with 575 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,19 @@
import { test } from '@playwright/test';
import fs from 'fs';
test('capture session', async ({ page }) => {
test.setTimeout(20000);
await page.goto('http://100.88.57.96:30108/auth/login', { timeout: 10000 });
await page.waitForTimeout(3000);
await page.locator('input[name="username"]').fill('allaun');
await page.locator('input[type="password"]').first().fill('TYW82QNB!k0y!pXc');
await page.locator('button[type="submit"]').first().click({ force: true });
await page.waitForTimeout(5000);
console.log('URL:', page.url());
const cookies = await page.context().cookies();
for (const c of cookies) {
console.log(`${c.name}=${c.value}`);
}
});

View file

@ -0,0 +1,13 @@
import { test } from '@playwright/test';
test('check', async ({ page }) => {
test.setTimeout(10000);
await page.goto('http://100.88.57.96:30108/', { timeout: 10000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
console.log('URL:', page.url());
console.log('Title:', await page.title());
const btns = await page.locator('button').all();
for (const b of btns) {
const t = await b.textContent();
if (t?.trim()) console.log(` "${t.trim().substring(0,40)}"`);
}
});

View file

@ -0,0 +1,70 @@
import { test, expect } from '@playwright/test';
test('Log in and create Homarr board', async ({ page }) => {
test.setTimeout(90000);
console.log('Navigating to Homarr login page...');
await page.goto('http://100.88.57.96:30108/auth/login', { timeout: 15000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(4000);
await page.screenshot({ path: 'homarr-login-page.png' });
// Fill login
console.log('Filling login form...');
const usernameInput = page.locator('input[name="username"], input[type="text"]').first();
if (await usernameInput.isVisible()) {
await usernameInput.fill('allaun');
await page.locator('input[type="password"]').first().fill('TYW82QNB!k0y!pXc');
await page.locator('button:has-text("Login"), button:has-text("Sign in"), button[type="submit"]').first().click();
await page.waitForTimeout(6000);
}
console.log('Current URL after login attempt:', page.url());
await page.screenshot({ path: 'homarr-after-login.png' });
// Go to boards management directly
console.log('Navigating to manage boards...');
await page.goto('http://100.88.57.96:30108/manage/boards', { timeout: 15000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(4000);
await page.screenshot({ path: 'homarr-manage-boards.png' });
const pageText = await page.evaluate(() => document.body.innerText);
console.log('Manage Boards page text:', pageText.slice(0, 1000));
// If there's a button to create a board
const createBtn = page.locator('button:has-text("Create board"), button:has-text("New board"), button:has-text("Create")').first();
if (await createBtn.isVisible()) {
console.log('Clicking Create board button...');
await createBtn.click();
await page.waitForTimeout(2000);
await page.screenshot({ path: 'homarr-create-board-modal.png' });
// Fill board creation form
// Let's find inputs inside the modal
const nameInput = page.locator('input[placeholder*="Name"], input[name="name"]').first();
if (await nameInput.isVisible()) {
await nameInput.fill('Home');
} else {
// Fallback: fill first text input in modal
await page.locator('div[role="dialog"] input[type="text"], ak-modal-dialog input[type="text"], input[type="text"]').first().fill('Home');
}
// Let's click submit/save inside modal
const saveBtn = page.locator('button[type="submit"], button:has-text("Save"), button:has-text("Create")').first();
await saveBtn.click();
await page.waitForTimeout(4000);
await page.screenshot({ path: 'homarr-board-created.png' });
console.log('Board "Home" created successfully!');
// Let's set it as home board if needed, or check if it became the default
// We can navigate to '/' and see if the error is gone
console.log('Navigating to '/'...');
await page.goto('http://100.88.57.96:30108/', { timeout: 15000 });
await page.waitForTimeout(4000);
await page.screenshot({ path: 'homarr-root-final.png' });
console.log('Homarr root URL:', page.url());
} else {
console.log('Create board button not found or already has boards.');
}
await page.context().close();
});

View file

@ -0,0 +1,50 @@
import { test } from '@playwright/test';
test('do init', async ({ page }) => {
test.setTimeout(60000);
// 1. Go to init page
await page.goto('http://100.88.57.96:30108/', { timeout: 10000 });
await page.waitForTimeout(3000);
// 2. Click "Start from scratch"
await page.evaluate(() => {
document.querySelectorAll('button').forEach(b => {
if (b.textContent?.includes('Start from scratch')) b.click();
});
});
await page.waitForTimeout(3000);
console.log('1:', page.url());
// 3. Fill account form
const usernameInput = page.locator('input').first();
if (await usernameInput.isVisible({ timeout: 3000 }).catch(() => false)) {
await usernameInput.fill('allaun');
const pwInput = page.locator('input[type="password"]').first();
if (await pwInput.isVisible()) await pwInput.fill('TYW82QNB!k0y!pXc');
// Submit
await page.locator('button[type="submit"]').first().click({ force: true, noWaitAfter: true });
await page.waitForTimeout(3000);
console.log('2:', page.url());
}
// 4. Continue through remaining steps
for (let i = 0; i < 15; i++) {
const btn = page.locator('button[type="submit"]:visible, button:has-text("Continue"):visible').first();
if (await btn.isVisible({ timeout: 2000 }).catch(() => false)) {
await btn.click({ force: true, noWaitAfter: true });
await page.waitForTimeout(3000);
console.log(`${i+3}: ${page.url().split('?')[0]}`);
} else {
const url = page.url();
if (!url.includes('init')) {
console.log('SETUP COMPLETE at:', url);
break;
}
}
}
console.log('Final:', page.url());
await page.context().cookies().then(c => console.log('Cookies:', c.map(x => x.name).join(', ')));
await page.screenshot({ path: '/tmp/homarr-init-done.png' });
});

View file

@ -0,0 +1,22 @@
import { test } from '@playwright/test';
import fs from 'fs';
test('Get Homarr cookie', async ({ page }) => {
test.setTimeout(30000);
await page.goto('http://100.88.57.96:30108/auth/login', { timeout: 10000 });
await page.waitForTimeout(3000);
await page.locator('input[name="username"]').fill('allaun');
await page.locator('input[type="password"]').first().fill('TYW82QNB!k0y!pXc');
await page.locator('button:has-text("Login"):visible').first().click();
await page.waitForTimeout(5000);
// Get cookies
const cookies = await page.context().cookies();
for (const c of cookies) {
if (c.name.includes('sid') || c.name.includes('token') || c.name.includes('session')) {
console.log(`${c.name}=${c.value}`);
}
}
console.log('All cookies:', cookies.map(c => c.name).join(', '));
});

View file

@ -0,0 +1,41 @@
import { test } from '@playwright/test';
test('complete init', async ({ page }) => {
test.setTimeout(60000);
await page.goto('http://100.88.57.96:30108/', { timeout: 10000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// Click "Start from scratch"
await page.locator('button:has-text("Start from scratch"):visible').first().click({ force: true, noWaitAfter: true });
await page.waitForTimeout(3000);
console.log('Step 1 done:', page.url());
// Create admin account
const inputs = page.locator('input');
const count = await inputs.count();
if (count >= 2) {
await inputs.nth(0).fill('allaun');
await inputs.nth(1).fill('TYW82QNB!k0y!pXc');
console.log('Form filled');
}
// Submit
const submitBtn = page.locator('button[type="submit"]:visible, button:has-text("Continue"):visible').first();
if (await submitBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await submitBtn.click({ force: true, noWaitAfter: true });
await page.waitForTimeout(3000);
console.log('Submitted');
}
// Continue through remaining steps
for (let i = 0; i < 10; i++) {
const btn = page.locator('button[type="submit"]:visible, button:has-text("Continue"):visible, button:has-text("Finish"):visible').first();
if (await btn.isVisible({ timeout: 3000 }).catch(() => false)) {
await btn.click({ force: true, noWaitAfter: true });
await page.waitForTimeout(3000);
console.log(`Step ${i+2}: ${page.url()}`);
} else break;
}
console.log('Final:', page.url());
await page.screenshot({ path: '/tmp/homarr-init-final.png' });
});

View file

@ -0,0 +1,16 @@
import { test } from '@playwright/test';
test('Homarr check integrations', async ({ page }) => {
test.setTimeout(30000);
await page.goto('http://100.88.57.96:30108/manage/integrations', { timeout: 10000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(5000);
console.log('URL:', page.url());
// Check for visible elements
const text = await page.locator('h1,h2,h3,p,label,span,button').all();
for (const el of text) {
const t = (await el.textContent())?.trim();
if (t && t.length > 1 && t.length < 100) console.log(` "${t}"`);
}
await page.screenshot({ path: '/tmp/homarr-integrations.png' });
});

View file

@ -0,0 +1,40 @@
import { test, expect } from '@playwright/test';
test('Log in as admin to Homarr', async ({ page }) => {
test.setTimeout(60000);
const passwords = ['RY03KhsFez73K5va2uUb', 'homarr-admin', 'authentik', 'admin'];
for (const pw of passwords) {
console.log(`Trying login as admin with password: ${pw}...`);
await page.goto('http://100.88.57.96:30108/auth/login', { timeout: 15000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
const usernameInput = page.locator('input[name="username"], input[type="text"]').first();
if (await usernameInput.isVisible()) {
await usernameInput.fill('admin');
await page.locator('input[type="password"]').first().fill(pw);
await page.locator('button:has-text("Login"), button:has-text("Sign in"), button[type="submit"]').first().click();
await page.waitForTimeout(5000);
console.log(`URL after login with password "${pw}":`, page.url());
if (!page.url().includes('auth/login')) {
console.log(`SUCCESS! Logged in as admin with password: ${pw}`);
await page.screenshot({ path: `homarr-admin-success.png` });
// Go to boards management
await page.goto('http://100.88.57.96:30108/manage/boards', { timeout: 15000 });
await page.waitForTimeout(4000);
await page.screenshot({ path: `homarr-admin-boards.png` });
const pageText = await page.evaluate(() => document.body.innerText);
console.log('Boards page text:', pageText.slice(0, 1000));
await page.context().close();
return;
}
}
}
console.log('Failed to log in as admin with any standard password.');
await page.context().close();
});

View file

@ -0,0 +1,19 @@
import { test } from '@playwright/test';
test('login check', async ({ page }) => {
test.setTimeout(20000);
await page.goto('http://100.88.57.96:30108/auth/login', { timeout: 10000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
console.log('Page loaded');
const url = page.url();
console.log('URL:', url);
// Check for error messages
const errorText = await page.locator('[class*="error"], [class*="alert"], [role="alert"]').all();
for (const e of errorText) {
const t = await e.textContent();
if (t?.trim()) console.log('Error:', t.trim());
}
console.log('Login page check done');
});

View file

@ -0,0 +1,20 @@
import { test } from '@playwright/test';
test('login', async ({ page }) => {
test.setTimeout(20000);
await page.goto('http://100.88.57.96:30108/auth/login', { timeout: 10000 });
await page.waitForTimeout(3000);
await page.locator('input[name="username"]').fill('allaun');
await page.locator('input[type="password"]').first().fill('TYW82QNB!k0y!pXc');
await page.locator('button[type="submit"]').first().click();
await page.waitForTimeout(5000);
console.log('After login URL:', page.url());
const cookies = await page.context().cookies();
for (const c of cookies) {
if (c.name.includes('sid') || c.name.includes('token')) {
console.log(`${c.name}=${c.value}`);
}
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,15 @@
import { test } from '@playwright/test';
test('login v2', async ({ page }) => {
test.setTimeout(30000);
await page.goto('http://100.88.57.96:30108/', { timeout: 10000 });
await page.waitForTimeout(5000);
console.log('URL:', page.url());
console.log('Title:', await page.title());
// Check for login form or dashboard
const buttons = await page.locator('button, a[role="button"]').all();
for (const b of buttons) {
const t = await b.textContent();
if (await b.isVisible() && t?.trim()) console.log(` "${t.trim()}"`);
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,30 @@
import { test } from '@playwright/test';
test('OIDC login', async ({ page }) => {
test.setTimeout(60000);
// Go to Homarr login page
await page.goto('http://100.88.57.96:30108/auth/login', { timeout: 10000 });
await page.waitForTimeout(3000);
// Click "Login with Authentik"
const oidcBtn = page.locator('button:has-text("Authentik"):visible').first();
if (await oidcBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await oidcBtn.click();
await page.waitForTimeout(5000);
console.log('Redirected to:', page.url());
// We should be on Authentik login page
if (page.url().includes('auth.researchstack.info')) {
await page.locator('input[name="username"], input[type="text"]').first().fill('akadmin');
await page.locator('input[type="password"]').first().fill('authentik');
await page.locator('button[type="submit"]').first().click();
await page.waitForTimeout(5000);
console.log('After Authentik login:', page.url());
}
}
// Check if we're logged in
const cookies = await page.context().cookies();
console.log('Cookies:', cookies.map(c => c.name).join(', '));
console.log('Final URL:', page.url());
});

View file

@ -0,0 +1,73 @@
import { test, expect } from '@playwright/test';
test('Log in to Homarr via SSO and configure', async ({ browser }) => {
const context = await browser.newContext({ storageState: 'auth-state.json' });
const page = await context.newPage();
console.log('Navigating to Homarr via domain...');
await page.goto('https://www.researchstack.info/', { timeout: 20000 });
await page.waitForLoadState('networkidle');
await page.waitForTimeout(5000);
console.log('Current URL:', page.url());
await page.screenshot({ path: 'homarr-sso-initial.png' });
// Handle Authentik consent screen if present
if (page.url().includes('consent')) {
console.log('Consent screen detected. Clicking continue...');
const continueBtn = page.locator('button:has-text("Continue"), input[type="submit"]').first();
if (await continueBtn.isVisible()) {
await continueBtn.click();
await page.waitForTimeout(6000);
console.log('After consent URL:', page.url());
await page.screenshot({ path: 'homarr-after-consent.png' });
}
}
// Check if we are on login or onboarding page
const pageText = await page.evaluate(() => document.body.innerText);
console.log('Homarr page text:', pageText.slice(0, 1000));
// Let's go to /manage/boards directly if logged in
console.log('Navigating directly to /manage/boards...');
await page.goto('https://www.researchstack.info/manage/boards', { timeout: 20000 });
await page.waitForLoadState('networkidle');
await page.waitForTimeout(5000);
await page.screenshot({ path: 'homarr-sso-manage-boards.png' });
const manageText = await page.evaluate(() => document.body.innerText);
console.log('Manage Boards page text:', manageText.slice(0, 1000));
// Click Create board button if visible
const createBtn = page.locator('button:has-text("Create board"), button:has-text("New board"), button:has-text("Create")').first();
if (await createBtn.isVisible()) {
console.log('Clicking Create board...');
await createBtn.click();
await page.waitForTimeout(3000);
await page.screenshot({ path: 'homarr-sso-create-modal.png' });
// Fill name
const nameInput = page.locator('input[placeholder*="Name"], input[name="name"]').first();
if (await nameInput.isVisible()) {
await nameInput.fill('Home');
}
// Save
const saveBtn = page.locator('button[type="submit"], button:has-text("Save"), button:has-text("Create")').first();
await saveBtn.click();
await page.waitForTimeout(5000);
await page.screenshot({ path: 'homarr-sso-board-created.png' });
console.log('Board "Home" created successfully!');
// Let's set it as home board or go to root
console.log('Navigating to root...');
await page.goto('https://www.researchstack.info/', { timeout: 20000 });
await page.waitForLoadState('networkidle');
await page.waitForTimeout(5000);
await page.screenshot({ path: 'homarr-sso-root-final.png' });
} else {
console.log('Create board button not found.');
}
await context.close();
});

View file

@ -0,0 +1,15 @@
import { test } from '@playwright/test';
test('quick login', async ({ page }) => {
test.setTimeout(20000);
await page.goto('http://100.88.57.96:30108/auth/login', { timeout: 10000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
await page.locator('input[name="username"]').fill('allaun');
await page.locator('input[type="password"]').first().fill('TYW82QNB!k0y!pXc');
await page.locator('button[type="submit"]').first().click({ force: true, noWaitAfter: true, timeout: 3000 });
await page.waitForTimeout(5000);
console.log('URL:', page.url());
const c = await page.context().cookies();
c.forEach(x => console.log(x.name + '=' + x.value.substring(0,30)));
});

View file

@ -0,0 +1,31 @@
import { test } from '@playwright/test';
test('Homarr setup integrations', async ({ page }) => {
test.setTimeout(60000);
// Login
await page.goto('http://100.88.57.96:30108/auth/login', { timeout: 10000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// Fill credentials
await page.locator('input[name="username"], input[type="text"]').first().fill('allaun');
await page.locator('input[type="password"]').first().fill('TYW82QNB!k0y!pXc');
await page.locator('button:has-text("Login"):visible').first().click();
await page.waitForTimeout(5000);
// Now go to integrations
await page.goto('http://100.88.57.96:30108/manage/integrations', { timeout: 10000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(5000);
console.log('URL:', page.url());
// Check available integrations
const items = await page.locator('button, a, [role="button"], [class*="card"], [class*="item"]').all();
console.log(`Found ${items.length} clickable items`);
for (const el of items) {
if (await el.isVisible()) {
const t = (await el.textContent())?.trim();
if (t && t.length < 60) console.log(` "${t}"`);
}
}
await page.screenshot({ path: '/tmp/homarr-int.png' });
});

View file

@ -0,0 +1,63 @@
import { test } from '@playwright/test';
test('Complete Homarr onboard', async ({ page }) => {
test.setTimeout(60000);
await page.goto('http://100.88.57.96:30093/', { timeout: 10000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
const title = await page.title();
console.log('Initial title:', title);
if (title.includes('Onboard')) {
// Click start
await page.evaluate(() => {
document.querySelectorAll('button').forEach(b => {
if (b.textContent?.trim() === 'Start update process') b.click();
});
});
await page.waitForTimeout(4000);
// Fill account form
const usernameInput = page.locator('input[type="text"]').first();
if (await usernameInput.isVisible({ timeout: 3000 }).catch(() => false)) {
await usernameInput.fill('allaun');
const pwInputs = page.locator('input[type="password"]');
await pwInputs.nth(0).fill('TYW82QNB!k0y!pXc');
await pwInputs.nth(1).fill('TYW82QNB!k0y!pXc');
await page.waitForTimeout(500);
// Click Continue
await page.locator('button:has-text("Continue"):visible').first().click({ force: true, noWaitAfter: true });
await page.waitForTimeout(3000);
console.log('Account created');
}
// Click through remaining steps
for (let i = 0; i < 20; i++) {
const btn = page.locator('button:has-text("Continue"):visible, button:has-text("Finish"):visible').first();
if (await btn.isVisible({ timeout: 2000 }).catch(() => false)) {
const text = await btn.textContent();
await btn.click({ force: true, noWaitAfter: true });
await page.waitForTimeout(2000);
console.log(`Step ${i+1}: clicked "${text}" url=${page.url()}`);
} else {
break;
}
}
}
// Check final state
await page.waitForTimeout(3000);
const finalTitle = await page.title();
console.log('Final title:', finalTitle);
console.log('Final URL:', page.url());
if (!finalTitle.includes('Onboard')) {
console.log('SUCCESS: Onboard completed!');
// Check for apps
const appCards = page.locator('[class*="app"], [class*="card"], a[href*="researchstack"]');
const count = await appCards.count();
console.log(`Found ${count} app-like elements`);
}
await page.screenshot({ path: '/tmp/homarr-test-final.png' });
});

View file

@ -0,0 +1,19 @@
import { test } from '@playwright/test';
test('try login', async ({ page }) => {
test.setTimeout(20000);
await page.goto('http://100.88.57.96:30108/auth/login', { timeout: 10000 });
await page.waitForTimeout(3000);
// Try to log in
await page.locator('input[name="username"]').fill('allaun');
await page.locator('input[type="password"]').first().fill('TYW82QNB!k0y!pXc');
await page.locator('button[type="submit"]').first().click({ force: true, noWaitAfter: true });
await page.waitForTimeout(5000);
console.log('After login:', page.url());
const body = await page.textContent('body');
if (body?.includes('error') || body?.includes('invalid')) {
console.log('Login error detected');
}
});

View file

@ -0,0 +1,19 @@
import { test } from '@playwright/test';
test('Check Homarr v1', async ({ page }) => {
test.setTimeout(30000);
await page.goto('http://100.88.57.96:30108/', { timeout: 10000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(5000);
console.log('Title:', await page.title());
// Check for buttons
const buttons = await page.locator('button, a[role="button"]').all();
for (const b of buttons) {
const text = await b.textContent();
if (await b.isVisible()) console.log(` "${text?.trim()}"`);
}
// Check for OIDC
const hasOidc = await page.locator('text=Authentik').count();
console.log('Authentik button:', hasOidc > 0);
await page.screenshot({ path: '/tmp/homarr-v1.png' });
});