more tests
This commit is contained in:
parent
84a82f521a
commit
80adf4aff7
10 changed files with 436 additions and 10 deletions
|
|
@ -13,8 +13,8 @@
|
|||
"format:check": "prettier --check .",
|
||||
"lint": "eslint --max-warnings=0 --no-warn-ignored packages/obsidian/src/**",
|
||||
"lint:fix": "eslint --max-warnings=0 --fix --no-warn-ignored packages/obsidian/src/**",
|
||||
"check": "bun run format:check && bun run typecheck && bun run lint",
|
||||
"check:fix": "bun run format && bun run typecheck && bun run lint:fix",
|
||||
"check": "bun run format:check && bun run typecheck && bun run lint && bun run test:log",
|
||||
"check:fix": "bun run format && bun run typecheck && bun run lint:fix && bun run test:log",
|
||||
"release": "lemons-automation release"
|
||||
},
|
||||
"keywords": [],
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export class PropertyMappingModel {
|
|||
}
|
||||
// remapped properties may not have the same name as any original property
|
||||
for (const property of this.getMappedProperties()) {
|
||||
const propertiesWithSameTarget = this.properties.filter(x => x.newProperty === property.property);
|
||||
const propertiesWithSameTarget = this.properties.filter(x => x.property === property.newProperty);
|
||||
if (propertiesWithSameTarget.length === 0) {
|
||||
// all good
|
||||
} else {
|
||||
|
|
|
|||
51
tests/api-model.test.ts
Normal file
51
tests/api-model.test.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { expect, test } from 'bun:test';
|
||||
import { APIModel, isSeasonListAPIModel } from 'packages/obsidian/src/api/APIModel';
|
||||
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import type { MDBError } from 'packages/obsidian/src/utils/MDBError';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import type { Result } from 'packages/obsidian/src/utils/result';
|
||||
import { ok } from 'packages/obsidian/src/utils/result';
|
||||
|
||||
class FakeAPI extends APIModel {
|
||||
apiName = 'fake';
|
||||
apiUrl = 'https://example.test';
|
||||
apiDescription = 'Fake API';
|
||||
types = [MediaType.Movie, MediaType.Series];
|
||||
disabledTypes: MediaType[] = [];
|
||||
|
||||
async searchByTitle(): Promise<Result<MediaTypeModel[], MDBError>> {
|
||||
return ok([]);
|
||||
}
|
||||
|
||||
async getById(): Promise<Result<MediaTypeModel, MDBError>> {
|
||||
throw new Error('not needed');
|
||||
}
|
||||
|
||||
getDisabledMediaTypes(): MediaType[] {
|
||||
return this.disabledTypes;
|
||||
}
|
||||
}
|
||||
|
||||
test('APIModel respects configured and disabled media types', () => {
|
||||
const api = new FakeAPI();
|
||||
|
||||
expect(api.hasType(MediaType.Movie)).toBe(true);
|
||||
expect(api.hasType(MediaType.Book)).toBe(false);
|
||||
expect(api.hasTypeOverlap([MediaType.Book, MediaType.Series])).toBe(true);
|
||||
|
||||
api.disabledTypes = [MediaType.Series];
|
||||
|
||||
expect(api.hasType(MediaType.Series)).toBe(false);
|
||||
expect(api.hasTypeOverlap([MediaType.Book, MediaType.Series])).toBe(false);
|
||||
});
|
||||
|
||||
test('isSeasonListAPIModel detects season-capable APIs', () => {
|
||||
const api = new FakeAPI();
|
||||
|
||||
expect(isSeasonListAPIModel(undefined)).toBe(false);
|
||||
expect(isSeasonListAPIModel(api)).toBe(false);
|
||||
|
||||
api.getSeasonsForSeries = async () => ok([]);
|
||||
|
||||
expect(isSeasonListAPIModel(api)).toBe(true);
|
||||
});
|
||||
56
tests/media-db-file-helper.test.ts
Normal file
56
tests/media-db-file-helper.test.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { expect, test } from 'bun:test';
|
||||
import type { TFile } from 'obsidian';
|
||||
import { MediaDbFileHelper } from 'packages/obsidian/src/utils/MediaDbFileHelper';
|
||||
|
||||
function createHelper(fileContent = '', frontmatter: Record<string, unknown> = {}): MediaDbFileHelper {
|
||||
return new MediaDbFileHelper({
|
||||
app: {
|
||||
metadataCache: {
|
||||
getFileCache: () => ({ frontmatter }),
|
||||
},
|
||||
vault: {
|
||||
read: async () => fileContent,
|
||||
},
|
||||
},
|
||||
} as never);
|
||||
}
|
||||
|
||||
test('getMetaDataFromFileContent parses frontmatter and ignores body-only files', () => {
|
||||
const helper = createHelper();
|
||||
|
||||
expect(helper.getMetaDataFromFileContent('---\ntitle: Arrival\nyear: 2016\nwatched: true\n---\nBody')).toEqual({
|
||||
title: 'Arrival',
|
||||
year: 2016,
|
||||
watched: true,
|
||||
});
|
||||
expect(helper.getMetaDataFromFileContent('Body only')).toEqual({});
|
||||
});
|
||||
|
||||
test('attachTemplate merges template metadata behind existing metadata and appends template body', async () => {
|
||||
const helper = createHelper();
|
||||
|
||||
const result = await helper.attachTemplate({ title: 'Arrival', id: '1' }, 'Existing\n', '---\ntitle: Template\nrating: 8\n---\nTemplate body');
|
||||
|
||||
expect(result.fileMetadata).toEqual({ title: 'Arrival', rating: 8, id: '1' });
|
||||
expect(result.fileContent).toBe('Existing\n\nTemplate body');
|
||||
});
|
||||
|
||||
test('attachFile merges attached note metadata behind generated metadata and strips frontmatter', async () => {
|
||||
const helper = createHelper('---\ntitle: Old title\ncustom: keep\n---\nAttached body', { title: 'Old title', custom: 'keep' });
|
||||
const file = { path: 'old.md' } as TFile;
|
||||
|
||||
const result = await helper.attachFile({ title: 'New title', id: '1' }, '', file);
|
||||
|
||||
expect(result.fileMetadata).toEqual({ title: 'New title', custom: 'keep', id: '1' });
|
||||
expect(result.fileContent).toBe('Attached body');
|
||||
});
|
||||
|
||||
test('getMetadataFromFileCache returns a clone instead of cache object reference', () => {
|
||||
const frontmatter = { title: 'Arrival', nested: { rating: 9 } };
|
||||
const helper = createHelper('', frontmatter);
|
||||
const metadata = helper.getMetadataFromFileCache({ path: 'movie.md' } as TFile);
|
||||
|
||||
(metadata.nested as { rating: number }).rating = 1;
|
||||
|
||||
expect(frontmatter.nested.rating).toBe(9);
|
||||
});
|
||||
76
tests/media-type-models.test.ts
Normal file
76
tests/media-type-models.test.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { expect, test } from 'bun:test';
|
||||
import { BoardGameModel } from 'packages/obsidian/src/models/BoardGameModel';
|
||||
import { BookModel } from 'packages/obsidian/src/models/BookModel';
|
||||
import { ComicMangaModel } from 'packages/obsidian/src/models/ComicMangaModel';
|
||||
import { GameModel } from 'packages/obsidian/src/models/GameModel';
|
||||
import { MovieModel } from 'packages/obsidian/src/models/MovieModel';
|
||||
import { MusicReleaseModel } from 'packages/obsidian/src/models/MusicReleaseModel';
|
||||
import { SeasonModel } from 'packages/obsidian/src/models/SeasonModel';
|
||||
import { SeasonSearchResultModel } from 'packages/obsidian/src/models/SeasonSearchResultModel';
|
||||
import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel';
|
||||
import { WikiModel } from 'packages/obsidian/src/models/WikiModel';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import { MediaTypeManager } from 'packages/obsidian/src/utils/MediaTypeManager';
|
||||
|
||||
test('MediaTypeManager creates the expected model for every supported media type', () => {
|
||||
const manager = new MediaTypeManager();
|
||||
|
||||
const cases = [
|
||||
[MediaType.BoardGame, BoardGameModel],
|
||||
[MediaType.Book, BookModel],
|
||||
[MediaType.ComicManga, ComicMangaModel],
|
||||
[MediaType.Game, GameModel],
|
||||
[MediaType.Movie, MovieModel],
|
||||
[MediaType.MusicRelease, MusicReleaseModel],
|
||||
[MediaType.Season, SeasonModel],
|
||||
[MediaType.Series, SeriesModel],
|
||||
[MediaType.Wiki, WikiModel],
|
||||
] as const;
|
||||
|
||||
for (const [mediaType, Model] of cases) {
|
||||
const model = manager.createMediaTypeModelFromMediaType({ title: 'Example' }, mediaType);
|
||||
|
||||
expect(model).toBeInstanceOf(Model);
|
||||
expect(model.type).toBe(mediaType);
|
||||
expect(model.getMediaType()).toBe(mediaType);
|
||||
}
|
||||
|
||||
expect(() => manager.createMediaTypeModelFromMediaType({}, 'unknown' as MediaType)).toThrow('Unknown media type: unknown');
|
||||
});
|
||||
|
||||
test('MediaTypeModel metadata flattens user data and adds slash-separated tags', () => {
|
||||
const movie = new MovieModel({
|
||||
title: 'Arrival',
|
||||
englishTitle: 'Arrival',
|
||||
year: '2016',
|
||||
userData: { watched: true, lastWatched: '2026-01-02', personalRating: 9 },
|
||||
});
|
||||
|
||||
expect(movie.toMetaDataObject()).toMatchObject({
|
||||
title: 'Arrival',
|
||||
watched: true,
|
||||
lastWatched: '2026-01-02',
|
||||
personalRating: 9,
|
||||
tags: 'mediaDB/tv/movie',
|
||||
});
|
||||
expect(movie.toMetaDataObject()).not.toHaveProperty('userData');
|
||||
});
|
||||
|
||||
test('legacy flat user data is migrated into model userData defaults', () => {
|
||||
const book = new BookModel({ title: 'Dune', read: true, personalRating: 8 } as never);
|
||||
|
||||
expect(book.userData).toEqual({ read: true, lastRead: '', personalRating: 8 });
|
||||
expect(book.type).toBe(MediaType.Book);
|
||||
});
|
||||
|
||||
test('models with custom tag and metadata behavior preserve their special cases', () => {
|
||||
const manga = new ComicMangaModel({ title: 'Berserk', subType: 'manga' });
|
||||
const wiki = new WikiModel({ title: 'Obsidian', article: 'Long article text' });
|
||||
const music = new MusicReleaseModel({ title: 'Kind of Blue', year: '1959', artists: ['Miles Davis'], subType: 'album' });
|
||||
const seasonSearchResult = new SeasonSearchResultModel({ seasonCount: 1 });
|
||||
|
||||
expect(manga.getTags()).toEqual(['mediaDB', 'manga']);
|
||||
expect(wiki.getWithOutUserData()).not.toHaveProperty('article');
|
||||
expect(music.getSummary()).toBe('Kind of Blue (1959) - Miles Davis');
|
||||
expect(seasonSearchResult.getSummary()).toBe('1 season');
|
||||
});
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { test, expect } from 'bun:test';
|
||||
|
||||
test('placeholder test', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
116
tests/property-mapping.test.ts
Normal file
116
tests/property-mapping.test.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { expect, test } from 'bun:test';
|
||||
import { PropertyMapper } from 'packages/obsidian/src/settings/PropertyMapper';
|
||||
import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from 'packages/obsidian/src/settings/PropertyMapping';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import { PropertyMappingNameConflictError, PropertyMappingValidationError } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
test('PropertyMapping validates locked and remapped property constraints', () => {
|
||||
expect(new PropertyMapping('title', 'title', PropertyMappingOption.Default, true).validate()).toEqual({ res: true });
|
||||
|
||||
const lockedRemoval = new PropertyMapping('title', 'title', PropertyMappingOption.Remove, true).validate();
|
||||
expect(lockedRemoval.res).toBe(false);
|
||||
expect(lockedRemoval.err).toBeInstanceOf(PropertyMappingValidationError);
|
||||
|
||||
const invalidSource = new PropertyMapping('bad property', 'name', PropertyMappingOption.Map).validate();
|
||||
expect(invalidSource.res).toBe(false);
|
||||
expect(invalidSource.err).toBeInstanceOf(PropertyMappingValidationError);
|
||||
|
||||
const invalidTarget = new PropertyMapping('title', 'bad-property', PropertyMappingOption.Map).validate();
|
||||
expect(invalidTarget.res).toBe(false);
|
||||
expect(invalidTarget.err).toBeInstanceOf(PropertyMappingValidationError);
|
||||
});
|
||||
|
||||
test('PropertyMappingModel detects conflicting mapped property names', () => {
|
||||
const duplicateTarget = new PropertyMappingModel(MediaType.Movie, [
|
||||
new PropertyMapping('title', 'name', PropertyMappingOption.Map),
|
||||
new PropertyMapping('englishTitle', 'name', PropertyMappingOption.Map),
|
||||
]).validate();
|
||||
|
||||
expect(duplicateTarget.res).toBe(false);
|
||||
expect(duplicateTarget.err).toBeInstanceOf(PropertyMappingNameConflictError);
|
||||
|
||||
const targetMatchesOriginal = new PropertyMappingModel(MediaType.Movie, [
|
||||
new PropertyMapping('title', 'year', PropertyMappingOption.Map),
|
||||
new PropertyMapping('year', 'year', PropertyMappingOption.Default),
|
||||
]).validate();
|
||||
|
||||
expect(targetMatchesOriginal.res).toBe(false);
|
||||
expect(targetMatchesOriginal.err).toBeInstanceOf(PropertyMappingNameConflictError);
|
||||
});
|
||||
|
||||
test('PropertyMappingModel serializes, copies, and migrates without sharing mutable property objects', () => {
|
||||
const model = new PropertyMappingModel(MediaType.Movie, [new PropertyMapping('title', 'name', PropertyMappingOption.Map, false, true)]);
|
||||
const copy = model.copy();
|
||||
|
||||
copy.properties[0].newProperty = 'label';
|
||||
|
||||
expect(model.properties[0].newProperty).toBe('name');
|
||||
expect(PropertyMappingModel.fromJSON(model.toJSON())).toEqual(model);
|
||||
|
||||
const migrated = PropertyMappingModel.migrateModels(
|
||||
[
|
||||
{
|
||||
type: MediaType.Movie,
|
||||
properties: [{ property: 'title', newProperty: 'customTitle', mapping: PropertyMappingOption.Map, locked: false, wikilink: true }],
|
||||
},
|
||||
],
|
||||
[
|
||||
new PropertyMappingModel(MediaType.Movie, [
|
||||
new PropertyMapping('title', 'title', PropertyMappingOption.Default, true),
|
||||
new PropertyMapping('year', 'year', PropertyMappingOption.Default, false),
|
||||
]),
|
||||
new PropertyMappingModel(MediaType.Book, [new PropertyMapping('author', 'author', PropertyMappingOption.Default)]),
|
||||
],
|
||||
);
|
||||
|
||||
expect(migrated).toEqual([
|
||||
new PropertyMappingModel(MediaType.Movie, [
|
||||
new PropertyMapping('title', 'customTitle', PropertyMappingOption.Map, true, true),
|
||||
new PropertyMapping('year', 'year', PropertyMappingOption.Default, false),
|
||||
]),
|
||||
new PropertyMappingModel(MediaType.Book, [new PropertyMapping('author', 'author', PropertyMappingOption.Default)]),
|
||||
]);
|
||||
});
|
||||
|
||||
test('PropertyMapper converts objects according to mapping and wikilink rules', () => {
|
||||
const mapper = new PropertyMapper({
|
||||
settings: {
|
||||
propertyMappingModels: [
|
||||
new PropertyMappingModel(MediaType.Movie, [
|
||||
new PropertyMapping('title', 'name', PropertyMappingOption.Map, false, true),
|
||||
new PropertyMapping('genres', 'genres', PropertyMappingOption.Default, false, true),
|
||||
new PropertyMapping('year', 'year', PropertyMappingOption.Remove),
|
||||
]),
|
||||
],
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(
|
||||
mapper.convertObject({
|
||||
type: MediaType.Movie,
|
||||
title: 'Arrival',
|
||||
genres: ['Drama', 2016],
|
||||
year: '2016',
|
||||
rating: 8,
|
||||
}),
|
||||
).toEqual({
|
||||
type: MediaType.Movie,
|
||||
name: '[[Arrival]]',
|
||||
genres: ['[[Drama]]', 2016],
|
||||
rating: 8,
|
||||
});
|
||||
});
|
||||
|
||||
test('PropertyMapper restores mapped properties and migrates legacy manga type', () => {
|
||||
const mapper = new PropertyMapper({
|
||||
settings: {
|
||||
propertyMappingModels: [new PropertyMappingModel(MediaType.ComicManga, [new PropertyMapping('title', 'name', PropertyMappingOption.Map)])],
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(mapper.convertObjectBack({ type: 'manga', name: 'Berserk', year: '1989' })).toEqual({
|
||||
type: MediaType.ComicManga,
|
||||
title: 'Berserk',
|
||||
year: '1989',
|
||||
});
|
||||
});
|
||||
40
tests/result.test.ts
Normal file
40
tests/result.test.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { expect, test } from 'bun:test';
|
||||
import { andThen, cancelled, err, failure, fromPromise, isErr, isOk, mapError, mapResult, ok, OutcomeStatus, skipped, success, tapError } from 'packages/obsidian/src/utils/result';
|
||||
import type { Result } from 'packages/obsidian/src/utils/result';
|
||||
|
||||
test('Result helpers construct and narrow ok and error values', () => {
|
||||
const good = ok(2);
|
||||
const bad = err('failed');
|
||||
|
||||
expect(isOk(good)).toBe(true);
|
||||
expect(isErr(good)).toBe(false);
|
||||
expect(isOk(bad)).toBe(false);
|
||||
expect(isErr(bad)).toBe(true);
|
||||
});
|
||||
|
||||
test('Result combinators map only the matching branch', () => {
|
||||
const good: Result<number, string> = ok(2);
|
||||
const bad: Result<number, string> = err('failed');
|
||||
let tappedError = '';
|
||||
|
||||
expect(mapResult(good, value => value * 2)).toEqual(ok(4));
|
||||
expect(mapResult<number, string, number>(bad, value => value * 2)).toBe(bad);
|
||||
expect(mapError<number, string, string>(good, error => error.toUpperCase())).toBe(good);
|
||||
expect(mapError(bad, error => error.toUpperCase())).toEqual(err('FAILED'));
|
||||
expect(andThen(good, value => ok(String(value)))).toEqual(ok('2'));
|
||||
expect(andThen(bad, value => ok(String(value)))).toBe(bad);
|
||||
expect(tapError(bad, error => (tappedError = error))).toBe(bad);
|
||||
expect(tappedError).toBe('failed');
|
||||
});
|
||||
|
||||
test('fromPromise captures resolved values and maps rejection causes', async () => {
|
||||
await expect(fromPromise(Promise.resolve('done'), String)).resolves.toEqual(ok('done'));
|
||||
await expect(fromPromise(Promise.reject(new Error('boom')), cause => (cause as Error).message)).resolves.toEqual(err('boom'));
|
||||
});
|
||||
|
||||
test('Outcome helpers construct the expected statuses', () => {
|
||||
expect(success('done')).toEqual({ status: OutcomeStatus.Ok, data: 'done' });
|
||||
expect(failure('bad')).toEqual({ status: OutcomeStatus.Error, error: 'bad' });
|
||||
expect(cancelled()).toEqual({ status: OutcomeStatus.Cancelled });
|
||||
expect(skipped()).toEqual({ status: OutcomeStatus.Skipped });
|
||||
});
|
||||
|
|
@ -1,5 +1,41 @@
|
|||
import { mock } from 'bun:test';
|
||||
|
||||
function parseScalar(value: string): unknown {
|
||||
if (value === 'true') return true;
|
||||
if (value === 'false') return false;
|
||||
if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
|
||||
return value.replace(/^['"]|['"]$/g, '');
|
||||
}
|
||||
|
||||
function parseSimpleYaml(yaml: string): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
for (const line of yaml.split('\n')) {
|
||||
const match = /^([^:#][^:]*):\s*(.*)$/.exec(line.trim());
|
||||
if (!match) continue;
|
||||
|
||||
const [, key, value] = match;
|
||||
result[key] =
|
||||
value.startsWith('[') && value.endsWith(']')
|
||||
? value
|
||||
.slice(1, -1)
|
||||
.split(',')
|
||||
.map(item => parseScalar(item.trim()))
|
||||
: parseScalar(value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function stringifySimpleYaml(value: unknown): string {
|
||||
if (!value || typeof value !== 'object') return String(value);
|
||||
|
||||
return Object.entries(value as Record<string, unknown>)
|
||||
.map(([key, entry]) => `${key}: ${Array.isArray(entry) ? `[${entry.join(', ')}]` : String(entry)}`)
|
||||
.join('\n')
|
||||
.concat('\n');
|
||||
}
|
||||
|
||||
mock.module('obsidian', () => ({
|
||||
AbstractInputSuggest: class {},
|
||||
Component: class {
|
||||
|
|
@ -22,14 +58,14 @@ mock.module('obsidian', () => ({
|
|||
Notice: class {},
|
||||
normalizePath: (path: string): string => path,
|
||||
moment: Object.assign((value?: unknown): unknown => value, { locale: (): void => {} }),
|
||||
parseYaml: (): unknown => ({}),
|
||||
parseYaml: parseSimpleYaml,
|
||||
Plugin: class {},
|
||||
PluginSettingTab: class {},
|
||||
requestUrl: async (): Promise<unknown> => ({}),
|
||||
SecretComponent: class {},
|
||||
Setting: class {},
|
||||
SettingGroup: class {},
|
||||
stringifyYaml: (value: unknown): string => String(value),
|
||||
stringifyYaml: stringifySimpleYaml,
|
||||
TFile: class {},
|
||||
TFolder: class {},
|
||||
TextComponent: class {},
|
||||
|
|
|
|||
56
tests/utils.test.ts
Normal file
56
tests/utils.test.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { expect, test } from 'bun:test';
|
||||
import { MovieModel } from 'packages/obsidian/src/models/MovieModel';
|
||||
import { markdownTable, migrateObject, replaceTags, wrapAround } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
test('replaceTags substitutes nested values and array operators', () => {
|
||||
const movie = new MovieModel({
|
||||
title: 'Alien',
|
||||
year: '1979',
|
||||
genres: ['Horror', 'Sci-Fi'],
|
||||
userData: { watched: true, lastWatched: '2026-01-02', personalRating: 9 },
|
||||
});
|
||||
|
||||
expect(replaceTags('{{ title }} ({{ year }}) - {{ userData.personalRating }}', movie)).toBe('Alien (1979) - 9');
|
||||
expect(replaceTags('{{ ENUM:genres }}', movie)).toBe('Horror, Sci-Fi');
|
||||
expect(replaceTags('{{ LIST:genres }}', movie)).toBe('- Horror\n- Sci-Fi');
|
||||
expect(replaceTags('{{ FIRST:genres }} / {{ LAST:genres }}', movie)).toBe('Horror / Sci-Fi');
|
||||
});
|
||||
|
||||
test('replaceTags reports invalid tags unless undefined values are ignored', () => {
|
||||
const movie = new MovieModel({ title: 'Alien' });
|
||||
|
||||
expect(replaceTags('{{ missing }}', movie)).toBe('{{ INVALID TEMPLATE TAG - object undefined }}');
|
||||
expect(replaceTags('{{ missing }}', movie, true)).toBe('');
|
||||
expect(replaceTags('{{ ENUM:title }}', movie)).toBe('{{ INVALID TEMPLATE TAG - operator ENUM is only applicable on an array }}');
|
||||
expect(replaceTags('{{ UNKNOWN:genres }}', movie)).toBe('{{ INVALID TEMPLATE TAG - unknown operator UNKNOWN }}');
|
||||
expect(replaceTags('{{ TOO:MANY:PARTS }}', movie)).toBe('{{ INVALID TEMPLATE TAG }}');
|
||||
});
|
||||
|
||||
test('markdownTable aligns columns and rejects jagged input', () => {
|
||||
expect(
|
||||
markdownTable([
|
||||
['Name', 'Year'],
|
||||
['Alien', '1979'],
|
||||
['Arrival', '2016'],
|
||||
]),
|
||||
).toBe('| Name | Year |\n| ------- | ---- |\n| Alien | 1979 |\n| Arrival | 2016 |\n');
|
||||
|
||||
expect(markdownTable([])).toBe('');
|
||||
expect(markdownTable([[]])).toBe('');
|
||||
expect(markdownTable([['Name'], ['Alien', '1979']])).toBe('');
|
||||
});
|
||||
|
||||
test('migrateObject keeps existing defined values and fills missing values from defaults', () => {
|
||||
const target = { title: '', year: '', rating: 0 };
|
||||
const defaults = { title: 'Untitled', year: '1900', rating: 1 };
|
||||
|
||||
migrateObject(target, { title: 'Arrival', year: null, rating: undefined }, defaults);
|
||||
|
||||
expect(target).toEqual({ title: 'Arrival', year: '1900', rating: 1 });
|
||||
});
|
||||
|
||||
test('wrapAround uses positive modulo and rejects invalid sizes', () => {
|
||||
expect(wrapAround(5, 3)).toBe(2);
|
||||
expect(wrapAround(-1, 3)).toBe(2);
|
||||
expect(() => wrapAround(1, 0)).toThrow('size may not be zero or negative');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue