PHP Classes

File: tests/e2e/truss.spec.js

Recommend this page to a friend!
  Packages of Alberto Arena   Laravel Truss   tests/e2e/truss.spec.js   Download  
File: tests/e2e/truss.spec.js
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Laravel Truss
View a diagram of a Laravel application models
Author: By
Last change:
Date: 9 days ago
Size: 15,507 bytes
 

Contents

Class file image Download
import { readFileSync } from 'node:fs'; import { test, expect } from '@playwright/test'; import { users, posts, roles, roleUser } from '../js/fixtures.js'; const primary = { connection: 'primary', fallback: false, skipped_migrations: [], generated_at: '2026-07-23T00:00:00Z', tables: [users, posts, roles, roleUser], // 4 tables > warn_above (3) }; const secondary = { connection: 'secondary', fallback: true, // exercises the SQLite-fallback banner skipped_migrations: [], generated_at: '2026-07-23T00:00:00Z', tables: [{ name: 'audit_log', columns: [{ name: 'id', type: 'bigint unsigned', nullable: false, default: null }], primary_key: ['id'], indexes: [], foreign_keys: [] }], }; test.beforeEach(async ({ page }) => { await page.route('**/api/schema**', (route) => { const connection = new URL(route.request().url()).searchParams.get('connection'); const body = connection === 'secondary' ? secondary : primary; return route.fulfill({ contentType: 'application/json', body: JSON.stringify(body) }); }); await page.goto('/tests/e2e/harness.html'); }); const canvas = (page) => page.locator('#truss-canvas'); const banners = (page) => page.locator('#truss-banners'); test('renders an ER diagram from the fetched schema', async ({ page }) => { await expect(page.locator('#truss-canvas > svg')).toBeVisible(); await expect(canvas(page)).toContainText('users'); await expect(canvas(page)).toContainText('posts'); }); test('the text filter reduces the diagram to matching tables', async ({ page }) => { await expect(canvas(page)).toContainText('roles'); await page.fill('#truss-search', 'post'); await expect(canvas(page)).toContainText('posts'); await expect(canvas(page)).not.toContainText('roles'); await expect(canvas(page)).not.toContainText('users'); }); test('focus mode reduces to a table and its FK neighbours', async ({ page }) => { await page.selectOption('#truss-focus', 'users'); // depth 1 from users ? users + posts + role_user, but not roles. await expect(canvas(page)).toContainText('role_user'); await expect(canvas(page)).toContainText('posts'); await expect(canvas(page)).not.toContainText('roles'); }); test('focus centres the focused table in the viewport', async ({ page }) => { await expect(page.locator('#truss-canvas > svg')).toBeVisible(); await page.selectOption('#truss-focus', 'users'); await expect(canvas(page)).toContainText('role_user'); // wait for the re-render const offset = await page.evaluate(() => { const svg = document.querySelector('#truss-canvas > svg'); let node = null; svg.querySelectorAll('g.node').forEach((n) => { const l = n.querySelector('g.label.name .nodeLabel'); if (l && l.textContent.trim() === 'users') node = n; }); const n = node.getBoundingClientRect(); const v = document.getElementById('truss-viewport').getBoundingClientRect(); return { dx: Math.abs((n.left + n.right) / 2 - (v.left + v.right) / 2) / v.width, dy: Math.abs((n.top + n.bottom) / 2 - (v.top + v.bottom) / 2) / v.height, }; }); // The focused node's centre should sit near the viewport centre. expect(offset.dx).toBeLessThan(0.1); expect(offset.dy).toBeLessThan(0.1); }); test('the focused table is flagged for highlighting', async ({ page }) => { await expect(page.locator('#truss-canvas > svg')).toBeVisible(); await page.selectOption('#truss-focus', 'users'); await expect(canvas(page)).toContainText('role_user'); const flags = await page.evaluate(() => { const named = (name) => { let node = null; document.querySelectorAll('#truss-canvas g.node').forEach((n) => { if (n.querySelector('g.label.name .nodeLabel')?.textContent.trim() === name) node = n; }); return node; }; return { focused: named('users')?.classList.contains('truss-focused'), neighbour: named('posts')?.classList.contains('truss-focused'), }; }); expect(flags.focused).toBe(true); expect(flags.neighbour).toBe(false); }); test('enum types compact to the keyword, with values on hover', async ({ page }) => { await expect(page.locator('#truss-canvas > svg')).toBeVisible(); await expect(canvas(page)).toContainText('enum'); await expect(canvas(page)).not.toContainText('archived'); // an enum value stays out of the label const title = await page.evaluate(() => { let found = null; document.querySelectorAll('#truss-canvas g.node').forEach((n) => { if (n.querySelector('g.label.name .nodeLabel')?.textContent.trim() !== 'posts') return; n.querySelectorAll('g.label.attribute-type .nodeLabel').forEach((l) => { if (l.getAttribute('title')?.includes('archived')) found = l.getAttribute('title'); }); }); return found; }); expect(title).toContain('draft'); expect(title).toContain('published'); expect(title).toContain('archived'); }); test('an enum label is clickable and reveals its values in a popover', async ({ page }) => { await expect(page.locator('#truss-canvas > svg')).toBeVisible(); const trigger = page.locator('#truss-canvas .truss-enum-type').first(); await expect(trigger).toBeVisible(); // the compacted `enum` label on posts.status await trigger.click(); const pop = page.locator('#truss-popover'); await expect(pop).toBeVisible(); await expect(pop).toContainText('draft'); await expect(pop).toContainText('published'); await expect(pop).toContainText('archived'); await trigger.click(); // toggles closed await expect(pop).toBeHidden(); }); const tableName = (page, name) => page.locator('#truss-canvas .truss-table-name', { hasText: new RegExp(`^${name}$`) }); test('clicking a table name opens the export/focus menu', async ({ page }) => { await expect(page.locator('#truss-canvas > svg')).toBeVisible(); await tableName(page, 'users').click(); const pop = page.locator('#truss-popover'); await expect(pop).toBeVisible(); await expect(pop.locator('.truss-menu button')).toHaveCount(5); await expect(pop).toContainText('Focus this table'); await expect(pop).toContainText('Download JSON'); await expect(pop).toContainText('Download CSV'); await expect(pop).toContainText('Download Markdown'); }); test('the menu Focus action focuses the table and updates the URL', async ({ page }) => { await expect(page.locator('#truss-canvas > svg')).toBeVisible(); await tableName(page, 'users').click(); await page.locator('#truss-popover').getByText('Focus this table').click(); await expect(page.locator('#truss-popover')).toBeHidden(); await expect(canvas(page)).toContainText('role_user'); // users + FK neighbours await expect(canvas(page)).not.toContainText('roles'); await expect(page.locator('#truss-focus')).toHaveValue('users'); await expect.poll(() => new URL(page.url()).searchParams.get('focus')).toBe('users'); }); test('the focused table offers Unfocus instead of Focus, and it clears focus', async ({ page }) => { await page.goto('/tests/e2e/harness.html?focus=users'); await expect(page.locator('#truss-canvas > svg')).toBeVisible(); await tableName(page, 'users').click(); const pop = page.locator('#truss-popover'); await expect(pop).toContainText('Unfocus this table'); await expect(pop).not.toContainText('Focus this table'); await pop.getByText('Unfocus this table').click(); await expect(canvas(page)).toContainText('roles'); // full schema is back await expect(page.locator('#truss-focus')).toHaveValue(''); await expect.poll(() => new URL(page.url()).searchParams.get('focus')).toBeNull(); // A non-focused table still offers Focus. await tableName(page, 'posts').click(); await expect(pop).toContainText('Focus this table'); }); test('the menu downloads a table structure as JSON and CSV', async ({ page }) => { await expect(page.locator('#truss-canvas > svg')).toBeVisible(); await tableName(page, 'posts').click(); const jsonDownload = page.waitForEvent('download'); await page.locator('#truss-popover').getByText('Download JSON').click(); expect((await jsonDownload).suggestedFilename()).toBe('posts.json'); await tableName(page, 'posts').click(); const csvDownload = page.waitForEvent('download'); await page.locator('#truss-popover').getByText('Download CSV').click(); const csv = await csvDownload; expect(csv.suggestedFilename()).toBe('posts.csv'); await tableName(page, 'posts').click(); const mdDownload = page.waitForEvent('download'); await page.locator('#truss-popover').getByText('Download Markdown').click(); expect((await mdDownload).suggestedFilename()).toBe('posts.md'); }); test('the export button offers image and text exports', async ({ page }) => { await expect(page.locator('#truss-canvas > svg')).toBeVisible(); await page.locator('#truss-export-btn').click(); const pop = page.locator('#truss-popover'); await expect(pop).toBeVisible(); await expect(pop).toContainText('Export PNG'); await expect(pop).toContainText('Export SVG'); await expect(pop).toContainText('Data dictionary (Markdown)'); await expect(pop).toContainText('DBML'); }); test('exporting downloads a PNG of the whole diagram', async ({ page }) => { await expect(page.locator('#truss-canvas > svg')).toBeVisible(); await page.locator('#truss-export-btn').click(); const download = page.waitForEvent('download'); await page.locator('#truss-popover').getByText('Export PNG').click(); expect((await download).suggestedFilename()).toBe('truss-schema.png'); }); test('exporting downloads a Markdown data dictionary and a DBML of the selection', async ({ page }) => { await expect(page.locator('#truss-canvas > svg')).toBeVisible(); await page.locator('#truss-export-btn').click(); const mdDownload = page.waitForEvent('download'); await page.locator('#truss-popover').getByText('Data dictionary (Markdown)').click(); const md = await mdDownload; expect(md.suggestedFilename()).toBe('truss-schema.md'); const mdText = readFileSync(await md.path(), 'utf8'); expect(mdText).toContain('# Data dictionary'); expect(mdText).toContain('## users'); await page.locator('#truss-export-btn').click(); const dbmlDownload = page.waitForEvent('download'); await page.locator('#truss-popover').getByText('DBML', { exact: true }).click(); const dbml = await dbmlDownload; expect(dbml.suggestedFilename()).toBe('truss-schema.dbml'); const dbmlText = readFileSync(await dbml.path(), 'utf8'); expect(dbmlText).toContain('Table users {'); expect(dbmlText).toContain('Ref: posts.user_id > users.id'); }); test('exporting a focused view names the file after the focus and flattens labels', async ({ page }) => { await page.goto('/tests/e2e/harness.html?focus=users'); await expect(page.locator('#truss-canvas > svg')).toBeVisible(); await page.locator('#truss-export-btn').click(); const download = page.waitForEvent('download'); await page.locator('#truss-popover').getByText('Export SVG').click(); const svg = await download; expect(svg.suggestedFilename()).toBe('truss-users.svg'); // The export flattens HTML labels to <text> (rasterisable, tool-portable). const contents = readFileSync(await svg.path(), 'utf8'); expect(contents).toContain('<text'); expect(contents).toContain('users'); expect(contents).not.toContain('foreignObject'); }); test('a focus in the URL is applied on load', async ({ page }) => { await page.goto('/tests/e2e/harness.html?focus=users'); await expect(canvas(page).locator('svg')).toBeVisible(); // depth 1 from users -> users + posts + role_user, not roles. await expect(canvas(page)).toContainText('role_user'); await expect(canvas(page)).not.toContainText('roles'); await expect(page.locator('#truss-focus')).toHaveValue('users'); }); test('a filter in the URL is applied on load', async ({ page }) => { await page.goto('/tests/e2e/harness.html?filter=post'); await expect(canvas(page).locator('svg')).toBeVisible(); await expect(page.locator('#truss-search')).toHaveValue('post'); await expect(canvas(page)).toContainText('posts'); await expect(canvas(page)).not.toContainText('roles'); }); test('changing focus and filter updates the URL', async ({ page }) => { await expect(canvas(page).locator('svg')).toBeVisible(); await page.selectOption('#truss-focus', 'users'); await expect(canvas(page)).toContainText('role_user'); await expect.poll(() => new URL(page.url()).searchParams.get('focus')).toBe('users'); await page.selectOption('#truss-focus', ''); await page.fill('#truss-search', 'post'); await expect(canvas(page)).toContainText('posts'); await expect.poll(() => new URL(page.url()).searchParams.get('filter')).toBe('post'); expect(new URL(page.url()).searchParams.get('focus')).toBeNull(); }); test('the Laravel-types toggle swaps native types for short labels', async ({ page }) => { await expect(canvas(page)).toContainText('bigint_unsigned'); await page.check('#truss-labels'); await expect(canvas(page)).toContainText('integer'); await expect(canvas(page)).not.toContainText('bigint_unsigned'); }); const scaleOf = (page) => canvas(page).evaluate((el) => Number(/scale\(([-0-9.]+)\)/.exec(el.style.transform)?.[1] ?? 'NaN')); const translateOf = (page) => canvas(page).evaluate((el) => { const m = /translate\(([-0-9.]+)px,\s*([-0-9.]+)px\)/.exec(el.style.transform); return { x: Number(m?.[1]), y: Number(m?.[2]) }; }); test('the wheel zooms toward the cursor', async ({ page }) => { await expect(page.locator('#truss-canvas > svg')).toBeVisible(); const before = await scaleOf(page); const box = await page.locator('#truss-viewport').boundingBox(); await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.mouse.wheel(0, -300); expect(await scaleOf(page)).toBeGreaterThan(before); }); test('dragging pans the canvas', async ({ page }) => { await expect(page.locator('#truss-canvas > svg')).toBeVisible(); const before = await translateOf(page); const box = await page.locator('#truss-viewport').boundingBox(); await page.mouse.move(box.x + 200, box.y + 200); await page.mouse.down(); await page.mouse.move(box.x + 340, box.y + 260); await page.mouse.up(); const after = await translateOf(page); expect(after.x).not.toBe(before.x); expect(after.y).not.toBe(before.y); }); test('the slider sets the zoom level and Fit re-frames', async ({ page }) => { await expect(page.locator('#truss-canvas > svg')).toBeVisible(); const fitted = await scaleOf(page); await page.locator('#truss-zoom-range').fill('2'); expect(await scaleOf(page)).toBeCloseTo(2, 1); await expect(page.locator('#truss-zoom-pct')).toHaveText('200%'); await page.click('[data-fit]'); expect(await scaleOf(page)).toBeCloseTo(fitted, 1); }); test('the connection switcher re-fetches and re-renders without reload', async ({ page }) => { const connection = page.locator('#truss-connection'); await expect(connection).toBeVisible(); // two connections ? switcher shown await connection.selectOption('secondary'); await expect(canvas(page)).toContainText('audit_log'); await expect(canvas(page)).not.toContainText('posts'); await expect(banners(page)).toContainText('SQLite'); // fallback banner }); test('shows the large-schema warning above the configured threshold', async ({ page }) => { // primary has 4 tables, warn_above is 3, and nothing is filtered/focused. await expect(banners(page)).toContainText('large schema'); });