Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 17 additions & 166 deletions packages/router/src/lib/i18n/provide-i18n.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import {
loadTranslationsRuntime,
clearTranslationsRuntime,
initI18n,
detectClientLocale,
replaceLocaleInPath,
resolveI18nConfig,
I18nConfig,
ɵregisterI18nComponentDef,
ɵresetI18nComponentDefCache,
getI18nComponentDefRegistrySize,
clearI18nComponentDefRegistry,
} from './provide-i18n';

describe('loadTranslationsRuntime', () => {
Expand All @@ -24,55 +19,36 @@ describe('loadTranslationsRuntime', () => {
(globalThis as any).$localize = originalLocalize;
});

it('should store translations in the parsed shape $localize.translate expects', async () => {
it('should store translations in $localize.TRANSLATIONS', () => {
(globalThis as any).$localize = {};

await loadTranslationsRuntime({
loadTranslationsRuntime({
'msg-hello': 'Bonjour',
'msg-goodbye': 'Au revoir',
});

const translations = (globalThis as any).$localize.TRANSLATIONS;
// `@angular/localize`'s `loadTranslations` parses each message into
// `{ text, messageParts, placeholderNames }` so that the runtime
// `translate()` function can build a translated template object.
expect(translations['msg-hello']).toMatchObject({
text: 'Bonjour',
messageParts: ['Bonjour'],
placeholderNames: [],
});
expect(translations['msg-goodbye']).toMatchObject({
text: 'Au revoir',
messageParts: ['Au revoir'],
placeholderNames: [],
});
expect(translations['msg-hello']).toBe('Bonjour');
expect(translations['msg-goodbye']).toBe('Au revoir');
});

it('should wire up $localize.translate so lookups actually happen', async () => {
it('should merge with existing translations', () => {
(globalThis as any).$localize = {};

await loadTranslationsRuntime({ 'msg-hello': 'Bonjour' });

expect(typeof (globalThis as any).$localize.translate).toBe('function');
});

it('should merge with existing translations', async () => {
(globalThis as any).$localize = {};
await loadTranslationsRuntime({ 'msg-existing': 'Existant' });
await loadTranslationsRuntime({ 'msg-new': 'Nouveau' });
loadTranslationsRuntime({ 'msg-existing': 'Existant' });
loadTranslationsRuntime({ 'msg-new': 'Nouveau' });

const translations = (globalThis as any).$localize.TRANSLATIONS;
expect(translations['msg-existing']).toMatchObject({ text: 'Existant' });
expect(translations['msg-new']).toMatchObject({ text: 'Nouveau' });
expect(translations['msg-existing']).toBe('Existant');
expect(translations['msg-new']).toBe('Nouveau');
});

it('should warn if $localize is not available', async () => {
it('should warn if $localize is not available', () => {
(globalThis as any).$localize = undefined;
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {
/* noop */
});

await loadTranslationsRuntime({ 'msg-hello': 'Bonjour' });
loadTranslationsRuntime({ 'msg-hello': 'Bonjour' });

expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('$localize is not available'),
Expand All @@ -81,34 +57,6 @@ describe('loadTranslationsRuntime', () => {
});
});

describe('clearTranslationsRuntime', () => {
let originalLocalize: any;

beforeEach(() => {
originalLocalize = (globalThis as any).$localize;
});

afterEach(() => {
(globalThis as any).$localize = originalLocalize;
});

it('should drop $localize.translate and empty TRANSLATIONS', async () => {
(globalThis as any).$localize = {};
await loadTranslationsRuntime({ 'msg-hello': 'Bonjour' });
expect((globalThis as any).$localize.translate).toBeTypeOf('function');

await clearTranslationsRuntime();

expect((globalThis as any).$localize.translate).toBeUndefined();
expect((globalThis as any).$localize.TRANSLATIONS).toEqual({});
});

it('should no-op when $localize is not available', async () => {
(globalThis as any).$localize = undefined;
await expect(clearTranslationsRuntime()).resolves.toBeUndefined();
});
});

describe('initI18n', () => {
let originalLocalize: any;

Expand All @@ -134,26 +82,6 @@ describe('initI18n', () => {
expect(loader).not.toHaveBeenCalled();
});

it('should clear translations even when the source locale is active', async () => {
// Simulate a prior render having loaded fr translations.
await loadTranslationsRuntime({ 'msg-hello': 'Bonjour' });
expect((globalThis as any).$localize.translate).toBeTypeOf('function');

const config: I18nConfig = {
defaultLocale: 'en',
locales: ['en', 'fr'],
loader: vi.fn(),
};

await initI18n(config, 'en');

// Previously loaded fr translations must be dropped so that the
// source locale's templates fall through to their source strings
// rather than silently rendering stale fr values.
expect((globalThis as any).$localize.translate).toBeUndefined();
expect((globalThis as any).$localize.TRANSLATIONS).toEqual({});
});

it('should load translations for a non-source locale', async () => {
const config: I18nConfig = {
defaultLocale: 'fr',
Expand All @@ -166,31 +94,9 @@ describe('initI18n', () => {
await initI18n(config, 'fr');

expect(config.loader).toHaveBeenCalledWith('fr');
expect(
(globalThis as any).$localize.TRANSLATIONS['msg-hello'],
).toMatchObject({
text: 'Bonjour',
});
});

it('should clear previous translations before loading new ones', async () => {
// Pretend an earlier request loaded fr.
await loadTranslationsRuntime({ 'msg-only-in-fr': 'Seulement' });

const config: I18nConfig = {
defaultLocale: 'en',
locales: ['en', 'de'],
loader: vi.fn().mockResolvedValue({ 'msg-only-in-de': 'Nur' }),
};

await initI18n(config, 'de');

const translations = (globalThis as any).$localize.TRANSLATIONS;
// The fr-only message must be gone; only the newly loaded de messages
// should be present. Without clearing, the two maps would mix and a
// /de request would still resolve fr-only messages.
expect(translations['msg-only-in-fr']).toBeUndefined();
expect(translations['msg-only-in-de']).toMatchObject({ text: 'Nur' });
expect((globalThis as any).$localize.TRANSLATIONS['msg-hello']).toBe(
'Bonjour',
);
});

it('should handle empty translations gracefully', async () => {
Expand All @@ -203,7 +109,6 @@ describe('initI18n', () => {
await initI18n(config, 'fr');

expect(config.loader).toHaveBeenCalledWith('fr');
expect((globalThis as any).$localize.TRANSLATIONS).toEqual({});
});

it('should support synchronous loaders', async () => {
Expand All @@ -215,11 +120,9 @@ describe('initI18n', () => {

await initI18n(config, 'de');

expect(
(globalThis as any).$localize.TRANSLATIONS['msg-hello'],
).toMatchObject({
text: 'Hallo',
});
expect((globalThis as any).$localize.TRANSLATIONS['msg-hello']).toBe(
'Hallo',
);
});

it('should use the passed locale over defaultLocale', async () => {
Expand Down Expand Up @@ -382,55 +285,3 @@ describe('resolveI18nConfig', () => {
);
});
});

describe('component def registry', () => {
beforeEach(() => {
clearI18nComponentDefRegistry();
});

it('should null def.tView on registered components when reset', () => {
const fakeDef = {
template: () => undefined,
tView: { someCachedValue: true },
};
ɵregisterI18nComponentDef(fakeDef);
expect(getI18nComponentDefRegistrySize()).toBe(1);

ɵresetI18nComponentDefCache();

expect(fakeDef.tView).toBeNull();
// The registry itself is intentionally preserved across resets so
// that subsequent requests keep clearing the same defs.
expect(getI18nComponentDefRegistrySize()).toBe(1);
});

it('should accept a Type with a ɵcmp static and unwrap it', () => {
const fakeDef = { template: () => undefined, tView: {} };
class FakeComponent {
static ɵcmp = fakeDef;
}

ɵregisterI18nComponentDef(FakeComponent);
ɵresetI18nComponentDefCache();

expect(fakeDef.tView).toBeNull();
});

it('should ignore things that are not component defs', () => {
ɵregisterI18nComponentDef(null);
ɵregisterI18nComponentDef(undefined);
ɵregisterI18nComponentDef({ notAComponent: true });
ɵregisterI18nComponentDef(class Bare {});

expect(getI18nComponentDefRegistrySize()).toBe(0);
});

it('should de-duplicate repeated registrations of the same def', () => {
const fakeDef = { template: () => undefined, tView: {} };
ɵregisterI18nComponentDef(fakeDef);
ɵregisterI18nComponentDef(fakeDef);
ɵregisterI18nComponentDef(fakeDef);

expect(getI18nComponentDefRegistrySize()).toBe(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('jitPlugin', () => {
);

const plugin = jitPlugin({ inlineStylesExtension: 'css' });
plugin.configResolved?.({} as any);
plugin.configResolved?.({ test: { css: true } } as any);

const encoded = encodeURIComponent(
Buffer.from('.demo { color: red; }').toString('base64'),
Expand All @@ -41,7 +41,7 @@ describe('jitPlugin', () => {
vi.mocked(preprocessCSS).mockRejectedValue(new Error('boom'));

const plugin = jitPlugin({ inlineStylesExtension: 'css' });
plugin.configResolved?.({} as any);
plugin.configResolved?.({ test: { css: true } } as any);

const encoded = encodeURIComponent(
Buffer.from('.demo { color: red; }').toString('base64'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ async function setupLegacyTransformPlugin() {
sys: {
readFile: vi.fn(),
},
ScriptTarget: { Latest: 99 },
readBuilderProgram: vi.fn().mockReturnValue(undefined),
createAbstractBuilder: vi.fn().mockReturnValue(mockBuilder),
createEmitAndSemanticDiagnosticsBuilderProgram: vi
.fn()
.mockReturnValue(mockBuilder),
createIncrementalCompilerHost: vi.fn().mockReturnValue({}),
createPrinter: vi.fn().mockReturnValue({ printNode: vi.fn() }),
createSourceFile: vi.fn().mockReturnValue({}),
}));

vi.doMock('@angular/compiler-cli', () => ({
Expand Down
35 changes: 15 additions & 20 deletions packages/vite-plugin-nitro/src/lib/build-server.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';

Expand All @@ -26,12 +26,11 @@ describe('buildServer', () => {
vi.restoreAllMocks();
});

it('fails when Nitro leaves an empty vercel config.json', async () => {
const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-vercel-config-'));
const outputDir = resolve(workspaceRoot, '.vercel', 'output');
const serverDir = resolve(outputDir, 'functions', '__server.func');
const publicDir = resolve(outputDir, 'static');
const buildConfigPath = resolve(outputDir, 'config.json');
it('forces rollup bundler and builds successfully', async () => {
const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-build-server-'));
const outputDir = resolve(workspaceRoot, '.output');
const serverDir = resolve(outputDir, 'server');
const publicDir = resolve(outputDir, 'public');

mkdirSync(serverDir, { recursive: true });
mkdirSync(publicDir, { recursive: true });
Expand All @@ -47,30 +46,26 @@ describe('buildServer', () => {
publicDir,
serverDir,
},
preset: 'vercel',
preset: 'node-server',
routeRules: {},
static: false,
vercel: {
functions: {
runtime: 'nodejs24.x',
},
},
},
close: vi.fn().mockResolvedValue(undefined),
} as never);
vi.mocked(prepare).mockResolvedValue(undefined as never);
vi.mocked(copyPublicAssets).mockResolvedValue(undefined as never);
vi.mocked(prerender).mockResolvedValue(undefined as never);
vi.mocked(build).mockImplementation(async () => {
writeFileSync(buildConfigPath, '', 'utf8');
});
vi.mocked(build).mockResolvedValue(undefined as never);

try {
await expect(
buildServer({}, { preset: 'vercel', output: { publicDir } }),
).rejects.toThrow(
`Nitro generated an empty Vercel build output config at "${buildConfigPath}".`,
await buildServer({}, { output: { publicDir } });

expect(createNitro).toHaveBeenCalledWith(
expect.objectContaining({
builder: 'rollup',
}),
);
expect(build).toHaveBeenCalled();
} finally {
rmSync(workspaceRoot, { recursive: true, force: true });
}
Expand Down
Loading
Loading