// @ts-check const { test, expect } = require('@playwright/test'); const { openTerminal, typeCommand, currentPromptLine, terminalLines } = require('./helpers'); test.describe('Fake terminal — basic commands', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); await openTerminal(page); }); test('hero click expands the terminal and shifts the hero', async ({ page }) => { await expect(page.locator('.terminal-display')).toHaveClass(/grown/); await expect(page.locator('.terminal-display')).toHaveClass(/opaque/); await expect(page.locator('#hero')).toHaveClass(/shifted/); }); test('initial boot lines are rendered', async ({ page }) => { const lines = terminalLines(page); await expect(lines.first()).toHaveText(/uptime/); await expect(lines.nth(1)).toHaveText('optional'); }); test('uptime shows the load-average line', async ({ page }) => { await typeCommand(page, 'uptime'); await expect(terminalLines(page).nth(-2)).toContainText('up 42 days'); }); test('ls lists the portfolio files', async ({ page }) => { await typeCommand(page, 'ls'); const out = await terminalLines(page).nth(-2).textContent(); expect(out).toContain('index.html'); expect(out).toContain('nginx.conf'); expect(out).toContain('profile.jpeg'); }); test('pwd / whoami / hostname / id', async ({ page }) => { await typeCommand(page, 'pwd'); await expect(terminalLines(page).nth(-2)).toHaveText(/\/home\/reese\/portfolio/); await typeCommand(page, 'whoami'); await expect(terminalLines(page).nth(-2)).toHaveText('reese'); await typeCommand(page, 'hostname'); await expect(terminalLines(page).nth(-2)).toHaveText('homelab'); await typeCommand(page, 'id'); await expect(terminalLines(page).nth(-2)).toContainText('uid=1000(reese)'); }); test('date shows a plausible current date', async ({ page }) => { await typeCommand(page, 'date'); const out = await terminalLines(page).nth(-2).textContent(); expect(out).toMatch(/\b(19|20)\d{2}\b/); }); test('uname and cat /etc/os-release', async ({ page }) => { await typeCommand(page, 'uname -a'); await expect(terminalLines(page).nth(-2)).toContainText('Linux homelab'); await typeCommand(page, 'cat /etc/os-release'); const out = await terminalLines(page).nth(-2).textContent(); expect(out).toContain('Fedora Linux'); expect(out).toContain('VERSION_ID'); }); test('echo prints its argument as plain text', async ({ page }) => { await typeCommand(page, 'echo hello world'); await expect(terminalLines(page).nth(-2)).toHaveText('hello world'); }); test('ss shows listening ports', async ({ page }) => { await typeCommand(page, 'ss'); const out = await terminalLines(page).nth(-2).textContent(); expect(out).toContain('0.0.0.0:22'); expect(out).toContain('0.0.0.0:443'); }); test('docker ps and podman ps', async ({ page }) => { await typeCommand(page, 'docker ps'); const docker = await terminalLines(page).nth(-2).textContent(); expect(docker).toContain('gitea'); expect(docker).toContain('Up 42 days'); await typeCommand(page, 'podman ps'); const podman = await terminalLines(page).nth(-2).textContent(); expect(podman).toContain('homepage'); expect(podman).toContain('llama'); }); test('ps aux and df -h and free -h', async ({ page }) => { await typeCommand(page, 'ps aux'); expect(await terminalLines(page).nth(-2).textContent()).toContain('llama-server'); await typeCommand(page, 'df -h'); expect(await terminalLines(page).nth(-2).textContent()).toContain('Filesystem'); await typeCommand(page, 'free -h'); expect(await terminalLines(page).nth(-2).textContent()).toContain('Mem:'); }); test('neofetch renders the ASCII logo and colored blocks', async ({ page }) => { await typeCommand(page, 'neofetch'); const line = terminalLines(page).nth(-2); await expect(line).toContainText('reese@homelab'); await expect(line).toContainText('RTX 4090'); await expect(line.locator('span[style*="color"]')).not.toHaveCount(0); }); test('sl prints the locomotive', async ({ page }) => { await typeCommand(page, 'sl'); expect(await terminalLines(page).nth(-2).textContent()).toContain('DD'); }); test('help lists the available commands', async ({ page }) => { await typeCommand(page, 'help'); const out = await terminalLines(page).nth(-2).textContent(); expect(out).toContain('Available commands'); expect(out).toContain('vim'); expect(out).toContain('sudo'); }); test('systemctl, ip addr, cat ssh key', async ({ page }) => { await typeCommand(page, 'systemctl list-units'); expect(await terminalLines(page).nth(-2).textContent()).toContain('gitea.service'); await typeCommand(page, 'ip addr show'); expect(await terminalLines(page).nth(-2).textContent()).toContain('eth0'); await typeCommand(page, 'cat ~/.ssh/id_ed25519.pub'); expect(await terminalLines(page).nth(-2).textContent()).toContain('ssh-ed25519'); }); test('unknown command produces no output and a fresh prompt', async ({ page }) => { const before = await terminalLines(page).count(); await typeCommand(page, 'frobnicate --lots'); const after = await terminalLines(page).count(); expect(after).toBe(before + 1); // only the new prompt line await expect(currentPromptLine(page)).toContainText(/[\$#]/); }); }); test.describe('Fake terminal — state and special commands', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); await openTerminal(page); }); test('history: ArrowUp/ArrowDown recall previous commands', async ({ page }) => { await typeCommand(page, 'whoami'); await typeCommand(page, 'hostname'); await page.keyboard.press('ArrowUp'); await expect(currentPromptLine(page)).toContainText('hostname'); await page.keyboard.press('ArrowUp'); await expect(currentPromptLine(page)).toContainText('whoami'); await page.keyboard.press('ArrowDown'); await expect(currentPromptLine(page)).toContainText('hostname'); // executing the recalled command still works await page.keyboard.press('Enter'); await expect(terminalLines(page).nth(-2)).toHaveText('homelab'); }); test('Tab completes a command', async ({ page }) => { await page.keyboard.type('host'); await page.keyboard.press('Tab'); await expect(currentPromptLine(page)).toContainText('hostname'); await page.keyboard.press('Enter'); await expect(terminalLines(page).nth(-2)).toHaveText('homelab'); }); test('Tab cycles between multiple matches', async ({ page }) => { // strip the leading prompt ("$ "/"# ") from the line text const typed = async () => (await currentPromptLine(page).textContent()).replace(/^[#$] /, '').trim(); await page.keyboard.type('cat '); await page.keyboard.press('Tab'); const first = await typed(); await page.keyboard.press('Tab'); const second = await typed(); expect(first).not.toEqual(second); expect(first).toMatch(/^cat /); expect(second).toMatch(/^cat /); }); test('clear empties the terminal and leaves one prompt line', async ({ page }) => { await typeCommand(page, 'uptime'); expect(await terminalLines(page).count()).toBeGreaterThan(3); await typeCommand(page, 'clear'); await expect(terminalLines(page)).toHaveCount(1); await expect(currentPromptLine(page)).toContainText(/[\$#]/); }); test('sudo su - switches the prompt to root (#)', async ({ page }) => { await typeCommand(page, 'sudo su -'); const out = await terminalLines(page).nth(-2).textContent(); expect(out).toContain("I hope you know what you're doing"); const prompt = currentPromptLine(page).locator('.terminal-prompt'); await expect(prompt).toHaveText('#'); // root styling: the cursor is the red one, the line is red too await expect(currentPromptLine(page).locator('.terminal-cursor')).toHaveClass(/red/); await typeCommand(page, 'whoami'); // whoami still answers "reese" (the fake does not change identity) await expect(terminalLines(page).nth(-2)).toHaveText('reese'); }); test('rm -rf / as a normal user is refused', async ({ page }) => { await typeCommand(page, 'rm -rf /'); const out = await terminalLines(page).nth(-2).textContent(); expect(out).toContain('nice try'); // page must survive await expect(page.locator('#hero')).toBeVisible(); }); test('rm -rf / as root triggers the destruction easter egg', async ({ page }) => { await typeCommand(page, 'sudo su -'); await typeCommand(page, 'rm -rf /'); await expect(terminalLines(page).nth(-2)).toContainText('System destruction initiated'); await page.waitForTimeout(700); // destruction setTimeout is 500ms await expect.poll(async () => page.evaluate(() => document.body.children.length) ).toBe(0); }); test('exit as a normal user swaps the page for a login screen', async ({ page }) => { await typeCommand(page, 'exit'); await expect(page.locator('body')).toContainText('homelab tty1'); await expect(page.locator('body')).toContainText('homelab login:'); const login = page.locator('body input'); await expect(login).toBeVisible(); await login.fill('reese'); await login.press('Enter'); await expect(page.locator('body')).toContainText('Access denied'); // a fresh login line with a new input appears await expect(page.locator('body input')).toHaveCount(1); }); test('backspace edits the current command', async ({ page }) => { await page.keyboard.type('hostnamex'); await page.keyboard.press('Backspace'); await expect(currentPromptLine(page)).toContainText('hostname'); await page.keyboard.press('Enter'); await expect(terminalLines(page).nth(-2)).toHaveText('homelab'); }); });