159 lines
5.6 KiB
JavaScript
159 lines
5.6 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { compileFunction } from 'node:vm';
|
|
import { createRequire } from 'node:module';
|
|
import React from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import ts from 'typescript';
|
|
const require = createRequire(import.meta.url),
|
|
root = new URL('../../resources/js/', import.meta.url);
|
|
let page = {
|
|
component: 'portal/about',
|
|
url: '/sobre',
|
|
props: { auth: { user: null } },
|
|
};
|
|
const MenuPlacement = React.createContext(false);
|
|
const PortalMenu = ({ primary = false }) =>
|
|
React.useContext(MenuPlacement) && !primary
|
|
? null
|
|
: React.createElement(
|
|
'header',
|
|
{ 'data-portal-horizontal-menu': true },
|
|
'Dropdown common header',
|
|
);
|
|
const passthrough = ({ children }) => children;
|
|
const mocks = {
|
|
'@inertiajs/react': {
|
|
usePage: () => page,
|
|
Link: ({ href, children, ...props }) =>
|
|
React.createElement('a', { href, ...props }, children),
|
|
},
|
|
'@/components/portal-menu': { MenuPlacement, PortalMenu },
|
|
'@/components/search-feedback': { default: () => null },
|
|
'@/lib/search-territory': { searchTerritoryHref: (href) => href },
|
|
'@/components/tempo-brand': {
|
|
TempoUiProvider: passthrough,
|
|
useTempoUi: () => ({ t: (x) => x }),
|
|
},
|
|
'@/translation/page-translator': { PageTranslator: () => null },
|
|
'@/components/tempo-footer': { TempoFooter: () => null },
|
|
'@/components/navigation-context': {
|
|
NavigationContextProvider: passthrough,
|
|
NavigationContextSummary: () =>
|
|
React.createElement('div', { 'data-context-summary': true }),
|
|
},
|
|
'@/components/breadcrumbs': {
|
|
Breadcrumbs: ({ breadcrumbs }) =>
|
|
React.createElement(
|
|
'nav',
|
|
{ 'aria-label': 'Local breadcrumbs' },
|
|
breadcrumbs.map((b) => b.title).join('/'),
|
|
),
|
|
},
|
|
};
|
|
function load(name) {
|
|
const output = ts.transpileModule(
|
|
fs.readFileSync(new URL(name, root), 'utf8'),
|
|
{
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.CommonJS,
|
|
target: ts.ScriptTarget.ES2022,
|
|
jsx: ts.JsxEmit.ReactJSX,
|
|
},
|
|
},
|
|
).outputText;
|
|
const module = { exports: {} };
|
|
compileFunction(output, ['require', 'module', 'exports'])(
|
|
(name) => mocks[name] ?? require(name),
|
|
module,
|
|
module.exports,
|
|
);
|
|
return module.exports.default;
|
|
}
|
|
const PortalNavigation = load('layouts/portal-navigation.tsx'),
|
|
AppHeaderLayout = load('layouts/app/app-header-layout.tsx');
|
|
mocks['@/layouts/app/app-header-layout'] = { default: AppHeaderLayout };
|
|
const AppLayout = load('layouts/app-layout.tsx');
|
|
const app = fs.readFileSync(new URL('app.tsx', root), 'utf8'),
|
|
match = app.match(
|
|
/layout: (\(name\) => \{[\s\S]*?\n \}),\n strictMode/,
|
|
);
|
|
assert.ok(match);
|
|
const resolveLayout = compileFunction('return ' + match[1], [
|
|
'PortalNavigation',
|
|
'AuthLayout',
|
|
'AppLayout',
|
|
'SettingsLayout',
|
|
])(PortalNavigation, passthrough, AppLayout, passthrough);
|
|
const files = fs
|
|
.readdirSync(new URL('pages/', root), { recursive: true })
|
|
.filter((f) => f.endsWith('.tsx'));
|
|
void test('every registered page layout renders exactly one common horizontal header, including auth/settings/private screens', () => {
|
|
for (const file of files) {
|
|
const name = file.replaceAll(path.sep, '/').replace(/\.tsx$/, '');
|
|
page = {
|
|
component: name,
|
|
url: '/qa-layout-check',
|
|
props: { auth: { user: null } },
|
|
};
|
|
const layouts = [resolveLayout(name)].flat(),
|
|
child = React.createElement(
|
|
'section',
|
|
{ 'data-page-content': true },
|
|
React.createElement(PortalMenu),
|
|
name,
|
|
);
|
|
const tree = layouts.reduceRight(
|
|
(children, Layout) =>
|
|
React.createElement(
|
|
Layout,
|
|
{ breadcrumbs: [{ title: 'QA breadcrumb', href: '/qa' }] },
|
|
children,
|
|
),
|
|
child,
|
|
);
|
|
const html = renderToStaticMarkup(tree);
|
|
assert.equal(
|
|
(html.match(/data-portal-horizontal-menu/g) ?? []).length,
|
|
1,
|
|
name,
|
|
);
|
|
assert.match(html, /data-page-content/, name);
|
|
assert.match(html, /data-context-summary/, name);
|
|
assert.doesNotMatch(html, /sidebar-trigger|data-sidebar=/, name);
|
|
}
|
|
});
|
|
void test('private layout keeps page breadcrumbs without reintroducing a separate navigation or sidebar provider', () => {
|
|
const html = renderToStaticMarkup(
|
|
React.createElement(
|
|
AppLayout,
|
|
{ breadcrumbs: [{ title: 'Histórico', href: '/historico' }] },
|
|
'Existing private content',
|
|
),
|
|
);
|
|
assert.match(html, /Histórico/);
|
|
assert.match(html, /Existing private content/);
|
|
assert.doesNotMatch(html, /Dropdown common header/);
|
|
});
|
|
void test('main five shortcuts remain in the hero while discovery no longer repeats them', () => {
|
|
const hero = fs.readFileSync(
|
|
new URL('components/portal-hero.tsx', root),
|
|
'utf8',
|
|
);
|
|
for (const href of [
|
|
'/porque-investir',
|
|
'/investimentos/iniciar',
|
|
'/oportunidades',
|
|
'/simulador-fiscal',
|
|
'/sectores',
|
|
])
|
|
assert.ok(hero.includes('href="' + href + '"'), href);
|
|
});
|
|
console.log(
|
|
'Page layout coverage:',
|
|
files.length,
|
|
'existing TSX pages; structural SSR only, not browser/viewport proof.',
|
|
);
|