integration tests
Some checks failed
CI / lint-typecheck (pull_request) Failing after 4m50s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
Some checks failed
CI / lint-typecheck (pull_request) Failing after 4m50s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
This commit is contained in:
207
tests/integration/website/ssr/WebsiteSSR.test.ts
Normal file
207
tests/integration/website/ssr/WebsiteSSR.test.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { describe, test, beforeAll, afterAll, expect } from 'vitest';
|
||||
import { getWebsiteRouteContracts, RouteContract } from '../../../shared/website/RouteContractSpec';
|
||||
import { WebsiteServerHarness } from '../../harness/WebsiteServerHarness';
|
||||
import { ApiServerHarness } from '../../harness/ApiServerHarness';
|
||||
import { HttpDiagnostics } from '../../../shared/website/HttpDiagnostics';
|
||||
|
||||
const WEBSITE_BASE_URL = process.env.WEBSITE_BASE_URL || 'http://localhost:3005';
|
||||
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3006';
|
||||
|
||||
// Ensure WebsiteRouteManager uses the same persistence mode as the API harness
|
||||
process.env.GRIDPILOT_API_PERSISTENCE = 'inmemory';
|
||||
|
||||
describe('Website SSR Integration', () => {
|
||||
let websiteHarness: WebsiteServerHarness | null = null;
|
||||
let apiHarness: ApiServerHarness | null = null;
|
||||
const contracts = getWebsiteRouteContracts();
|
||||
|
||||
beforeAll(async () => {
|
||||
// 1. Start API
|
||||
console.log(`[WebsiteSSR] Starting API harness on ${API_BASE_URL}...`);
|
||||
apiHarness = new ApiServerHarness({
|
||||
port: parseInt(new URL(API_BASE_URL).port) || 3006,
|
||||
});
|
||||
await apiHarness.start();
|
||||
console.log(`[WebsiteSSR] API Harness started.`);
|
||||
|
||||
// 2. Start Website
|
||||
console.log(`[WebsiteSSR] Starting website harness on ${WEBSITE_BASE_URL}...`);
|
||||
websiteHarness = new WebsiteServerHarness({
|
||||
port: parseInt(new URL(WEBSITE_BASE_URL).port) || 3005,
|
||||
env: {
|
||||
PORT: '3005',
|
||||
API_BASE_URL: API_BASE_URL,
|
||||
NEXT_PUBLIC_API_BASE_URL: API_BASE_URL,
|
||||
NODE_ENV: 'test',
|
||||
},
|
||||
});
|
||||
await websiteHarness.start();
|
||||
console.log(`[WebsiteSSR] Website Harness started.`);
|
||||
}, 180000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (websiteHarness) {
|
||||
await websiteHarness.stop();
|
||||
}
|
||||
if (apiHarness) {
|
||||
await apiHarness.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test.each(contracts)('SSR for $path ($accessLevel)', async (contract: RouteContract) => {
|
||||
const url = `${WEBSITE_BASE_URL}${contract.path}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
redirect: 'manual',
|
||||
});
|
||||
|
||||
const status = response.status;
|
||||
const location = response.headers.get('location');
|
||||
const html = await response.text();
|
||||
|
||||
if (status === 500) {
|
||||
console.error(`[WebsiteSSR] 500 Error at ${contract.path}. HTML:`, html.substring(0, 10000));
|
||||
const errorMatch = html.match(/<pre[^>]*>([\s\S]*?)<\/pre>/);
|
||||
if (errorMatch) {
|
||||
console.error(`[WebsiteSSR] Error details from HTML:`, errorMatch[1]);
|
||||
}
|
||||
const nextDataMatch = html.match(/<script id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/);
|
||||
if (nextDataMatch) {
|
||||
console.error(`[WebsiteSSR] NEXT_DATA:`, nextDataMatch[1]);
|
||||
}
|
||||
// Look for Next.js 13+ flight data or error markers
|
||||
const flightDataMatch = html.match(/self\.__next_f\.push\(\[1,"([^"]+)"\]\)/g);
|
||||
if (flightDataMatch) {
|
||||
console.error(`[WebsiteSSR] Flight Data found, checking for errors...`);
|
||||
flightDataMatch.forEach(m => {
|
||||
if (m.includes('Error') || m.includes('failed')) {
|
||||
console.error(`[WebsiteSSR] Potential error in flight data:`, m);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Check for specific error message in the body
|
||||
if (html.includes('Error:')) {
|
||||
const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/);
|
||||
if (bodyMatch) {
|
||||
console.error(`[WebsiteSSR] Body content:`, bodyMatch[1].substring(0, 1000));
|
||||
}
|
||||
}
|
||||
// Check for Next.js 14+ error markers
|
||||
const nextErrorMatch = html.match(/<meta name="next-error" content="([^"]+)"\/>/);
|
||||
if (nextErrorMatch) {
|
||||
console.error(`[WebsiteSSR] Next.js Error Marker:`, nextErrorMatch[1]);
|
||||
}
|
||||
// Check for "digest" error markers
|
||||
const digestMatch = html.match(/"digest":"([^"]+)"/);
|
||||
if (digestMatch) {
|
||||
console.error(`[WebsiteSSR] Error Digest:`, digestMatch[1]);
|
||||
}
|
||||
// Check for "notFound" in flight data
|
||||
if (html.includes('notFound')) {
|
||||
console.error(`[WebsiteSSR] "notFound" found in HTML source`);
|
||||
}
|
||||
// Check for "NEXT_NOT_FOUND"
|
||||
if (html.includes('NEXT_NOT_FOUND')) {
|
||||
console.error(`[WebsiteSSR] "NEXT_NOT_FOUND" found in HTML source`);
|
||||
}
|
||||
// Check for "Invariant: notFound() called in shell"
|
||||
if (html.includes('Invariant: notFound() called in shell')) {
|
||||
console.error(`[WebsiteSSR] "Invariant: notFound() called in shell" found in HTML source`);
|
||||
}
|
||||
// Check for "Error: notFound()"
|
||||
if (html.includes('Error: notFound()')) {
|
||||
console.error(`[WebsiteSSR] "Error: notFound()" found in HTML source`);
|
||||
}
|
||||
// Check for "DIGEST"
|
||||
if (html.includes('DIGEST')) {
|
||||
console.error(`[WebsiteSSR] "DIGEST" found in HTML source`);
|
||||
}
|
||||
// Check for "NEXT_REDIRECT"
|
||||
if (html.includes('NEXT_REDIRECT')) {
|
||||
console.error(`[WebsiteSSR] "NEXT_REDIRECT" found in HTML source`);
|
||||
}
|
||||
// Check for "Error: "
|
||||
const genericErrorMatch = html.match(/Error: ([^<]+)/);
|
||||
if (genericErrorMatch) {
|
||||
console.error(`[WebsiteSSR] Generic Error Match:`, genericErrorMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
const failureContext = {
|
||||
url,
|
||||
status,
|
||||
location,
|
||||
html: html.substring(0, 1000), // Limit HTML in logs
|
||||
serverLogs: websiteHarness?.getLogTail(60),
|
||||
};
|
||||
|
||||
const formatFailure = (extra: string) => HttpDiagnostics.formatHttpFailure({ ...failureContext, extra });
|
||||
|
||||
// 1. Assert Status
|
||||
if (contract.expectedStatus === 'ok') {
|
||||
if (status !== 200) {
|
||||
throw new Error(formatFailure(`Expected status 200 OK, but got ${status}`));
|
||||
}
|
||||
} else if (contract.expectedStatus === 'redirect') {
|
||||
if (status !== 302 && status !== 307) {
|
||||
throw new Error(formatFailure(`Expected redirect status (302/307), but got ${status}`));
|
||||
}
|
||||
|
||||
// 2. Assert Redirect Location
|
||||
if (contract.expectedRedirectTo) {
|
||||
if (!location) {
|
||||
throw new Error(formatFailure(`Expected redirect to ${contract.expectedRedirectTo}, but got no Location header`));
|
||||
}
|
||||
const locationPathname = new URL(location, WEBSITE_BASE_URL).pathname;
|
||||
if (locationPathname !== contract.expectedRedirectTo) {
|
||||
throw new Error(formatFailure(`Expected redirect to pathname "${contract.expectedRedirectTo}", but got "${locationPathname}" (full: ${location})`));
|
||||
}
|
||||
}
|
||||
} else if (contract.expectedStatus === 'notFoundAllowed') {
|
||||
if (status !== 404 && status !== 200) {
|
||||
throw new Error(formatFailure(`Expected 404 or 200 (notFoundAllowed), but got ${status}`));
|
||||
}
|
||||
} else if (contract.expectedStatus === 'errorRoute') {
|
||||
// Error routes themselves should return 200 or their respective error codes (like 500)
|
||||
if (status >= 600) {
|
||||
throw new Error(formatFailure(`Error route returned unexpected status ${status}`));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Assert SSR HTML Markers (only if not a redirect)
|
||||
if (status === 200 || status === 404) {
|
||||
if (contract.ssrMustContain) {
|
||||
for (const marker of contract.ssrMustContain) {
|
||||
if (typeof marker === 'string') {
|
||||
if (!html.includes(marker)) {
|
||||
throw new Error(formatFailure(`SSR HTML missing expected marker: "${marker}"`));
|
||||
}
|
||||
} else if (marker instanceof RegExp) {
|
||||
if (!marker.test(html)) {
|
||||
throw new Error(formatFailure(`SSR HTML missing expected regex marker: ${marker}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (contract.ssrMustNotContain) {
|
||||
for (const marker of contract.ssrMustNotContain) {
|
||||
if (typeof marker === 'string') {
|
||||
if (html.includes(marker)) {
|
||||
throw new Error(formatFailure(`SSR HTML contains forbidden marker: "${marker}"`));
|
||||
}
|
||||
} else if (marker instanceof RegExp) {
|
||||
if (marker.test(html)) {
|
||||
throw new Error(formatFailure(`SSR HTML contains forbidden regex marker: ${marker}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (contract.minTextLength && html.length < contract.minTextLength) {
|
||||
throw new Error(formatFailure(`SSR HTML length ${html.length} is less than minimum ${contract.minTextLength}`));
|
||||
}
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
Reference in New Issue
Block a user