feat: add SSR and prerendering support #7
No files matched your search
@@ -0,0 +1,90 @@
|
||||
// Load the JIT compiler BEFORE any Angular router/common imports
|
||||
import '@angular/compiler';
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock the @angular/ssr/node imports that pull in Node.js http server code
|
||||
// — we only want to test PRERENDERED_ROUTES and isPrerenderedRoute here.
|
||||
vi.mock('@angular/ssr/node', () => ({
|
||||
AngularNodeAppEngine: class {},
|
||||
createNodeRequestHandler: (fn: Function) => fn,
|
||||
isMainModule: () => false,
|
||||
writeResponseToNodeResponse: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('node:http', () => ({
|
||||
default: { createServer: vi.fn() },
|
||||
}));
|
||||
|
||||
import { PRERENDERED_ROUTES, isPrerenderedRoute } from './server';
|
||||
|
||||
describe('SSR server — server.ts', () => {
|
||||
describe('PRERENDERED_ROUTES', () => {
|
||||
it('should contain exactly 5 routes', () => {
|
||||
expect(PRERENDERED_ROUTES).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('should include the home route', () => {
|
||||
expect(PRERENDERED_ROUTES).toContain('/');
|
||||
});
|
||||
|
||||
it('should include /about', () => {
|
||||
expect(PRERENDERED_ROUTES).toContain('/about');
|
||||
});
|
||||
|
||||
it('should include /schedule', () => {
|
||||
expect(PRERENDERED_ROUTES).toContain('/schedule');
|
||||
});
|
||||
|
||||
it('should include /events', () => {
|
||||
expect(PRERENDERED_ROUTES).toContain('/events');
|
||||
});
|
||||
|
||||
it('should include /contact', () => {
|
||||
expect(PRERENDERED_ROUTES).toContain('/contact');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPrerenderedRoute()', () => {
|
||||
it('should match exact prerendered paths', () => {
|
||||
expect(isPrerenderedRoute('/')).toBe(true);
|
||||
expect(isPrerenderedRoute('/about')).toBe(true);
|
||||
expect(isPrerenderedRoute('/schedule')).toBe(true);
|
||||
expect(isPrerenderedRoute('/events')).toBe(true);
|
||||
expect(isPrerenderedRoute('/contact')).toBe(true);
|
||||
});
|
||||
|
||||
it('should match paths with trailing slashes', () => {
|
||||
expect(isPrerenderedRoute('/about/')).toBe(true);
|
||||
expect(isPrerenderedRoute('/schedule/')).toBe(true);
|
||||
expect(isPrerenderedRoute('/events/')).toBe(true);
|
||||
expect(isPrerenderedRoute('/contact/')).toBe(true);
|
||||
});
|
||||
|
||||
it('should match paths with query strings', () => {
|
||||
expect(isPrerenderedRoute('/about?foo=bar')).toBe(true);
|
||||
expect(isPrerenderedRoute('/schedule?day=monday')).toBe(true);
|
||||
});
|
||||
|
||||
it('should match paths with fragments', () => {
|
||||
expect(isPrerenderedRoute('/events#upcoming')).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject non-prerendered routes', () => {
|
||||
expect(isPrerenderedRoute('/login')).toBe(false);
|
||||
expect(isPrerenderedRoute('/admin')).toBe(false);
|
||||
expect(isPrerenderedRoute('/nonexistent')).toBe(false);
|
||||
expect(isPrerenderedRoute('/admin/dashboard')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle full URLs', () => {
|
||||
expect(isPrerenderedRoute('https://kmtnflower.org/about')).toBe(true);
|
||||
expect(isPrerenderedRoute('http://localhost:4200/events')).toBe(true);
|
||||
expect(isPrerenderedRoute('https://kmtnflower.org/login')).toBe(false);
|
||||
});
|
||||
|
||||
it('should normalise root path — "/" stays "/"', () => {
|
||||
expect(isPrerenderedRoute('/')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,16 +6,53 @@ import {
|
||||
} from '@angular/ssr/node';
|
||||
import * as http from 'node:http';
|
||||
|
||||
/**
|
||||
* Public routes prerendered at build time (SSG — Static Site Generation).
|
||||
* These five routes are compiled to static HTML during `ng build` and
|
||||
* served directly from disk at runtime for optimal performance and SEO.
|
||||
* The list here mirrors `prerender-routes.txt` and the Prerender entries
|
||||
* in `src/app/app.server.routes.ts`.
|
||||
*/
|
||||
export const PRERENDERED_ROUTES: readonly string[] = [
|
||||
'/',
|
||||
'/about',
|
||||
'/schedule',
|
||||
'/events',
|
||||
'/contact',
|
||||
];
|
||||
|
||||
const ngApp = new AngularNodeAppEngine();
|
||||
|
||||
/**
|
||||
* Normalise a request pathname and check against prerendered routes.
|
||||
* Handles both bare paths (`/about`) and trailing-slash variants (`/about/`).
|
||||
*/
|
||||
export function isPrerenderedRoute(url: string): boolean {
|
||||
const pathname = new URL(url, 'http://localhost').pathname;
|
||||
const normalised = pathname.endsWith('/') && pathname !== '/'
|
||||
? pathname.slice(0, -1)
|
||||
: pathname;
|
||||
return PRERENDERED_ROUTES.includes(normalised);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming requests using the Angular SSR engine.
|
||||
* Prerendered routes are served as static HTML at build time.
|
||||
* Dynamic routes fall back to server-side rendering.
|
||||
*
|
||||
* Prerendered routes (/ , /about, /schedule, /events, /contact) are served
|
||||
* as pre-generated static HTML from the build output. All other routes
|
||||
* fall back to server-side rendering (SSR) or the fallback 404 handler.
|
||||
*/
|
||||
export default createNodeRequestHandler(async (req, res, next) => {
|
||||
try {
|
||||
const prerendered = isPrerenderedRoute(req.url || '/');
|
||||
if (prerendered) {
|
||||
// Prerendered routes are served as static HTML — AngularNodeAppEngine
|
||||
// reads the build-time manifest and returns the pre-generated response.
|
||||
console.log(`[SSR] Prerendered route: ${req.url}`);
|
||||
}
|
||||
|
||||
const response = await ngApp.handle(req);
|
||||
|
||||
if (response) {
|
||||
await writeResponseToNodeResponse(response, res);
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Load the JIT compiler BEFORE any Angular router/common imports
|
||||
import '@angular/compiler';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { RenderMode } from '@angular/ssr';
|
||||
import { serverRoutes } from './app.server.routes';
|
||||
|
||||
describe('SSR server routes — app.server.routes.ts', () => {
|
||||
it('should define 8 route entries', () => {
|
||||
expect(serverRoutes).toHaveLength(8);
|
||||
});
|
||||
|
||||
it('should prerender the home route', () => {
|
||||
const home = serverRoutes.find((r) => r.path === '');
|
||||
expect(home).toBeDefined();
|
||||
expect(home!.renderMode).toBe(RenderMode.Prerender);
|
||||
});
|
||||
|
||||
it('should prerender /about', () => {
|
||||
const about = serverRoutes.find((r) => r.path === 'about');
|
||||
expect(about).toBeDefined();
|
||||
expect(about!.renderMode).toBe(RenderMode.Prerender);
|
||||
});
|
||||
|
||||
it('should prerender /schedule', () => {
|
||||
const schedule = serverRoutes.find((r) => r.path === 'schedule');
|
||||
expect(schedule).toBeDefined();
|
||||
expect(schedule!.renderMode).toBe(RenderMode.Prerender);
|
||||
});
|
||||
|
||||
it('should prerender /events', () => {
|
||||
const events = serverRoutes.find((r) => r.path === 'events');
|
||||
expect(events).toBeDefined();
|
||||
expect(events!.renderMode).toBe(RenderMode.Prerender);
|
||||
});
|
||||
|
||||
it('should prerender /contact', () => {
|
||||
const contact = serverRoutes.find((r) => r.path === 'contact');
|
||||
expect(contact).toBeDefined();
|
||||
expect(contact!.renderMode).toBe(RenderMode.Prerender);
|
||||
});
|
||||
|
||||
it('should use SSR mode for /login', () => {
|
||||
const login = serverRoutes.find((r) => r.path === 'login');
|
||||
expect(login).toBeDefined();
|
||||
expect(login!.renderMode).toBe(RenderMode.Server);
|
||||
});
|
||||
|
||||
it('should use SSR mode for /admin', () => {
|
||||
const admin = serverRoutes.find((r) => r.path === 'admin');
|
||||
expect(admin).toBeDefined();
|
||||
expect(admin!.renderMode).toBe(RenderMode.Server);
|
||||
});
|
||||
|
||||
it('should fall back to SSR for unmatched routes', () => {
|
||||
const wildcard = serverRoutes.find((r) => r.path === '**');
|
||||
expect(wildcard).toBeDefined();
|
||||
expect(wildcard!.renderMode).toBe(RenderMode.Server);
|
||||
});
|
||||
|
||||
it('should have exactly 5 prerendered routes', () => {
|
||||
const prerendered = serverRoutes.filter((r) => r.renderMode === RenderMode.Prerender);
|
||||
expect(prerendered).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('should have exactly 3 SSR routes', () => {
|
||||
const ssr = serverRoutes.filter((r) => r.renderMode === RenderMode.Server);
|
||||
expect(ssr).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
// Load the JIT compiler BEFORE any Angular router/common imports
|
||||
import '@angular/compiler';
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { Injector } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { EventService } from './event.service';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
// Mock the app-config service
|
||||
vi.mock('./app-config.service', () => ({
|
||||
getAppConfig: () => ({ apiBaseUrl: 'http://localhost:8000' }),
|
||||
}));
|
||||
|
||||
describe('EventService — SSR error handling', () => {
|
||||
let httpMock: any;
|
||||
let service: EventService;
|
||||
|
||||
beforeEach(() => {
|
||||
httpMock = {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
};
|
||||
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
{ provide: HttpClient, useValue: httpMock },
|
||||
EventService,
|
||||
],
|
||||
});
|
||||
service = injector.get(EventService);
|
||||
});
|
||||
|
||||
it('should return empty array when API fails (SSR fallback)', async () => {
|
||||
const { throwError } = await import('rxjs');
|
||||
httpMock.get.mockReturnValue(throwError(() => new Error('Network error')));
|
||||
|
||||
const result = await firstValueFrom(service.getEvents());
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when API fails with active filter', async () => {
|
||||
const { throwError } = await import('rxjs');
|
||||
httpMock.get.mockReturnValue(throwError(() => new Error('Network error')));
|
||||
|
||||
const result = await firstValueFrom(service.getEvents(true));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should call the API endpoint', async () => {
|
||||
const { of } = await import('rxjs');
|
||||
httpMock.get.mockReturnValue(of([{ id: 1, name: 'Test Event' }]));
|
||||
|
||||
await firstValueFrom(service.getEvents());
|
||||
expect(httpMock.get).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
// Load the JIT compiler BEFORE any Angular router/common imports
|
||||
import '@angular/compiler';
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { Injector } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ShowService } from './show.service';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
// Mock the app-config service
|
||||
vi.mock('./app-config.service', () => ({
|
||||
getAppConfig: () => ({ apiBaseUrl: 'http://localhost:8000' }),
|
||||
}));
|
||||
|
||||
describe('ShowService — SSR error handling', () => {
|
||||
let httpMock: any;
|
||||
let service: ShowService;
|
||||
|
||||
beforeEach(() => {
|
||||
httpMock = {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
};
|
||||
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
{ provide: HttpClient, useValue: httpMock },
|
||||
ShowService,
|
||||
],
|
||||
});
|
||||
service = injector.get(ShowService);
|
||||
});
|
||||
|
||||
it('should return empty array when API fails (SSR fallback)', async () => {
|
||||
const { throwError } = await import('rxjs');
|
||||
httpMock.get.mockReturnValue(throwError(() => new Error('Network error')));
|
||||
|
||||
const result = await firstValueFrom(service.getShows());
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should call the API endpoint', async () => {
|
||||
const { of } = await import('rxjs');
|
||||
httpMock.get.mockReturnValue(of([{ id: 1, title: 'Test Show' }]));
|
||||
|
||||
await firstValueFrom(service.getShows());
|
||||
expect(httpMock.get).toHaveBeenCalledWith('http://localhost:8000/api/shows');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
// Load the JIT compiler BEFORE any Angular router/common imports
|
||||
import '@angular/compiler';
|
||||
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { Injector } from '@angular/core';
|
||||
import { ThemeService } from './theme.service';
|
||||
import { StationConfig } from '../interfaces/station-config';
|
||||
|
||||
describe('ThemeService', () => {
|
||||
let service: ThemeService;
|
||||
|
||||
const mockConfig: StationConfig = {
|
||||
id: 1,
|
||||
key: 'default',
|
||||
callsign: 'KMOF',
|
||||
name_primary: 'KMountain',
|
||||
name_secondary: 'Flower Radio',
|
||||
tagline: 'Community Radio',
|
||||
frequency: '91.7',
|
||||
founded_year: 2020,
|
||||
nonprofit_type: '501c3',
|
||||
ein: '12-3456789',
|
||||
logo_url: 'https://kmtnflower.org/logo.png',
|
||||
hero_background_url: '',
|
||||
hero_icon_url: '',
|
||||
hero_divider_url: '',
|
||||
address: '',
|
||||
phone: '',
|
||||
email: 'info@kmtnflower.org',
|
||||
website: 'kmtnflower.org',
|
||||
donation_url: '',
|
||||
stream_url: '',
|
||||
stream_metadata_url: '',
|
||||
play_store_icon_url: '',
|
||||
play_store_url: '',
|
||||
app_store_embed_html: '',
|
||||
social_facebook: '',
|
||||
social_twitter: '',
|
||||
social_instagram: '',
|
||||
social_youtube: '',
|
||||
hero_subtitle: 'Your community-powered radio station.',
|
||||
about_intro: 'About KMountain Flower Radio.',
|
||||
about_donation_text: '',
|
||||
schedule_intro: 'Our weekly schedule.',
|
||||
events_intro: 'Community events.',
|
||||
color_primary: '#4a90d9',
|
||||
color_primary_light: '#6ba3eb',
|
||||
color_primary_dark: '#2e6faa',
|
||||
color_primary_muted: '#7baee7',
|
||||
color_accent: '#e85d3a',
|
||||
color_accent_light: '#eb7d5e',
|
||||
color_accent_dark: '#c94422',
|
||||
color_accent_muted: '#e98a72',
|
||||
color_background: '#faf6f0',
|
||||
color_text: '#333333',
|
||||
color_success: '#4caf50',
|
||||
color_danger: '#f44336',
|
||||
color_info: '#2196f3',
|
||||
color_white: '#ffffff',
|
||||
color_light: '#eeeeee',
|
||||
color_medium: '#9e9e9e',
|
||||
color_black: '#000000',
|
||||
} as const as StationConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
const injector = Injector.create({
|
||||
providers: [ThemeService],
|
||||
});
|
||||
service = injector.get(ThemeService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clear all inline CSS custom properties after each test.
|
||||
const root = document.documentElement.style;
|
||||
for (const { prop } of [
|
||||
{ prop: '--color-primary' },
|
||||
{ prop: '--color-primary-light' },
|
||||
{ prop: '--color-primary-dark' },
|
||||
{ prop: '--color-primary-muted' },
|
||||
{ prop: '--color-accent' },
|
||||
{ prop: '--color-accent-light' },
|
||||
{ prop: '--color-accent-dark' },
|
||||
{ prop: '--color-accent-muted' },
|
||||
{ prop: '--color-cream' },
|
||||
{ prop: '--color-dark' },
|
||||
{ prop: '--color-success' },
|
||||
{ prop: '--color-danger' },
|
||||
{ prop: '--color-info' },
|
||||
{ prop: '--color-white' },
|
||||
{ prop: '--color-light' },
|
||||
{ prop: '--color-medium' },
|
||||
{ prop: '--color-black' },
|
||||
{ prop: '--sky-gradient' },
|
||||
{ prop: '--sunset-gradient' },
|
||||
{ prop: '--mixed-gradient' },
|
||||
]) {
|
||||
root.removeProperty(prop);
|
||||
}
|
||||
});
|
||||
|
||||
it('should apply all CSS color variables', () => {
|
||||
const setPropertySpy = vi.spyOn(document.documentElement.style, 'setProperty');
|
||||
service.apply(mockConfig);
|
||||
// 17 color fields + 3 gradient fields = 20 setProperty calls
|
||||
expect(setPropertySpy).toHaveBeenCalledTimes(20);
|
||||
});
|
||||
|
||||
it('should set primary color CSS variable', () => {
|
||||
service.apply(mockConfig);
|
||||
const rootStyle = document.documentElement.style;
|
||||
expect(rootStyle.getPropertyValue('--color-primary')).toBe('#4a90d9');
|
||||
});
|
||||
|
||||
it('should set accent color CSS variable', () => {
|
||||
service.apply(mockConfig);
|
||||
const rootStyle = document.documentElement.style;
|
||||
expect(rootStyle.getPropertyValue('--color-accent')).toBe('#e85d3a');
|
||||
});
|
||||
|
||||
it('should set background color to --color-cream', () => {
|
||||
service.apply(mockConfig);
|
||||
const rootStyle = document.documentElement.style;
|
||||
expect(rootStyle.getPropertyValue('--color-cream')).toBe('#faf6f0');
|
||||
});
|
||||
|
||||
it('should derive --sky-gradient from primary colors', () => {
|
||||
service.apply(mockConfig);
|
||||
const rootStyle = document.documentElement.style;
|
||||
const sky = rootStyle.getPropertyValue('--sky-gradient');
|
||||
expect(sky).toContain('linear-gradient(135deg');
|
||||
expect(sky).toContain('#2e6faa');
|
||||
expect(sky).toContain('#4a90d9');
|
||||
expect(sky).toContain('#6ba3eb');
|
||||
});
|
||||
|
||||
it('should derive --sunset-gradient from accent colors', () => {
|
||||
service.apply(mockConfig);
|
||||
const rootStyle = document.documentElement.style;
|
||||
const sunset = rootStyle.getPropertyValue('--sunset-gradient');
|
||||
expect(sunset).toContain('linear-gradient(135deg');
|
||||
expect(sunset).toContain('#c94422');
|
||||
expect(sunset).toContain('#e85d3a');
|
||||
expect(sunset).toContain('#eb7d5e');
|
||||
});
|
||||
|
||||
it('should derive --mixed-gradient from primary and accent', () => {
|
||||
service.apply(mockConfig);
|
||||
const rootStyle = document.documentElement.style;
|
||||
const mixed = rootStyle.getPropertyValue('--mixed-gradient');
|
||||
expect(mixed).toContain('linear-gradient(135deg');
|
||||
expect(mixed).toContain('#2e6faa');
|
||||
expect(mixed).toContain('#4a90d9');
|
||||
expect(mixed).toContain('#e85d3a');
|
||||
});
|
||||
|
||||
it('should work on both browser and server platforms (no platform check)', () => {
|
||||
// ThemeService no longer guards on isPlatformBrowser — it applies CSS vars
|
||||
// during SSR so the server-rendered HTML carries the correct theme.
|
||||
service.apply(mockConfig);
|
||||
const rootStyle = document.documentElement.style;
|
||||
expect(rootStyle.getPropertyValue('--color-primary')).toBe('#4a90d9');
|
||||
});
|
||||
|
||||
it('should skip fields with empty string values', () => {
|
||||
const emptyConfig = { ...mockConfig, color_primary: '' } as StationConfig;
|
||||
const setPropertySpy = vi.spyOn(document.documentElement.style, 'setProperty');
|
||||
service.apply(emptyConfig);
|
||||
expect(setPropertySpy).not.toHaveBeenCalledWith('--color-primary', '');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Injectable, inject, PLATFORM_ID } from '@angular/core';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { StationConfig } from '../interfaces/station-config';
|
||||
|
||||
@@ -33,16 +32,16 @@ function gradient(from: string, mid: string, to: string): string {
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ThemeService {
|
||||
private platformId = inject(PLATFORM_ID);
|
||||
|
||||
/**
|
||||
* Apply color theme from StationConfig as CSS custom properties on `:root`.
|
||||
* Also generates derived gradient variables.
|
||||
* Skips silently on the server (SSR) — CSS vars are set in the browser after hydration.
|
||||
*
|
||||
* Works on both browser and server platforms. During SSR the style properties
|
||||
* are written to the virtual DOM and serialised into the HTML output so the
|
||||
* prerendered / server-rendered page carries the correct colour scheme —
|
||||
* eliminating the "white flash" before hydration.
|
||||
*/
|
||||
apply(config: StationConfig): void {
|
||||
if (!isPlatformBrowser(this.platformId)) return;
|
||||
|
||||
const root = document.documentElement.style;
|
||||
|
||||
for (const { field, prop } of COLOR_MAP) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Load the JIT compiler BEFORE any Angular router/common imports
|
||||
import '@angular/compiler';
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { Injector, PLATFORM_ID } from '@angular/core';
|
||||
import { StructuredDataComponent } from './structured-data.component';
|
||||
import { StructuredDataService } from '../services/structured-data.service';
|
||||
|
||||
describe('StructuredDataComponent — SSR compatibility', () => {
|
||||
let serviceMock: any;
|
||||
|
||||
beforeEach(() => {
|
||||
serviceMock = {
|
||||
structuredData: vi.fn(() => []),
|
||||
};
|
||||
});
|
||||
|
||||
it('should NOT throw when instantiated on the server platform', () => {
|
||||
expect(() => {
|
||||
Injector.create({
|
||||
providers: [
|
||||
{ provide: PLATFORM_ID, useValue: 'server' },
|
||||
{ provide: StructuredDataService, useValue: serviceMock },
|
||||
{
|
||||
provide: StructuredDataComponent,
|
||||
useFactory: () => new StructuredDataComponent(),
|
||||
deps: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should instantiate without errors on the browser platform', () => {
|
||||
expect(() => {
|
||||
Injector.create({
|
||||
providers: [
|
||||
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||
{ provide: StructuredDataService, useValue: serviceMock },
|
||||
{
|
||||
provide: StructuredDataComponent,
|
||||
useFactory: () => new StructuredDataComponent(),
|
||||
deps: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
+5
-1
@@ -1,6 +1,10 @@
|
||||
import 'zone.js';
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { provideClientHydration } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { App } from './app/app';
|
||||
|
||||
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
|
||||
bootstrapApplication(App, {
|
||||
...appConfig,
|
||||
providers: [...appConfig.providers, provideClientHydration()],
|
||||
}).catch((err) => console.error(err));
|
||||
+12
-6
@@ -1,11 +1,17 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
/* To learn more about Typescript configuration file: https://typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.spec.ts"]
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"server.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user