Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@
<ng-template #header>
<h2 class="mr-2">{{ 'project.wiki.compare' | translate }}</h2>
</ng-template>
<div class="flex flex-wrap align-items-center mb-2 lg:flex-nowrap">
<span class="mr-2">Live preview to</span>

<div class="flex flex-wrap align-items-center mb-4 lg:flex-nowrap">
<span class="min-w-max mr-2">{{ 'project.wiki.livePreviewTo' | translate }}:</span>

<p-select
class="w-full"
styleClass="select-version"
[options]="mappedVersions()"
[ngModel]="selectedVersion"
(onChange)="onVersionChange($event.value)"
placeholder="Version"
class="w-full"
styleClass="select-version"
[placeholder]="'project.wiki.version.title' | translate"
/>
</div>

<p-panel styleClass="compare-view" showHeader="false">
<div lass="mt-3" [innerHTML]="content()"></div>
</p-panel>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideNoopAnimations } from '@angular/platform-browser/animations';

import { WikiVersion } from '@shared/models/wiki/wiki.model';

import { CompareSectionComponent } from './compare-section.component';

import { TranslateServiceMock } from '@testing/mocks/translate.service.mock';
import { OSFTestingModule } from '@testing/osf.testing.module';

describe('CompareSectionComponent', () => {
let component: CompareSectionComponent;
let fixture: ComponentFixture<CompareSectionComponent>;
let translateServiceMock: any;

const mockVersions: WikiVersion[] = [
{
Expand All @@ -22,32 +23,35 @@ describe('CompareSectionComponent', () => {
createdAt: '2024-01-02T10:00:00Z',
createdBy: 'Jane Smith',
},
{
id: 'version-3',
createdAt: '2024-01-03T10:00:00Z',
createdBy: 'Bob Johnson',
},
];

const mockVersionContent = 'Original content';
const mockPreviewContent = 'Updated content with changes';

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CompareSectionComponent],
providers: [TranslateServiceMock, provideNoopAnimations()],
imports: [CompareSectionComponent, OSFTestingModule],
providers: [TranslateServiceMock],
}).compileComponents();

translateServiceMock = TestBed.inject(TranslateServiceMock.provide);
translateServiceMock.instant.mockReturnValue('Current');

fixture = TestBed.createComponent(CompareSectionComponent);
component = fixture.componentInstance;

fixture.componentRef.setInput('versions', mockVersions);
fixture.componentRef.setInput('versionContent', mockVersionContent);
fixture.componentRef.setInput('previewContent', mockPreviewContent);
fixture.componentRef.setInput('isLoading', false);

fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should set versions input', () => {
expect(component.versions()).toEqual(mockVersions);
});
Expand All @@ -64,14 +68,60 @@ describe('CompareSectionComponent', () => {
expect(component.isLoading()).toBe(false);
});

it('should emit selectVersion when version changes', () => {
it('should handle empty versions array', () => {
fixture.componentRef.setInput('versions', []);
fixture.detectChanges();

expect(component.versions()).toEqual([]);
expect(component.selectedVersion).toBeUndefined();
});

it('should initialize with first version selected and emit selectVersion', () => {
expect(component.selectedVersion).toBe(mockVersions[0].id);
});

it('should not emit when no versions available', () => {
const emitSpy = jest.spyOn(component.selectVersion, 'emit');
const versionId = 'version-2';

component.onVersionChange(versionId);
fixture.componentRef.setInput('versions', []);
fixture.detectChanges();

expect(component.selectedVersion).toBe(versionId);
expect(emitSpy).toHaveBeenCalledWith(versionId);
expect(component.selectedVersion).toBeUndefined();
expect(emitSpy).not.toHaveBeenCalled();
});

it('should map versions correctly', () => {
const mappedVersions = component.mappedVersions();

expect(mappedVersions).toHaveLength(3);
expect(mappedVersions[0].value).toBe('version-1');
expect(mappedVersions[0].label).toContain('(Current)');
expect(mappedVersions[0].label).toContain('John Doe');
expect(mappedVersions[1].value).toBe('version-2');
expect(mappedVersions[1].label).toContain('(2)');
expect(mappedVersions[1].label).toContain('Jane Smith');
expect(mappedVersions[2].value).toBe('version-3');
expect(mappedVersions[2].label).toContain('(1)');
expect(mappedVersions[2].label).toContain('Bob Johnson');
});

it('should handle version with undefined createdBy', () => {
const versionsWithUndefinedCreator: WikiVersion[] = [
{
id: 'version-1',
createdAt: '2024-01-01T10:00:00Z',
createdBy: undefined,
},
];

fixture.componentRef.setInput('versions', versionsWithUndefinedCreator);
fixture.detectChanges();

const mappedVersions = component.mappedVersions();
expect(mappedVersions).toHaveLength(1);
expect(mappedVersions[0].value).toBe('version-1');
expect(mappedVersions[0].label).toContain('(Current)');
expect(mappedVersions[0].label).toContain('1/1/2024');
});

it('should handle single version', () => {
Expand All @@ -84,7 +134,61 @@ describe('CompareSectionComponent', () => {
expect(mappedVersions[0].label).toContain('(Current)');
});

it('should initialize with first version selected', () => {
expect(component.selectedVersion).toBe(mockVersions[0].id);
it('should compute content diff correctly', () => {
const content = component.content();

expect(content).toContain('<span class="removed">Original</span>');
expect(content).toContain('<span class="added">Updated</span>');
expect(content).toContain('content');
expect(content).toContain('<span class="added">with changes</span>');
});

it('should handle identical content', () => {
fixture.componentRef.setInput('previewContent', mockVersionContent);
fixture.detectChanges();

const content = component.content();
expect(content).toBe(mockVersionContent);
});

it('should handle empty version content', () => {
fixture.componentRef.setInput('versionContent', '');
fixture.detectChanges();

const content = component.content();
expect(content).toContain('<span class="added">Updated content with changes</span>');
});

it('should handle empty preview content', () => {
fixture.componentRef.setInput('previewContent', '');
fixture.detectChanges();

const content = component.content();
expect(content).toContain('<span class="removed">Original content</span>');
});

it('should update selectedVersion and emit selectVersion', () => {
const emitSpy = jest.spyOn(component.selectVersion, 'emit');
const versionId = 'version-2';

component.onVersionChange(versionId);

expect(component.selectedVersion).toBe(versionId);
expect(emitSpy).toHaveBeenCalledWith(versionId);
expect(emitSpy).toHaveBeenCalledTimes(1);
});

it('should emit correct version id when called multiple times', () => {
const emitSpy = jest.spyOn(component.selectVersion, 'emit');

component.onVersionChange('version-2');
component.onVersionChange('version-3');
component.onVersionChange('version-1');

expect(component.selectedVersion).toBe('version-1');
expect(emitSpy).toHaveBeenCalledTimes(3);
expect(emitSpy).toHaveBeenNthCalledWith(1, 'version-2');
expect(emitSpy).toHaveBeenNthCalledWith(2, 'version-3');
expect(emitSpy).toHaveBeenNthCalledWith(3, 'version-1');
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { TranslatePipe } from '@ngx-translate/core';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';

import { Panel } from 'primeng/panel';
import { Select } from 'primeng/select';
import { Skeleton } from 'primeng/skeleton';

import { ChangeDetectionStrategy, Component, computed, effect, input, output } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, effect, inject, input, output } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { WikiVersion } from '@osf/shared/models/wiki/wiki.model';
Expand All @@ -25,20 +25,23 @@ export class CompareSectionComponent {
isLoading = input.required<boolean>();
selectVersion = output<string>();

translateService = inject(TranslateService);

selectedVersion: string | null = null;

private readonly currentLabel = this.translateService.instant('project.wiki.version.current');
private readonly unknownAuthorLabel = this.translateService.instant('project.wiki.version.unknownAuthor');

mappedVersions = computed(() => [
...this.versions().map((version, index) => {
const labelPrefix = index === 0 ? '(Current)' : `(${this.versions().length - index})`;
return {
label: `${labelPrefix} ${version.createdBy}: (${new Date(version.createdAt).toLocaleString()})`,
value: version.id,
};
}),
...this.versions().map((version, index) => ({
label: this.formatVersionLabel(version, index),
value: version.id,
})),
]);

content = computed(() => {
const changes = Diff.diffWords(this.versionContent(), this.previewContent());

return changes
.map((change) => {
if (change.added) {
Expand All @@ -54,13 +57,21 @@ export class CompareSectionComponent {
constructor() {
effect(() => {
this.selectedVersion = this.versions()[0]?.id;

if (this.selectedVersion) {
this.selectVersion.emit(this.selectedVersion);
}
});
}

onVersionChange(versionId: string): void {
this.selectedVersion = versionId;
this.selectVersion.emit(versionId);
}

private formatVersionLabel(version: WikiVersion, index: number): string {
const prefix = index === 0 ? `(${this.currentLabel})` : `(${this.versions().length - index})`;
const creator = version.createdBy || this.unknownAuthorLabel;
return `${prefix} ${creator}: (${new Date(version.createdAt).toLocaleString()})`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@
<ng-template #header>
<h2 class="mr-2">{{ 'project.wiki.view' | translate }}</h2>
</ng-template>

<div class="flex flex-wrap align-items-center mb-4 lg:flex-nowrap">
<span class="mr-2">{{ 'project.wiki.version.title' | translate }}:</span>

<p-select
class="w-full"
styleClass="select-version"
[options]="mappedVersions()"
[ngModel]="selectedVersion()"
(onChange)="onVersionChange($event.value)"
[placeholder]="'project.wiki.version.select' | translate"
class="w-full"
styleClass="select-version"
/>
</div>

<p-panel class="view-panel" showHeader="false">
<div class="editor-view-mode mt-3">
@if (content()) {
Expand Down
Loading