mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
[#66563] Migrate specs to Vitest
Apply Angular's Vitest migration schematic to update frontend and plugin
specs from Jasmine globals to Vitest APIs.
ng g @schematics/angular:refactor-jasmine-vitest
Fix migrated edge cases where async assertions or shallow tests changed.
https://community.openproject.org/wp/66563
This commit is contained in:
@@ -26,9 +26,7 @@
|
||||
// See COPYRIGHT and LICENSE files for more details.
|
||||
//++
|
||||
|
||||
import {
|
||||
TestBed,
|
||||
} from '@angular/core/testing';
|
||||
import { TestBed, } from '@angular/core/testing';
|
||||
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
|
||||
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
|
||||
import { States } from 'core-app/core/states/states.service';
|
||||
@@ -58,7 +56,7 @@ describe('APIv3Service', () => {
|
||||
expect(service.projects.id(projectIdentifier).path).toEqual('/api/v3/projects/majora');
|
||||
});
|
||||
|
||||
it('should provide a path to work package query on subject or ID ', () => {
|
||||
it('should provide a path to work package query on subject or ID', () => {
|
||||
let params = {
|
||||
filters: '[{"typeahead":{"operator":"**","values":["bogus"]}}]',
|
||||
sortBy: '[["updatedAt","desc"]]',
|
||||
@@ -66,9 +64,7 @@ describe('APIv3Service', () => {
|
||||
pageSize: '10',
|
||||
};
|
||||
|
||||
expect(
|
||||
service.work_packages.filterByTypeaheadOrId('bogus').path,
|
||||
).toEqual(`/api/v3/work_packages?${encodeParams(params)}`);
|
||||
expect(service.work_packages.filterByTypeaheadOrId('bogus').path).toEqual(`/api/v3/work_packages?${encodeParams(params)}`);
|
||||
|
||||
params = {
|
||||
filters: '[{"id":{"operator":"=","values":["1234"]}}]',
|
||||
@@ -77,9 +73,7 @@ describe('APIv3Service', () => {
|
||||
pageSize: '10',
|
||||
};
|
||||
|
||||
expect(
|
||||
service.work_packages.filterByTypeaheadOrId('1234', true).path,
|
||||
).toEqual(`/api/v3/work_packages?${encodeParams(params)}`);
|
||||
expect(service.work_packages.filterByTypeaheadOrId('1234', true).path).toEqual(`/api/v3/work_packages?${encodeParams(params)}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,7 +35,8 @@ import { WorkPackageResource } from 'core-app/features/hal/resources/work-packag
|
||||
import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service';
|
||||
import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service';
|
||||
import { States } from 'core-app/core/states/states.service';
|
||||
import { take, takeWhile } from 'rxjs/operators';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { take, toArray } from 'rxjs/operators';
|
||||
import { WorkPackagesActivityService } from 'core-app/features/work-packages/components/wp-single-view-tabs/activity-panel/wp-activity.service';
|
||||
import { ConfigurationService } from 'core-app/core/config/configuration.service';
|
||||
import { WorkPackageNotificationService } from 'core-app/features/work-packages/services/notifications/work-package-notification.service';
|
||||
@@ -55,8 +56,8 @@ describe('WorkPackageCache', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [OpenprojectHalModule],
|
||||
providers: [
|
||||
imports: [OpenprojectHalModule],
|
||||
providers: [
|
||||
States,
|
||||
HalResourceService,
|
||||
TimezoneService,
|
||||
@@ -64,15 +65,15 @@ describe('WorkPackageCache', () => {
|
||||
SchemaCacheService,
|
||||
PathHelperService,
|
||||
{ provide: ConfigurationService, useValue: {} },
|
||||
{ provide: I18nService, useValue: { t: (...args:any[]) => 'translation' } },
|
||||
{ provide: I18nService, useValue: { t: (..._args:any[]) => 'translation' } },
|
||||
{ provide: WorkPackageResource, useValue: {} },
|
||||
{ provide: ToastService, useValue: {} },
|
||||
{ provide: HalResourceNotificationService, useValue: { handleRawError: () => false } },
|
||||
{ provide: WorkPackageNotificationService, useValue: {} },
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
]
|
||||
});
|
||||
]
|
||||
});
|
||||
|
||||
injector = TestBed.inject(Injector);
|
||||
states = TestBed.inject(States);
|
||||
@@ -80,55 +81,40 @@ describe('WorkPackageCache', () => {
|
||||
workPackageCache = new WorkPackageCache(injector, states.workPackages);
|
||||
|
||||
// sinon.stub(schemaCacheService, 'ensureLoaded').returns(Promise.resolve(true));
|
||||
spyOn(schemaCacheService, 'ensureLoaded').and.resolveTo(true as any);
|
||||
vi.spyOn(schemaCacheService, 'ensureLoaded').mockResolvedValue(true as any);
|
||||
|
||||
const workPackage1 = new WorkPackageResource(
|
||||
injector,
|
||||
{
|
||||
id: '1',
|
||||
_links: {
|
||||
self: '',
|
||||
},
|
||||
const workPackage1 = new WorkPackageResource(injector, {
|
||||
id: '1',
|
||||
_links: {
|
||||
self: '',
|
||||
},
|
||||
true,
|
||||
(wp:WorkPackageResource) => undefined,
|
||||
'WorkPackage',
|
||||
);
|
||||
}, true, (_wp:WorkPackageResource) => undefined, 'WorkPackage');
|
||||
|
||||
dummyWorkPackages = [workPackage1 as any];
|
||||
});
|
||||
|
||||
it('returns a work package after the list has been initialized', (done:any) => {
|
||||
workPackageCache.state('1').values$()
|
||||
.pipe(
|
||||
take(1),
|
||||
)
|
||||
.subscribe((wp:WorkPackageResource) => {
|
||||
expect(wp.id!).toEqual('1');
|
||||
done();
|
||||
});
|
||||
it('returns a work package after the list has been initialized', async () => {
|
||||
const emittedWorkPackage = firstValueFrom(
|
||||
workPackageCache.state('1').values$().pipe(take(1)),
|
||||
);
|
||||
|
||||
workPackageCache.updateWorkPackageList(dummyWorkPackages);
|
||||
|
||||
await expect(emittedWorkPackage).resolves.toMatchObject({ id: '1' });
|
||||
});
|
||||
|
||||
it('should return/stream a work package every time it gets updated', (done:any) => {
|
||||
let count = 0;
|
||||
|
||||
workPackageCache.state('1').values$()
|
||||
.pipe(
|
||||
takeWhile((wp) => count < 2),
|
||||
)
|
||||
.subscribe((wp:WorkPackageResource) => {
|
||||
expect(wp.id!).toEqual('1');
|
||||
|
||||
count += 1;
|
||||
if (count === 2) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
it('should return/stream a work package every time it gets updated', async () => {
|
||||
const emittedWorkPackages = firstValueFrom(
|
||||
workPackageCache.state('1').values$().pipe(take(3), toArray()),
|
||||
);
|
||||
|
||||
workPackageCache.updateWorkPackageList([dummyWorkPackages[0]], false);
|
||||
workPackageCache.updateWorkPackageList([dummyWorkPackages[0]], false);
|
||||
workPackageCache.updateWorkPackageList([dummyWorkPackages[0]], false);
|
||||
|
||||
const values = await emittedWorkPackages;
|
||||
|
||||
expect(values).toHaveLength(3);
|
||||
expect(values.map((wp) => wp.id)).toEqual(['1', '1', '1']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,8 @@ import { OpenprojectHalModule } from 'core-app/features/hal/openproject-hal.modu
|
||||
import { HalLink, HalLinkInterface } from 'core-app/features/hal/hal-link/hal-link';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import Spy = jasmine.Spy;
|
||||
import type { Mock } from 'vitest';
|
||||
type Spy = Mock;
|
||||
|
||||
describe('HalResource', () => {
|
||||
let halResourceService:HalResourceService;
|
||||
@@ -45,21 +46,21 @@ describe('HalResource', () => {
|
||||
|
||||
let source:any;
|
||||
let resource:HalResource;
|
||||
|
||||
class OtherResource extends HalResource {
|
||||
}
|
||||
let OtherResource:typeof HalResource;
|
||||
|
||||
beforeEach(async () => {
|
||||
OtherResource = class extends HalResource {};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [OpenprojectHalModule],
|
||||
providers: [
|
||||
imports: [OpenprojectHalModule],
|
||||
providers: [
|
||||
HalResourceService,
|
||||
States,
|
||||
I18nService,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
]
|
||||
}).compileComponents();
|
||||
]
|
||||
}).compileComponents();
|
||||
halResourceService = TestBed.inject(HalResourceService);
|
||||
injector = TestBed.inject(Injector);
|
||||
});
|
||||
@@ -71,7 +72,7 @@ describe('HalResource', () => {
|
||||
});
|
||||
|
||||
describe('when updating a loaded resource using `$update()`', () => {
|
||||
let getStub:jasmine.Spy;
|
||||
let getStub:Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
source = {
|
||||
@@ -82,7 +83,7 @@ describe('HalResource', () => {
|
||||
},
|
||||
};
|
||||
|
||||
getStub = spyOn(halResourceService, 'request').and.callFake((verb:string, path:string) => {
|
||||
getStub = vi.spyOn(halResourceService, 'request').mockImplementation((verb:string, path:string) => {
|
||||
if (verb === 'get' && path === '/api/hello') {
|
||||
return of(halResourceService.createHalResource(source)) as any;
|
||||
}
|
||||
@@ -106,7 +107,7 @@ describe('HalResource', () => {
|
||||
});
|
||||
|
||||
it('should be an instance of HalResource', () => {
|
||||
expect(resource).toEqual(jasmine.any(HalResource));
|
||||
expect(resource).toEqual(expect.any(HalResource));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,19 +122,16 @@ describe('HalResource', () => {
|
||||
},
|
||||
};
|
||||
|
||||
halResourceService.registerResource(
|
||||
'Other',
|
||||
{ cls: OtherResource, attrTypes: { someResource: 'Other' } },
|
||||
);
|
||||
halResourceService.registerResource('Other', { cls: OtherResource, attrTypes: { someResource: 'Other' } });
|
||||
resource = halResourceService.createHalResource(source, false);
|
||||
});
|
||||
|
||||
it('should be an instance of that type', () => {
|
||||
expect(resource).toEqual(jasmine.any(OtherResource));
|
||||
expect(resource).toEqual(expect.any(OtherResource));
|
||||
});
|
||||
|
||||
it('should have an attribute that is of the configured instance', () => {
|
||||
expect(resource.someResource).toEqual(jasmine.any(OtherResource));
|
||||
expect(resource.someResource).toEqual(expect.any(OtherResource));
|
||||
});
|
||||
|
||||
it('should not be loaded', () => {
|
||||
@@ -147,8 +145,8 @@ describe('HalResource', () => {
|
||||
let embeddedFn:Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
linkFn = jasmine.createSpy();
|
||||
embeddedFn = jasmine.createSpy();
|
||||
linkFn = vi.fn();
|
||||
embeddedFn = vi.fn();
|
||||
|
||||
resource = halResourceService.createHalResource({
|
||||
_links: {
|
||||
@@ -167,25 +165,25 @@ describe('HalResource', () => {
|
||||
});
|
||||
|
||||
it('should not have touched the source links initially', () => {
|
||||
expect(linkFn.calls.count()).toEqual(0);
|
||||
expect(vi.mocked(linkFn).mock.calls.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should not have touched the embedded elements of the source initially', () => {
|
||||
expect(embeddedFn.calls.count()).toEqual(0);
|
||||
expect(vi.mocked(embeddedFn).mock.calls.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should use the source link only once when called', () => {
|
||||
resource.link;
|
||||
resource.link;
|
||||
|
||||
expect(linkFn.calls.count()).toEqual(1);
|
||||
expect(vi.mocked(linkFn).mock.calls.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should use the source embedded only once when called', () => {
|
||||
resource.resource;
|
||||
resource.resource;
|
||||
|
||||
expect(embeddedFn.calls.count()).toEqual(1);
|
||||
expect(vi.mocked(embeddedFn).mock.calls.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -377,7 +375,7 @@ describe('HalResource', () => {
|
||||
});
|
||||
|
||||
it('should have a callable self link', () => {
|
||||
spyOn(halResourceService, 'request').and.callFake((verb:string, path:string) => {
|
||||
vi.spyOn(halResourceService, 'request').mockImplementation((verb:string, path:string) => {
|
||||
if (verb === 'get' && path === 'unicorn/69') {
|
||||
return of(halResourceService.createHalResource({})) as any;
|
||||
}
|
||||
@@ -388,7 +386,7 @@ describe('HalResource', () => {
|
||||
});
|
||||
|
||||
it('should have a callable beaver', () => {
|
||||
spyOn(halResourceService, 'request').and.callFake((verb:string, path:string) => {
|
||||
vi.spyOn(halResourceService, 'request').mockImplementation((verb:string, path:string) => {
|
||||
if (verb === 'get' && path === 'justin/420') {
|
||||
return of(halResourceService.createHalResource({})) as any;
|
||||
}
|
||||
@@ -632,7 +630,8 @@ describe('HalResource', () => {
|
||||
it('should not be possible to override a link', () => {
|
||||
try {
|
||||
resource.$links.action = 'foo';
|
||||
} catch (ignore) {
|
||||
}
|
||||
catch (ignore) {
|
||||
/**/
|
||||
}
|
||||
|
||||
@@ -642,7 +641,8 @@ describe('HalResource', () => {
|
||||
it('should not be possible to override an embedded resource', () => {
|
||||
try {
|
||||
resource.$embedded.embedded = 'foo';
|
||||
} catch (ignore) {
|
||||
}
|
||||
catch (ignore) {
|
||||
/**/
|
||||
}
|
||||
|
||||
@@ -713,18 +713,18 @@ describe('HalResource', () => {
|
||||
});
|
||||
|
||||
describe('when loading it', () => {
|
||||
let getStub:jasmine.Spy;
|
||||
let getStub:Mock;
|
||||
let newResult:any;
|
||||
let promise:Promise<any>;
|
||||
|
||||
beforeEach((done) => {
|
||||
beforeEach(async () => {
|
||||
const result = halResourceService.createHalResource({
|
||||
_links: {},
|
||||
name: 'name',
|
||||
foo: 'bar',
|
||||
});
|
||||
|
||||
getStub = spyOn(halResourceService, 'request').and.callFake((verb:string, path:string) => {
|
||||
getStub = vi.spyOn(halResourceService, 'request').mockImplementation((verb:string, path:string) => {
|
||||
if (verb === 'get' && path === '/api/property') {
|
||||
return of(result) as any;
|
||||
}
|
||||
@@ -737,14 +737,13 @@ describe('HalResource', () => {
|
||||
});
|
||||
|
||||
expect(getStub).toHaveBeenCalled();
|
||||
done();
|
||||
;
|
||||
});
|
||||
|
||||
it('should be loaded', (done) => {
|
||||
promise.then(() => {
|
||||
expect(resource.$loaded).toBeTruthy();
|
||||
done();
|
||||
}).catch(done.fail);
|
||||
it('should be loaded', async () => {
|
||||
await promise;
|
||||
|
||||
expect(resource.$loaded).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should be updated', () => {
|
||||
@@ -754,10 +753,10 @@ describe('HalResource', () => {
|
||||
it('should have properties that have a getter and setter', () => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(newResult, 'foo');
|
||||
|
||||
expect(descriptor).toBeDefined('Descriptor should be defined');
|
||||
expect(descriptor).toBeDefined();
|
||||
|
||||
expect(descriptor!.get).toBeDefined('Descriptor getter should be defined');
|
||||
expect(descriptor!.set).toBeDefined('Descriptor setter should be defined');
|
||||
expect(descriptor!.get).toBeDefined();
|
||||
expect(descriptor!.set).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return itself in a promise if already loaded', () => {
|
||||
|
||||
@@ -70,8 +70,8 @@ describe('WorkPackage', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [OpenprojectHalModule],
|
||||
providers: [
|
||||
imports: [OpenprojectHalModule],
|
||||
providers: [
|
||||
HalResourceService,
|
||||
States,
|
||||
TimezoneService,
|
||||
@@ -89,8 +89,8 @@ describe('WorkPackage', () => {
|
||||
{ provide: SchemaCacheService, useValue: {} },
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
]
|
||||
}).compileComponents();
|
||||
]
|
||||
}).compileComponents();
|
||||
halResourceService = TestBed.inject(HalResourceService);
|
||||
injector = TestBed.inject(Injector);
|
||||
halResourceNotification = injector.get(HalResourceNotificationService);
|
||||
@@ -109,7 +109,7 @@ describe('WorkPackage', () => {
|
||||
beforeEach(createWorkPackage);
|
||||
|
||||
it('should have an attachments property of type `AttachmentCollectionResource`', () => {
|
||||
expect(workPackage.attachments).toEqual(jasmine.any(AttachmentCollectionResource));
|
||||
expect(workPackage.attachments).toEqual(expect.any(AttachmentCollectionResource));
|
||||
});
|
||||
|
||||
it('should return true for `isNewResource`', () => {
|
||||
@@ -190,11 +190,12 @@ describe('WorkPackage', () => {
|
||||
|
||||
it('surfaces the semantic displayId on each ancestor resource', () => {
|
||||
const ancestor = (workPackage as any).ancestors[0] as WorkPackageResource;
|
||||
|
||||
expect(ancestor.displayId).toEqual('ACSMT-15');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('formattedId', () => {
|
||||
afterEach(() => {
|
||||
@@ -215,7 +216,7 @@ describe('WorkPackage', () => {
|
||||
expect(workPackage.formattedId).toEqual('#42');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('subjectWithId', () => {
|
||||
afterEach(() => {
|
||||
|
||||
@@ -31,8 +31,8 @@ import { HookService } from 'core-app/features/plugins/hook-service';
|
||||
describe('HookService', () => {
|
||||
let service:HookService = new HookService();
|
||||
|
||||
let callback:any; let
|
||||
invalidCallback:any;
|
||||
let callback:any;
|
||||
let invalidCallback:any;
|
||||
const validId = 'myValidCallbacks';
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -80,7 +80,7 @@ describe('HookService', () => {
|
||||
|
||||
describe('valid function callback registered', () => {
|
||||
beforeEach(() => {
|
||||
callback = jasmine.createSpy('hook');
|
||||
callback = vi.fn();
|
||||
service.register('myValidCallbacks', callback);
|
||||
});
|
||||
|
||||
@@ -93,7 +93,7 @@ describe('HookService', () => {
|
||||
describe('call', () => {
|
||||
describe('function that returns undefined', () => {
|
||||
beforeEach(() => {
|
||||
callback = jasmine.createSpy('hook');
|
||||
callback = vi.fn();
|
||||
service.register('myValidCallbacks', callback);
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ describe('HookService', () => {
|
||||
|
||||
describe('function that returns something that is not undefined', () => {
|
||||
beforeEach(() => {
|
||||
callback = jasmine.createSpy('hook').and.returnValue({});
|
||||
callback = vi.fn().mockReturnValue({});
|
||||
|
||||
service.register('myValidCallbacks', callback);
|
||||
});
|
||||
@@ -116,7 +116,7 @@ describe('HookService', () => {
|
||||
|
||||
describe('function that returns something that is not undefined', () => {
|
||||
beforeEach(() => {
|
||||
callback = jasmine.createSpy('hook').and.returnValue({});
|
||||
callback = vi.fn().mockReturnValue({});
|
||||
|
||||
service.register('myValidCallbacks', callback);
|
||||
});
|
||||
@@ -128,8 +128,8 @@ describe('HookService', () => {
|
||||
|
||||
describe('function that returns something that is not undefined', () => {
|
||||
beforeEach(() => {
|
||||
callback = jasmine.createSpy('hook');
|
||||
invalidCallback = jasmine.createSpy('invalidHook');
|
||||
callback = vi.fn();
|
||||
invalidCallback = vi.fn();
|
||||
|
||||
service.register('myValidCallbacks', callback);
|
||||
|
||||
@@ -142,12 +142,12 @@ describe('HookService', () => {
|
||||
});
|
||||
|
||||
describe('function that returns something that is not undefined', () => {
|
||||
let callback1; let
|
||||
callback2;
|
||||
let callback1;
|
||||
let callback2;
|
||||
|
||||
beforeEach(() => {
|
||||
callback1 = jasmine.createSpy('hook1').and.returnValue({});
|
||||
callback2 = jasmine.createSpy('hook1').and.returnValue({});
|
||||
callback1 = vi.fn().mockReturnValue({});
|
||||
callback2 = vi.fn().mockReturnValue({});
|
||||
|
||||
service.register('myValidCallbacks', callback1);
|
||||
service.register('myValidCallbacks', callback2);
|
||||
|
||||
+6
-12
@@ -73,8 +73,8 @@ describe('WorkPackageFilterValues', () => {
|
||||
function setupTestBed() {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
void TestBed.configureTestingModule({
|
||||
imports: [UIRouterModule.forRoot({})],
|
||||
providers: [
|
||||
imports: [UIRouterModule.forRoot({})],
|
||||
providers: [
|
||||
I18nService,
|
||||
{ provide: WeekdayService, useValue: WeekdayServiceStub },
|
||||
States,
|
||||
@@ -95,8 +95,8 @@ describe('WorkPackageFilterValues', () => {
|
||||
HalResourceEditingService,
|
||||
WorkPackagesActivityService,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
]
|
||||
}).compileComponents();
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
injector = TestBed.inject(Injector);
|
||||
halResourceService = injector.get(HalResourceService);
|
||||
@@ -104,14 +104,8 @@ describe('WorkPackageFilterValues', () => {
|
||||
resource = halResourceService.createHalResourceOfClass(WorkPackageResource, source, true);
|
||||
changeset = new WorkPackageChangeset(resource);
|
||||
|
||||
const type1 = halResourceService.createHalResourceOfClass(
|
||||
TypeResource,
|
||||
{ _type: 'Type', id: '1', _links: { self: { href: '/api/v3/types/1', name: 'Task' } } },
|
||||
);
|
||||
const type2 = halResourceService.createHalResourceOfClass(
|
||||
TypeResource,
|
||||
{ _type: 'Type', id: '2', _links: { self: { href: '/api/v3/types/2', name: 'Bug' } } },
|
||||
);
|
||||
const type1 = halResourceService.createHalResourceOfClass(TypeResource, { _type: 'Type', id: '1', _links: { self: { href: '/api/v3/types/1', name: 'Task' } } });
|
||||
const type2 = halResourceService.createHalResourceOfClass(TypeResource, { _type: 'Type', id: '2', _links: { self: { href: '/api/v3/types/2', name: 'Bug' } } });
|
||||
|
||||
filters = [
|
||||
{
|
||||
|
||||
+6
-6
@@ -79,7 +79,7 @@ describe('keepTab service', () => {
|
||||
let currentPathPrefix = 'work-packages.show.*';
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn($state, 'includes').and.callFake((path:string) => path === currentPathPrefix);
|
||||
vi.spyOn($state, 'includes').mockImplementation((path:string) => path === currentPathPrefix);
|
||||
|
||||
$state.current.name = 'work-packages.show.tabs';
|
||||
uiRouterGlobals.params.tabIdentifier = 'relations';
|
||||
@@ -95,7 +95,7 @@ describe('keepTab service', () => {
|
||||
});
|
||||
|
||||
it('should propagate the previous change', () => {
|
||||
const cb = jasmine.createSpy();
|
||||
const cb = vi.fn();
|
||||
|
||||
const expected = {
|
||||
active: 'relations',
|
||||
@@ -121,7 +121,7 @@ describe('keepTab service', () => {
|
||||
|
||||
describe('when opening show#activity', () => {
|
||||
beforeEach(() => {
|
||||
spyOn($state, 'includes').and.callFake((path:string) => path === 'work-packages.show.*');
|
||||
vi.spyOn($state, 'includes').mockImplementation((path:string) => path === 'work-packages.show.*');
|
||||
|
||||
uiRouterGlobals.params.tabIdentifier = 'activity';
|
||||
$state.current.name = 'work-packages.show.tabs';
|
||||
@@ -135,7 +135,7 @@ describe('keepTab service', () => {
|
||||
|
||||
describe('when opening a details route', () => {
|
||||
beforeEach(() => {
|
||||
spyOn($state, 'includes').and.callFake((path:string) => path === '**.details.*');
|
||||
vi.spyOn($state, 'includes').mockImplementation((path:string) => path === '**.details.*');
|
||||
|
||||
uiRouterGlobals.params.tabIdentifier = 'activity';
|
||||
$state.current.name = 'work-packages.partitioned.list.details.tabs';
|
||||
@@ -151,7 +151,7 @@ describe('keepTab service', () => {
|
||||
});
|
||||
|
||||
it('should propagate the previous and next change', () => {
|
||||
const cb = jasmine.createSpy();
|
||||
const cb = vi.fn();
|
||||
|
||||
const expected = {
|
||||
active: 'activity',
|
||||
@@ -165,7 +165,7 @@ describe('keepTab service', () => {
|
||||
|
||||
keepTab.updateTabs();
|
||||
|
||||
expect(cb.calls.count()).toEqual(2);
|
||||
expect(vi.mocked(cb).mock.calls.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+59
-65
@@ -52,14 +52,11 @@ function setupMocks(paginationService:PaginationService) {
|
||||
optionsTruncationSize: 6,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line jasmine/no-unsafe-spy
|
||||
spyOn(paginationService, 'getMaxVisiblePageOptions').and.callFake(() => options.maxVisiblePageOptions);
|
||||
vi.spyOn(paginationService, 'getMaxVisiblePageOptions').mockImplementation(() => options.maxVisiblePageOptions);
|
||||
|
||||
// eslint-disable-next-line jasmine/no-unsafe-spy
|
||||
spyOn(paginationService, 'getOptionsTruncationSize').and.callFake(() => options.optionsTruncationSize);
|
||||
vi.spyOn(paginationService, 'getOptionsTruncationSize').mockImplementation(() => options.optionsTruncationSize);
|
||||
|
||||
// eslint-disable-next-line jasmine/no-unsafe-spy
|
||||
spyOn(paginationService, 'getPaginationOptions').and.callFake(() => options);
|
||||
vi.spyOn(paginationService, 'getPaginationOptions').mockImplementation(() => options);
|
||||
}
|
||||
|
||||
function pageString(element:HTMLElement) {
|
||||
@@ -75,12 +72,12 @@ describe('wpTablePagination Directive', () => {
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
declarations: [
|
||||
WorkPackageTablePaginationComponent,
|
||||
OpIconComponent,
|
||||
],
|
||||
imports: [],
|
||||
providers: [
|
||||
],
|
||||
imports: [],
|
||||
providers: [
|
||||
States,
|
||||
PaginationService,
|
||||
WorkPackageViewSortByService,
|
||||
@@ -92,80 +89,77 @@ describe('wpTablePagination Directive', () => {
|
||||
IsolatedQuerySpace,
|
||||
I18nService,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
]
|
||||
}).compileComponents();
|
||||
]
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
describe('page ranges and links', () => {
|
||||
it('should display the correct page range',
|
||||
inject([PaginationService], (paginationService:PaginationService) => {
|
||||
setupMocks(paginationService);
|
||||
const fixture = TestBed.createComponent(WorkPackageTablePaginationComponent);
|
||||
const app:WorkPackageTablePaginationComponent = fixture.debugElement.componentInstance;
|
||||
const element = fixture.elementRef.nativeElement;
|
||||
it('should display the correct page range', inject([PaginationService], (paginationService:PaginationService) => {
|
||||
setupMocks(paginationService);
|
||||
const fixture = TestBed.createComponent(WorkPackageTablePaginationComponent);
|
||||
const app:WorkPackageTablePaginationComponent = fixture.debugElement.componentInstance;
|
||||
const element = fixture.elementRef.nativeElement;
|
||||
|
||||
app.pagination = new PaginationInstance(1, 0, 10);
|
||||
app.update();
|
||||
fixture.detectChanges();
|
||||
app.pagination = new PaginationInstance(1, 0, 10);
|
||||
app.update();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(pageString(element)).toEqual('');
|
||||
expect(pageString(element)).toEqual('');
|
||||
|
||||
app.pagination = new PaginationInstance(1, 11, 10);
|
||||
app.update();
|
||||
fixture.detectChanges();
|
||||
app.pagination = new PaginationInstance(1, 11, 10);
|
||||
app.update();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(pageString(element)).toEqual('(1 - 10/11)');
|
||||
}));
|
||||
expect(pageString(element)).toEqual('(1 - 10/11)');
|
||||
}));
|
||||
|
||||
describe('"next" link', () => {
|
||||
it('hidden on the last page',
|
||||
inject([PaginationService], (paginationService:PaginationService) => {
|
||||
setupMocks(paginationService);
|
||||
const fixture = TestBed.createComponent(WorkPackageTablePaginationComponent);
|
||||
const app:WorkPackageTablePaginationComponent = fixture.debugElement.componentInstance;
|
||||
const element = fixture.elementRef.nativeElement;
|
||||
|
||||
app.pagination = new PaginationInstance(2, 11, 10);
|
||||
app.update();
|
||||
fixture.detectChanges();
|
||||
|
||||
const liWithNextLink = element.querySelector('.op-pagination--item-link_next')?.parentElement;
|
||||
|
||||
expect(liWithNextLink?.matches('li')).toBeTrue();
|
||||
const attrHidden = liWithNextLink.getAttribute('hidden');
|
||||
|
||||
expect(attrHidden).toBeDefined();
|
||||
}));
|
||||
});
|
||||
|
||||
it('should display correct number of page number links',
|
||||
inject([PaginationService], (paginationService:PaginationService) => {
|
||||
it('hidden on the last page', inject([PaginationService], (paginationService:PaginationService) => {
|
||||
setupMocks(paginationService);
|
||||
const fixture = TestBed.createComponent(WorkPackageTablePaginationComponent);
|
||||
const app:WorkPackageTablePaginationComponent = fixture.debugElement.componentInstance;
|
||||
const element = fixture.elementRef.nativeElement;
|
||||
|
||||
function numberOfPageNumberLinks() {
|
||||
return element.querySelectorAll('button[data-rel="next"]').length;
|
||||
}
|
||||
|
||||
app.pagination = new PaginationInstance(1, 1, 10);
|
||||
app.pagination = new PaginationInstance(2, 11, 10);
|
||||
app.update();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(numberOfPageNumberLinks()).toEqual(1);
|
||||
const liWithNextLink = element.querySelector('.op-pagination--item-link_next')?.parentElement;
|
||||
|
||||
app.pagination = new PaginationInstance(1, 11, 10);
|
||||
app.update();
|
||||
fixture.detectChanges();
|
||||
expect(liWithNextLink?.matches('li')).toBe(true);
|
||||
const attrHidden = liWithNextLink.getAttribute('hidden');
|
||||
|
||||
expect(numberOfPageNumberLinks()).toEqual(2);
|
||||
|
||||
app.pagination = new PaginationInstance(1, 59, 10);
|
||||
app.update();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(numberOfPageNumberLinks()).toEqual(6);
|
||||
expect(attrHidden).toBeDefined();
|
||||
}));
|
||||
});
|
||||
|
||||
it('should display correct number of page number links', inject([PaginationService], (paginationService:PaginationService) => {
|
||||
setupMocks(paginationService);
|
||||
const fixture = TestBed.createComponent(WorkPackageTablePaginationComponent);
|
||||
const app:WorkPackageTablePaginationComponent = fixture.debugElement.componentInstance;
|
||||
const element = fixture.elementRef.nativeElement;
|
||||
|
||||
function numberOfPageNumberLinks() {
|
||||
return element.querySelectorAll('button[data-rel="next"]').length;
|
||||
}
|
||||
|
||||
app.pagination = new PaginationInstance(1, 1, 10);
|
||||
app.update();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(numberOfPageNumberLinks()).toEqual(1);
|
||||
|
||||
app.pagination = new PaginationInstance(1, 11, 10);
|
||||
app.update();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(numberOfPageNumberLinks()).toEqual(2);
|
||||
|
||||
app.pagination = new PaginationInstance(1, 59, 10);
|
||||
app.update();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(numberOfPageNumberLinks()).toEqual(6);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
+8
-3
@@ -1,4 +1,4 @@
|
||||
import { Input, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { Component, Input, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
|
||||
@@ -12,8 +12,13 @@ import { PathHelperService } from 'core-app/core/path-helper/path-helper.service
|
||||
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
|
||||
|
||||
describe('WpTabsComponent', () => {
|
||||
@Component({
|
||||
template: '',
|
||||
standalone: false,
|
||||
})
|
||||
class TestComponent implements TabComponent {
|
||||
@Input() public workPackage:WorkPackageResource;
|
||||
@Input()
|
||||
public workPackage:WorkPackageResource;
|
||||
}
|
||||
|
||||
const displayableTab = {
|
||||
@@ -64,6 +69,6 @@ describe('WpTabsComponent', () => {
|
||||
it('displays the visible tab', () => {
|
||||
const tabLink:HTMLElement = fixture.debugElement.query(By.css('[data-qa-tab-id="displayable-test-tab"]')).nativeElement;
|
||||
|
||||
expect(tabLink.innerText).toContain('Displayable TestTab');
|
||||
expect(tabLink.textContent).toContain('Displayable TestTab');
|
||||
});
|
||||
});
|
||||
|
||||
+14
-11
@@ -1,20 +1,23 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { Input } from '@angular/core';
|
||||
import { StateService } from '@uirouter/angular';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { StateService } from '@uirouter/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
|
||||
import {
|
||||
WorkPackageTabsService,
|
||||
} from 'core-app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service';
|
||||
import { WorkPackageTabsService, } from 'core-app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service';
|
||||
import { TabComponent } from '../../components/wp-tab-wrapper/tab';
|
||||
|
||||
describe('WpTabsService', () => {
|
||||
let service:WorkPackageTabsService;
|
||||
const workPackage:any = { id: 1234 };
|
||||
|
||||
@Component({
|
||||
template: '',
|
||||
standalone: false,
|
||||
})
|
||||
class TestComponent implements TabComponent {
|
||||
@Input() public workPackage:WorkPackageResource;
|
||||
@Input()
|
||||
public workPackage:WorkPackageResource;
|
||||
}
|
||||
|
||||
const displayableTab = {
|
||||
@@ -34,12 +37,12 @@ describe('WpTabsService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
imports: [],
|
||||
providers: [
|
||||
{ provide: StateService, useValue: { includes: () => false } },
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
]
|
||||
});
|
||||
]
|
||||
});
|
||||
service = TestBed.inject(WorkPackageTabsService);
|
||||
(service as any).registeredTabs = [];
|
||||
service.register({ ...displayableTab }, { ...notDisplayableTab });
|
||||
@@ -66,7 +69,7 @@ describe('WpTabsService', () => {
|
||||
|
||||
const displayableTabs = service.getDisplayableTabs(workPackage);
|
||||
|
||||
expect(displayableTabs).toHaveSize(1);
|
||||
expect(displayableTabs).toHaveLength(1);
|
||||
expect(displayableTabs[0].id).toEqual(notDisplayableTab.id);
|
||||
});
|
||||
});
|
||||
|
||||
+28
-35
@@ -35,7 +35,8 @@ import { WorkPackageViewHierarchyIdentationService } from 'core-app/features/wor
|
||||
import { WorkPackageViewDisplayRepresentationService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-display-representation.service';
|
||||
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
|
||||
import { of } from 'rxjs';
|
||||
import SpyObj = jasmine.SpyObj;
|
||||
import type { MockedObject } from 'vitest';
|
||||
type SpyObj<T> = MockedObject<T>;
|
||||
|
||||
describe('WorkPackageViewIndentation service', () => {
|
||||
let service:WorkPackageViewHierarchyIdentationService;
|
||||
@@ -59,12 +60,11 @@ describe('WorkPackageViewIndentation service', () => {
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
parentServiceSpy = jasmine.createSpyObj(
|
||||
'WorkPackageRelationHierarchyService',
|
||||
['changeParent'],
|
||||
);
|
||||
parentServiceSpy = {
|
||||
changeParent: vi.fn().mockName('WorkPackageRelationHierarchyService.changeParent')
|
||||
};
|
||||
|
||||
parentServiceSpy.changeParent.and.resolveTo();
|
||||
parentServiceSpy.changeParent.mockResolvedValue();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -118,8 +118,7 @@ describe('WorkPackageViewIndentation service', () => {
|
||||
{ workPackageId: '1234', hidden: false, classIdentifier: 'foo' },
|
||||
]);
|
||||
|
||||
spyOnProperty(hierarchyServiceStub, 'isEnabled', 'get')
|
||||
.and.returnValue(false);
|
||||
vi.spyOn(hierarchyServiceStub, 'isEnabled', 'get').mockReturnValue(false);
|
||||
|
||||
const workPackage:any = { id: '1234', changeParent: () => 'foo', ancestorIds: [] };
|
||||
|
||||
@@ -160,8 +159,7 @@ describe('WorkPackageViewIndentation service', () => {
|
||||
it('Cannot outdent with changeParent link but disabled', () => {
|
||||
const workPackage:any = { id: '1234', changeParent: () => 'foo', parent: { id: '2345' } };
|
||||
|
||||
spyOnProperty(hierarchyServiceStub, 'isEnabled', 'get')
|
||||
.and.returnValue(false);
|
||||
vi.spyOn(hierarchyServiceStub, 'isEnabled', 'get').mockReturnValue(false);
|
||||
|
||||
expect(service.canOutdent(workPackage)).toBeFalsy();
|
||||
});
|
||||
@@ -174,7 +172,7 @@ describe('WorkPackageViewIndentation service', () => {
|
||||
});
|
||||
|
||||
describe('indent', () => {
|
||||
it('Can indent with a predecessor that is NOT an ancestor already', (done) => {
|
||||
it('Can indent with a predecessor that is NOT an ancestor already', async () => {
|
||||
querySpace.tableRendered.putValue([
|
||||
{ workPackageId: '5555', hidden: false, classIdentifier: 'foo' },
|
||||
{ workPackageId: '2345', hidden: false, classIdentifier: 'foo' },
|
||||
@@ -186,13 +184,12 @@ describe('WorkPackageViewIndentation service', () => {
|
||||
|
||||
states.workPackages.get('2345').putValue(predecessor);
|
||||
|
||||
service.indent(workPackage).then(() => {
|
||||
expect(parentServiceSpy.changeParent).toHaveBeenCalledWith(workPackage, '2345');
|
||||
done();
|
||||
}).catch(done.fail);
|
||||
await service.indent(workPackage);
|
||||
|
||||
expect(parentServiceSpy.changeParent).toHaveBeenCalledWith(workPackage, '2345');
|
||||
});
|
||||
|
||||
it('Can indent with a predecessor that shares an ancestor chain', (done) => {
|
||||
it('Can indent with a predecessor that shares an ancestor chain', async () => {
|
||||
querySpace.tableRendered.putValue([
|
||||
{ workPackageId: '5555', hidden: false, classIdentifier: 'foo' },
|
||||
{ workPackageId: '2345', hidden: false, classIdentifier: 'foo' },
|
||||
@@ -204,13 +201,12 @@ describe('WorkPackageViewIndentation service', () => {
|
||||
|
||||
states.workPackages.get('2345').putValue(predecessor);
|
||||
|
||||
service.indent(workPackage).then(() => {
|
||||
expect(parentServiceSpy.changeParent).toHaveBeenCalledWith(workPackage, '5555');
|
||||
done();
|
||||
}).catch(done.fail);
|
||||
await service.indent(workPackage);
|
||||
|
||||
expect(parentServiceSpy.changeParent).toHaveBeenCalledWith(workPackage, '5555');
|
||||
});
|
||||
|
||||
it('Can indent with a predecessor that shares an ancestor chain', (done) => {
|
||||
it('Can indent with a predecessor that shares an ancestor chain', async () => {
|
||||
querySpace.tableRendered.putValue([
|
||||
{ workPackageId: '5555', hidden: false, classIdentifier: 'foo' },
|
||||
{ workPackageId: '2345', hidden: false, classIdentifier: 'foo' },
|
||||
@@ -222,15 +218,14 @@ describe('WorkPackageViewIndentation service', () => {
|
||||
|
||||
states.workPackages.get('2345').putValue(predecessor);
|
||||
|
||||
service.indent(workPackage).then(() => {
|
||||
expect(parentServiceSpy.changeParent).toHaveBeenCalledWith(workPackage, '2345');
|
||||
done();
|
||||
}).catch(done.fail);
|
||||
await service.indent(workPackage);
|
||||
|
||||
expect(parentServiceSpy.changeParent).toHaveBeenCalledWith(workPackage, '2345');
|
||||
});
|
||||
});
|
||||
|
||||
describe('outdent', () => {
|
||||
it('will outdent to the previous last ancestorId', (done) => {
|
||||
it('will outdent to the previous last ancestorId', async () => {
|
||||
querySpace.tableRendered.putValue([
|
||||
{ workPackageId: '1234', hidden: false, classIdentifier: 'foo' },
|
||||
]);
|
||||
@@ -239,13 +234,12 @@ describe('WorkPackageViewIndentation service', () => {
|
||||
id: '1234', changeParent: () => 'foo', parent: '5555', ancestorIds: ['2345', '5555'],
|
||||
};
|
||||
|
||||
service.outdent(workPackage).then(() => {
|
||||
expect(parentServiceSpy.changeParent).toHaveBeenCalledWith(workPackage, '2345');
|
||||
done();
|
||||
}).catch(done.fail);
|
||||
await service.outdent(workPackage);
|
||||
|
||||
expect(parentServiceSpy.changeParent).toHaveBeenCalledWith(workPackage, '2345');
|
||||
});
|
||||
|
||||
it('will outdent to null in case of ancestorIds.length < 2', (done) => {
|
||||
it('will outdent to null in case of ancestorIds.length < 2', async () => {
|
||||
querySpace.tableRendered.putValue([
|
||||
{ workPackageId: '1234', hidden: false, classIdentifier: 'foo' },
|
||||
]);
|
||||
@@ -254,10 +248,9 @@ describe('WorkPackageViewIndentation service', () => {
|
||||
id: '1234', changeParent: () => 'foo', parent: '2345', ancestorIds: ['2345'],
|
||||
};
|
||||
|
||||
service.outdent(workPackage).then(() => {
|
||||
expect(parentServiceSpy.changeParent).toHaveBeenCalledWith(workPackage, null);
|
||||
done();
|
||||
}).catch(done.fail);
|
||||
await service.outdent(workPackage);
|
||||
|
||||
expect(parentServiceSpy.changeParent).toHaveBeenCalledWith(workPackage, null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+38
-40
@@ -1,3 +1,4 @@
|
||||
import type { Mock } from 'vitest';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AttributeHelpTextsService } from './attribute-help-text.service';
|
||||
import { AttributeHelpTextModalService } from './attribute-help-text-modal.service';
|
||||
@@ -6,12 +7,12 @@ import { TurboRequestsService } from 'core-app/core/turbo/turbo-requests.service
|
||||
import { ToastService } from '../toaster/toast.service';
|
||||
|
||||
describe('AttributeHelpTextModalService', () => {
|
||||
let fetchSpy:jasmine.Spy<Window['fetch']>;
|
||||
let fetchSpy:Mock<Window['fetch']>;
|
||||
let modalService:AttributeHelpTextModalService;
|
||||
let dialog:HTMLDialogElement|null;
|
||||
|
||||
beforeEach(() => {
|
||||
fetchSpy = spyOn(window, 'fetch');
|
||||
fetchSpy = vi.spyOn(window, 'fetch') as unknown as Mock<Window['fetch']>;
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -34,12 +35,12 @@ describe('AttributeHelpTextModalService', () => {
|
||||
|
||||
const makeSuccessResponse = (dialogId:string, dialogContent:string) => {
|
||||
const body = `<turbo-stream action="dialog">
|
||||
<template>
|
||||
<dialog-helper>
|
||||
<dialog id="${dialogId}">${dialogContent}</dialog>
|
||||
</dialog-helper>
|
||||
</template>
|
||||
</turbo-stream>`;
|
||||
<template>
|
||||
<dialog-helper>
|
||||
<dialog id="${dialogId}">${dialogContent}</dialog>
|
||||
</dialog-helper>
|
||||
</template>
|
||||
</turbo-stream>`;
|
||||
|
||||
return new Response(body, { status: 200, headers: { 'Content-Type': 'text/vnd.turbo-stream.html' } });
|
||||
};
|
||||
@@ -47,84 +48,84 @@ describe('AttributeHelpTextModalService', () => {
|
||||
afterEach(() => {
|
||||
dialog?.remove();
|
||||
dialog = null;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('with a successful request', () => {
|
||||
beforeEach(() => {
|
||||
fetchSpy
|
||||
.withArgs(jasmine.stringMatching('/1/show_dialog'), jasmine.any(Object))
|
||||
.and.resolveTo(makeSuccessResponse('test1', 'Hello Dialog'));
|
||||
fetchSpy.mockImplementation((url:RequestInfo | URL) => {
|
||||
const requestUrl = url instanceof Request ? url.url : url.toString();
|
||||
|
||||
if (requestUrl.includes('/1/show_dialog')) {
|
||||
return Promise.resolve(makeSuccessResponse('test1', 'Hello Dialog'));
|
||||
}
|
||||
return Promise.reject(new Error(`Unexpected url: ${requestUrl}`));
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle Turbo Stream dialog response and open the dialog', async () => {
|
||||
expect(document.querySelector('dialog#test1')).toBeFalsy();
|
||||
|
||||
await expectAsync(modalService.show('1')).toBeResolved();
|
||||
await modalService.show('1');
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
dialog = await waitForNativeElement<HTMLDialogElement>('dialog#test1');
|
||||
|
||||
expect(dialog.textContent).toEqual('Hello Dialog');
|
||||
expect(dialog.open).toBeTrue();
|
||||
expect(dialog.open).toBe(true);
|
||||
dialog.close();
|
||||
|
||||
expect(dialog.open).toBeFalse();
|
||||
expect(dialog.open).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an aborted request followed by a successful request', () => {
|
||||
beforeEach(() => {
|
||||
fetchSpy
|
||||
.withArgs(jasmine.stringMatching('/2/show_dialog'), jasmine.any(Object))
|
||||
.and.returnValues(
|
||||
Promise.reject(new DOMException('message', 'AbortError')),
|
||||
Promise.resolve(makeSuccessResponse('test2', 'Noch mal ein Dialog')),
|
||||
);
|
||||
.mockReturnValueOnce(Promise.reject(new DOMException('message', 'AbortError')))
|
||||
.mockReturnValueOnce(Promise.resolve(makeSuccessResponse('test2', 'Noch mal ein Dialog')));
|
||||
});
|
||||
|
||||
it('should handle Turbo Stream dialog response and still open the dialog', async () => {
|
||||
expect(document.querySelector('dialog#test2')).toBeFalsy();
|
||||
|
||||
await expectAsync(modalService.show('2')).toBeRejected();
|
||||
await expectAsync(modalService.show('2')).toBeResolved();
|
||||
await expect(modalService.show('2')).rejects.toThrow();
|
||||
await modalService.show('2');
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
dialog = await waitForNativeElement<HTMLDialogElement>('dialog#test2');
|
||||
|
||||
expect(dialog.textContent).toEqual('Noch mal ein Dialog');
|
||||
expect(dialog.open).toBeTrue();
|
||||
expect(dialog.open).toBe(true);
|
||||
dialog.close();
|
||||
|
||||
expect(dialog.open).toBeFalse();
|
||||
expect(dialog.open).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with 3 successful requests with the same dialog id', () => {
|
||||
beforeEach(() => {
|
||||
fetchSpy
|
||||
.withArgs(jasmine.stringMatching('/3/show_dialog'), jasmine.any(Object))
|
||||
.and.returnValues(
|
||||
Promise.resolve(makeSuccessResponse('test3', '<p>initial content</p>')),
|
||||
Promise.resolve(makeSuccessResponse('test3', '<p>updated content</p>')),
|
||||
Promise.resolve(makeSuccessResponse('test3', '<h3>new headline</h3>')),
|
||||
);
|
||||
.mockReturnValueOnce(Promise.resolve(makeSuccessResponse('test3', '<p>initial content</p>')))
|
||||
.mockReturnValueOnce(Promise.resolve(makeSuccessResponse('test3', '<p>updated content</p>')))
|
||||
.mockReturnValueOnce(Promise.resolve(makeSuccessResponse('test3', '<h3>new headline</h3>')));
|
||||
});
|
||||
|
||||
it('should handle Turbo Stream dialog response and update dialog', async () => {
|
||||
expect(document.querySelector('dialog#test3')).toBeFalsy();
|
||||
|
||||
await expectAsync(modalService.show('3')).toBeResolved();
|
||||
await modalService.show('3');
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
dialog = await waitForNativeElement<HTMLDialogElement>('dialog#test3');
|
||||
|
||||
expect(dialog.textContent).toEqual('initial content');
|
||||
expect(dialog.open).toBeTrue();
|
||||
expect(dialog.open).toBe(true);
|
||||
|
||||
await expectAsync(modalService.show('3')).toBeResolved();
|
||||
await modalService.show('3');
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
@@ -132,9 +133,9 @@ describe('AttributeHelpTextModalService', () => {
|
||||
|
||||
expect(mutation.type).toEqual('characterData');
|
||||
expect(dialog.textContent).toEqual('updated content');
|
||||
expect(dialog.open).toBeTrue();
|
||||
expect(dialog.open).toBe(true);
|
||||
|
||||
await expectAsync(modalService.show('3')).toBeResolved();
|
||||
await modalService.show('3');
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
||||
|
||||
@@ -143,11 +144,11 @@ describe('AttributeHelpTextModalService', () => {
|
||||
expect(mutation.type).toEqual('childList');
|
||||
expect(dialog.textContent).toEqual('new headline');
|
||||
expect(dialog.querySelector('h3')).toBeTruthy();
|
||||
expect(dialog.open).toBeTrue();
|
||||
expect(dialog.open).toBe(true);
|
||||
|
||||
dialog.close();
|
||||
|
||||
expect(dialog.open).toBeFalse();
|
||||
expect(dialog.open).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -170,10 +171,7 @@ function waitForNativeElement<T extends Element>(selector:string):Promise<T> {
|
||||
});
|
||||
}
|
||||
|
||||
function waitForElementMutation<T extends Element>(
|
||||
element:T,
|
||||
predicate:(mutation:MutationRecord) => boolean = () => true,
|
||||
):Promise<MutationRecord> {
|
||||
function waitForElementMutation<T extends Element>(element:T, predicate:(mutation:MutationRecord) => boolean = () => true):Promise<MutationRecord> {
|
||||
return new Promise((resolve) => {
|
||||
const observer = new MutationObserver((mutationList) => {
|
||||
const record = mutationList.find(predicate);
|
||||
|
||||
+17
-11
@@ -14,12 +14,14 @@ describe('AttributeHelpTextComponent', () => {
|
||||
let element:DebugElement;
|
||||
|
||||
const serviceStub = {};
|
||||
let modalServiceStub:jasmine.SpyObj<AttributeHelpTextModalService>;
|
||||
const i18nStub = { t: (_scope:string|string[], _options?:Record<string, any>) => 'Show help text' };
|
||||
let modalServiceStub:{ show:ReturnType<typeof vi.fn> };
|
||||
const i18nStub = { t: (_scope:string | string[], _options?:Record<string, any>) => 'Show help text' };
|
||||
|
||||
beforeEach(async () => {
|
||||
modalServiceStub = jasmine.createSpyObj('AttributeHelpTextModalService', ['show']);
|
||||
modalServiceStub.show.and.resolveTo();
|
||||
modalServiceStub = {
|
||||
show: vi.fn().mockName('AttributeHelpTextModalService.show')
|
||||
};
|
||||
modalServiceStub.show.mockResolvedValue(undefined);
|
||||
|
||||
await TestBed
|
||||
.configureTestingModule({
|
||||
@@ -59,7 +61,7 @@ describe('AttributeHelpTextComponent', () => {
|
||||
const button = element.query(By.css("[role='button']"));
|
||||
|
||||
expect(button).toBeTruthy();
|
||||
expect(button.nativeElement).toHaveClass('spot-link');
|
||||
expect(button.nativeElement.classList.contains('spot-link')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders a tooltip', () => {
|
||||
@@ -68,7 +70,7 @@ describe('AttributeHelpTextComponent', () => {
|
||||
expect(tooltip).toBeTruthy();
|
||||
expect(tooltip.nativeElement.textContent).toEqual('Show help text');
|
||||
expect(tooltip.nativeElement.getAttribute('for')).toMatch(/attribute-help-text-component-\d+/);
|
||||
expect(tooltip.nativeElement.popover).toEqual('manual');
|
||||
expect(tooltip.nativeElement.getAttribute('popover')).toEqual('manual');
|
||||
expect(tooltip.nativeElement.dataset.direction).toEqual('sw');
|
||||
expect(tooltip.nativeElement.dataset.type).toEqual('label');
|
||||
});
|
||||
@@ -82,7 +84,7 @@ describe('AttributeHelpTextComponent', () => {
|
||||
it('applies .help-text--entry class', () => {
|
||||
const button = element.query(By.css("[role='button']"));
|
||||
|
||||
expect(button.nativeElement).toHaveClass('help-text--entry');
|
||||
expect(button.nativeElement.classList.contains('help-text--entry')).toBe(true);
|
||||
});
|
||||
|
||||
it('applies an ID', () => {
|
||||
@@ -106,11 +108,13 @@ describe('AttributeHelpTextComponent', () => {
|
||||
expect(button.nativeElement.ariaDisabled).toEqual('true');
|
||||
|
||||
await Promise.resolve();
|
||||
await modalServiceStub.show.calls.mostRecent().returnValue;
|
||||
await modalServiceStub.show.mock.results.at(-1)!.value;
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(modalServiceStub.show).toHaveBeenCalledOnceWith('1');
|
||||
expect(modalServiceStub.show).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(modalServiceStub.show).toHaveBeenCalledWith('1');
|
||||
expect(button.nativeElement.ariaDisabled).toEqual('false');
|
||||
});
|
||||
|
||||
@@ -128,11 +132,13 @@ describe('AttributeHelpTextComponent', () => {
|
||||
|
||||
fixture.detectChanges();
|
||||
await Promise.resolve();
|
||||
await modalServiceStub.show.calls.mostRecent().returnValue;
|
||||
await modalServiceStub.show.mock.results.at(-1)!.value;
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(modalServiceStub.show).toHaveBeenCalledOnceWith('1');
|
||||
expect(modalServiceStub.show).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(modalServiceStub.show).toHaveBeenCalledWith('1');
|
||||
expect(button.nativeElement.ariaDisabled).toEqual('false');
|
||||
});
|
||||
});
|
||||
|
||||
+49
-41
@@ -1,3 +1,4 @@
|
||||
import type { Mock } from 'vitest';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { States } from 'core-app/core/states/states.service';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
@@ -12,7 +13,7 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
|
||||
describe('autocompleter', () => {
|
||||
let fixture:ComponentFixture<OpAutocompleterComponent>;
|
||||
let getOptionsFnSpy:jasmine.Spy;
|
||||
let getOptionsFnSpy:Mock;
|
||||
const workPackagesStub = [
|
||||
{
|
||||
id: 1,
|
||||
@@ -60,7 +61,11 @@ describe('autocompleter', () => {
|
||||
},
|
||||
];
|
||||
|
||||
type WindowWithOpenProject = Omit<Window, 'OpenProject'> & { OpenProject?:{ environment:string } };
|
||||
type WindowWithOpenProject = Omit<Window, 'OpenProject'> & {
|
||||
OpenProject?:{
|
||||
environment:string;
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(window as WindowWithOpenProject).OpenProject = { environment: 'test' };
|
||||
@@ -79,10 +84,8 @@ describe('autocompleter', () => {
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(OpAutocompleterComponent);
|
||||
getOptionsFnSpy = jasmine.createSpy('getOptionsFn').and.callFake((searchTerm:string) => {
|
||||
return of(workPackagesStub).pipe(
|
||||
map((wps) => wps.filter((wp) => searchTerm !== '' && wp.subject.includes(searchTerm)))
|
||||
);
|
||||
getOptionsFnSpy = vi.fn().mockImplementation((searchTerm:string) => {
|
||||
return of(workPackagesStub).pipe(map((wps) => wps.filter((wp) => searchTerm !== '' && wp.subject.includes(searchTerm))));
|
||||
});
|
||||
|
||||
fixture.componentInstance.resource = 'work_packages' as TOpAutocompleterResource;
|
||||
@@ -98,35 +101,36 @@ describe('autocompleter', () => {
|
||||
});
|
||||
|
||||
it('should load the ng-select correctly', () => {
|
||||
jasmine.clock().install();
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
fixture.detectChanges();
|
||||
jasmine.clock().tick(0);
|
||||
vi.advanceTimersByTime(0);
|
||||
|
||||
const autocompleter = document.querySelector('.ng-select-container');
|
||||
|
||||
expect(document.contains(autocompleter)).toBeTruthy();
|
||||
} finally {
|
||||
jasmine.clock().uninstall();
|
||||
}
|
||||
finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
describe('without debounce', () => {
|
||||
it('should load items', () => {
|
||||
jasmine.clock().install();
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
jasmine.clock().tick(0);
|
||||
vi.advanceTimersByTime(0);
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.ngAfterViewInit();
|
||||
jasmine.clock().tick(1000);
|
||||
vi.advanceTimersByTime(1000);
|
||||
fixture.detectChanges();
|
||||
const select = fixture.componentInstance.ngSelectInstance;
|
||||
|
||||
expect(select.isOpen()).toBeFalse();
|
||||
expect(select.isOpen()).toBe(false);
|
||||
select.open();
|
||||
select.focus();
|
||||
|
||||
expect(select.isOpen()).toBeTrue();
|
||||
expect(select.isOpen()).toBe(true);
|
||||
|
||||
expect(select.itemsList.items.length).toEqual(0);
|
||||
|
||||
@@ -134,14 +138,14 @@ describe('autocompleter', () => {
|
||||
const inputElement = inputDebugElement.nativeElement as HTMLInputElement;
|
||||
|
||||
fixture.detectChanges();
|
||||
jasmine.clock().tick(0);
|
||||
vi.advanceTimersByTime(0);
|
||||
|
||||
expect(getOptionsFnSpy).toHaveBeenCalledWith('');
|
||||
|
||||
inputElement.value = 'Wor';
|
||||
inputElement.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
jasmine.clock().tick(0);
|
||||
vi.advanceTimersByTime(0);
|
||||
|
||||
expect(getOptionsFnSpy).toHaveBeenCalledWith('Wor');
|
||||
|
||||
@@ -152,27 +156,28 @@ describe('autocompleter', () => {
|
||||
inputElement.value = 'package 2';
|
||||
inputElement.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
jasmine.clock().tick(0);
|
||||
vi.advanceTimersByTime(0);
|
||||
|
||||
expect(getOptionsFnSpy).toHaveBeenCalledWith('package 2');
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(select.itemsList.items.length).toEqual(1);
|
||||
} finally {
|
||||
jasmine.clock().uninstall();
|
||||
}
|
||||
finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('work package option rendering', () => {
|
||||
it('should display formattedId in dropdown options', () => {
|
||||
jasmine.clock().install();
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
jasmine.clock().tick(0);
|
||||
vi.advanceTimersByTime(0);
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.ngAfterViewInit();
|
||||
jasmine.clock().tick(1000);
|
||||
vi.advanceTimersByTime(1000);
|
||||
fixture.detectChanges();
|
||||
const select = fixture.componentInstance.ngSelectInstance;
|
||||
|
||||
@@ -185,7 +190,7 @@ describe('autocompleter', () => {
|
||||
inputElement.value = 'Wor';
|
||||
inputElement.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
jasmine.clock().tick(0);
|
||||
vi.advanceTimersByTime(0);
|
||||
fixture.detectChanges();
|
||||
|
||||
const wpIdElements = document.querySelectorAll('.op-autocompleter--wp-id');
|
||||
@@ -195,18 +200,19 @@ describe('autocompleter', () => {
|
||||
const renderedIds = Array.from(wpIdElements).map(el => el.textContent?.trim());
|
||||
|
||||
expect(renderedIds).toContain('#1');
|
||||
} finally {
|
||||
jasmine.clock().uninstall();
|
||||
}
|
||||
finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('should display classic formattedId in selected value label', () => {
|
||||
jasmine.clock().install();
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
jasmine.clock().tick(0);
|
||||
vi.advanceTimersByTime(0);
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.ngAfterViewInit();
|
||||
jasmine.clock().tick(1000);
|
||||
vi.advanceTimersByTime(1000);
|
||||
fixture.detectChanges();
|
||||
const select = fixture.componentInstance.ngSelectInstance;
|
||||
|
||||
@@ -219,7 +225,7 @@ describe('autocompleter', () => {
|
||||
inputElement.value = 'Wor';
|
||||
inputElement.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
jasmine.clock().tick(0);
|
||||
vi.advanceTimersByTime(0);
|
||||
fixture.detectChanges();
|
||||
|
||||
// Select the first item (classic mode: #1)
|
||||
@@ -232,18 +238,19 @@ describe('autocompleter', () => {
|
||||
expect(labelElement).toBeTruthy();
|
||||
expect(labelElement!.textContent).toContain('#1');
|
||||
expect(labelElement!.textContent).toContain('Workpackage 1');
|
||||
} finally {
|
||||
jasmine.clock().uninstall();
|
||||
}
|
||||
finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('should display semantic formattedId in selected value label', () => {
|
||||
jasmine.clock().install();
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
jasmine.clock().tick(0);
|
||||
vi.advanceTimersByTime(0);
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.ngAfterViewInit();
|
||||
jasmine.clock().tick(1000);
|
||||
vi.advanceTimersByTime(1000);
|
||||
fixture.detectChanges();
|
||||
const select = fixture.componentInstance.ngSelectInstance;
|
||||
|
||||
@@ -256,7 +263,7 @@ describe('autocompleter', () => {
|
||||
inputElement.value = 'package 2';
|
||||
inputElement.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
jasmine.clock().tick(0);
|
||||
vi.advanceTimersByTime(0);
|
||||
fixture.detectChanges();
|
||||
|
||||
// Select the semantic mode item (PROJ-2)
|
||||
@@ -270,8 +277,9 @@ describe('autocompleter', () => {
|
||||
expect(labelElement!.textContent).toContain('PROJ-2');
|
||||
expect(labelElement!.textContent).not.toContain('#PROJ-2');
|
||||
expect(labelElement!.textContent).toContain('Workpackage 2');
|
||||
} finally {
|
||||
jasmine.clock().uninstall();
|
||||
}
|
||||
finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -290,11 +298,11 @@ describe('autocompleter', () => {
|
||||
fixture.detectChanges();
|
||||
const select = fixture.componentInstance.ngSelectInstance;
|
||||
|
||||
expect(select.isOpen()).toBeFalse();
|
||||
expect(select.isOpen()).toBe(false);
|
||||
select.open();
|
||||
select.focus();
|
||||
|
||||
expect(select.isOpen()).toBeTrue();
|
||||
expect(select.isOpen()).toBe(true);
|
||||
|
||||
expect(select.itemsList.items.length).toEqual(0);
|
||||
|
||||
@@ -307,7 +315,7 @@ describe('autocompleter', () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(getOptionsFnSpy).toHaveBeenCalledWith('');
|
||||
getOptionsFnSpy.calls.reset();
|
||||
getOptionsFnSpy.mockClear();
|
||||
|
||||
inputElement.value = 'Wor';
|
||||
inputElement.dispatchEvent(new Event('input'));
|
||||
|
||||
@@ -33,12 +33,7 @@ import { EditFieldHandler } from 'core-app/shared/components/fields/edit/editing
|
||||
import { IFieldSchema } from 'core-app/shared/components/fields/field.base';
|
||||
|
||||
class TestEditForm extends EditForm<HalResource> {
|
||||
constructor(
|
||||
injector:Injector,
|
||||
private readonly requireVisibleSpy:(fieldName:string) => Promise<void>,
|
||||
private readonly activateFieldSpy:() => Promise<EditFieldHandler>,
|
||||
private readonly resetSpy:(fieldName:string, focus?:boolean) => void,
|
||||
) {
|
||||
constructor(injector:Injector, private readonly requireVisibleSpy:(fieldName:string) => Promise<void>, private readonly activateFieldSpy:() => Promise<EditFieldHandler>, private readonly resetSpy:(fieldName:string, focus?:boolean) => void) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
@@ -61,12 +56,12 @@ class TestEditForm extends EditForm<HalResource> {
|
||||
|
||||
describe('EditForm', () => {
|
||||
it('does not require visibility twice for newly erroneous inactive fields', async () => {
|
||||
const tick = jasmine.createSpy('tick');
|
||||
const requireVisible = jasmine.createSpy('requireVisible').and.resolveTo();
|
||||
const activateField = jasmine.createSpy('activateField').and.resolveTo({} as EditFieldHandler);
|
||||
const reset = jasmine.createSpy('reset');
|
||||
const tick = vi.fn();
|
||||
const requireVisible = vi.fn().mockResolvedValue(undefined);
|
||||
const activateField = vi.fn().mockResolvedValue({} as EditFieldHandler);
|
||||
const reset = vi.fn();
|
||||
const injector = {
|
||||
get: jasmine.createSpy('get').and.callFake((token:unknown) => {
|
||||
get: vi.fn().mockImplementation((token:unknown) => {
|
||||
if (token === ApplicationRef) {
|
||||
return { tick };
|
||||
}
|
||||
@@ -79,25 +74,27 @@ describe('EditForm', () => {
|
||||
const change = {
|
||||
inFlight: false,
|
||||
schema: {
|
||||
ofProperty: jasmine.createSpy('ofProperty').and.returnValue({
|
||||
ofProperty: vi.fn().mockReturnValue({
|
||||
writable: true,
|
||||
name: 'Foo',
|
||||
} as IFieldSchema),
|
||||
},
|
||||
getForm: jasmine.createSpy('getForm').and.resolveTo(),
|
||||
getForm: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
form.resource = { id: 1 } as unknown as HalResource;
|
||||
form.halEditing = {
|
||||
changeFor: jasmine.createSpy('changeFor').and.returnValue(change),
|
||||
changeFor: vi.fn().mockReturnValue(change),
|
||||
} as never;
|
||||
form.halNotification = {
|
||||
handleRawError: jasmine.createSpy('handleRawError'),
|
||||
showEditingBlockedError: jasmine.createSpy('showEditingBlockedError'),
|
||||
handleRawError: vi.fn(),
|
||||
showEditingBlockedError: vi.fn(),
|
||||
} as never;
|
||||
form.errorsPerAttribute = { foo: ['Required'] };
|
||||
|
||||
(form as unknown as { setErrorsForFields:(fields:string[]) => void }).setErrorsForFields(['foo']);
|
||||
(form as unknown as {
|
||||
setErrorsForFields:(fields:string[]) => void;
|
||||
}).setErrorsForFields(['foo']);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
|
||||
+8
-8
@@ -20,7 +20,7 @@ describe('GlobalEditFormChangesTrackerService', () => {
|
||||
});
|
||||
|
||||
it('should report no changes when empty', () => {
|
||||
expect(service.thereAreFormsWithUnsavedChanges).toBeFalse();
|
||||
expect(service.thereAreFormsWithUnsavedChanges).toBe(false);
|
||||
});
|
||||
|
||||
it('should report no changes when one form has no changes', () => {
|
||||
@@ -28,7 +28,7 @@ describe('GlobalEditFormChangesTrackerService', () => {
|
||||
|
||||
service.addToActiveForms(form);
|
||||
|
||||
expect(service.thereAreFormsWithUnsavedChanges).toBeFalse();
|
||||
expect(service.thereAreFormsWithUnsavedChanges).toBe(false);
|
||||
});
|
||||
|
||||
it('should report no changes when multiple forms have no changes', () => {
|
||||
@@ -40,7 +40,7 @@ describe('GlobalEditFormChangesTrackerService', () => {
|
||||
service.addToActiveForms(form2);
|
||||
service.addToActiveForms(form3);
|
||||
|
||||
expect(service.thereAreFormsWithUnsavedChanges).toBeFalse();
|
||||
expect(service.thereAreFormsWithUnsavedChanges).toBe(false);
|
||||
});
|
||||
|
||||
it('should report no changes when the only form with changes is removed', () => {
|
||||
@@ -49,7 +49,7 @@ describe('GlobalEditFormChangesTrackerService', () => {
|
||||
service.addToActiveForms(form);
|
||||
service.removeFromActiveForms(form);
|
||||
|
||||
expect(service.thereAreFormsWithUnsavedChanges).toBeFalse();
|
||||
expect(service.thereAreFormsWithUnsavedChanges).toBe(false);
|
||||
});
|
||||
|
||||
it('should report changes when one form has changes', () => {
|
||||
@@ -57,7 +57,7 @@ describe('GlobalEditFormChangesTrackerService', () => {
|
||||
|
||||
service.addToActiveForms(form);
|
||||
|
||||
expect(service.thereAreFormsWithUnsavedChanges).toBeTrue();
|
||||
expect(service.thereAreFormsWithUnsavedChanges).toBe(true);
|
||||
});
|
||||
|
||||
it('should report forms with changes when multiple form have changes', () => {
|
||||
@@ -69,13 +69,13 @@ describe('GlobalEditFormChangesTrackerService', () => {
|
||||
service.addToActiveForms(form2);
|
||||
service.addToActiveForms(form3);
|
||||
|
||||
expect(service.thereAreFormsWithUnsavedChanges).toBeTrue();
|
||||
expect(service.thereAreFormsWithUnsavedChanges).toBe(true);
|
||||
});
|
||||
|
||||
it('should call thereAreFormsWithUnsavedChangesSpy on beforeunload', () => {
|
||||
const thereAreFormsWithUnsavedChangesSpy = spyOnProperty(service, 'thereAreFormsWithUnsavedChanges', 'get');
|
||||
const thereAreFormsWithUnsavedChangesSpy = vi.spyOn(service, 'thereAreFormsWithUnsavedChanges', 'get');
|
||||
|
||||
window.onbeforeunload = jasmine.createSpy();
|
||||
window.onbeforeunload = vi.fn();
|
||||
|
||||
window.dispatchEvent(new Event('beforeunload'));
|
||||
|
||||
|
||||
@@ -57,6 +57,10 @@ describe('DynamicIconDirective', () => {
|
||||
svgElement = fixture.nativeElement.querySelector('svg');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
component.iconName = 'star';
|
||||
fixture.detectChanges();
|
||||
@@ -151,8 +155,8 @@ describe('DynamicIconDirective', () => {
|
||||
});
|
||||
|
||||
it('should warn when rendering unknown icon', () => {
|
||||
spyOn(console, 'warn');
|
||||
|
||||
vi.spyOn(console, 'warn');
|
||||
|
||||
component.iconName = 'unknown-icon';
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -160,15 +164,15 @@ describe('DynamicIconDirective', () => {
|
||||
});
|
||||
|
||||
it('should not render anything for unknown icon', () => {
|
||||
spyOn(console, 'warn');
|
||||
|
||||
vi.spyOn(console, 'warn');
|
||||
|
||||
component.iconName = 'unknown-icon';
|
||||
fixture.detectChanges();
|
||||
|
||||
// Should not have set viewBox or other attributes
|
||||
expect(svgElement.getAttribute('viewBox')).toBeNull();
|
||||
expect(svgElement.getAttribute('fill')).toBeNull();
|
||||
|
||||
|
||||
// Should not have any paths
|
||||
const paths = svgElement.querySelectorAll('path');
|
||||
|
||||
@@ -176,28 +180,28 @@ describe('DynamicIconDirective', () => {
|
||||
});
|
||||
|
||||
it('should handle empty icon name', () => {
|
||||
spyOn(console, 'warn');
|
||||
|
||||
vi.spyOn(console, 'warn');
|
||||
|
||||
component.iconName = '';
|
||||
fixture.detectChanges();
|
||||
|
||||
// Should not warn or render anything
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
expect(svgElement.getAttribute('viewBox')).toBeNull();
|
||||
|
||||
|
||||
const paths = svgElement.querySelectorAll('path');
|
||||
|
||||
expect(paths.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should only render once when loaded', () => {
|
||||
spyOn(console, 'warn');
|
||||
vi.spyOn(console, 'warn');
|
||||
const directive = fixture.debugElement.children[0].injector.get(DynamicIconDirective);
|
||||
spyOn(directive as any, 'renderIcon').and.callThrough();
|
||||
|
||||
vi.spyOn(directive as any, 'renderIcon');
|
||||
|
||||
component.iconName = 'star';
|
||||
fixture.detectChanges();
|
||||
|
||||
|
||||
// Change icon name after initial load - should not re-render
|
||||
component.iconName = 'x';
|
||||
fixture.detectChanges();
|
||||
|
||||
+18
-31
@@ -32,19 +32,12 @@ import { OpModalLocalsMap } from 'core-app/shared/components/modal/modal.types';
|
||||
import { SortFilesPipe } from 'core-app/shared/components/storages/pipes/sort-files.pipe';
|
||||
import { StorageFilesResourceService } from 'core-app/core/state/storage-files/storage-files.service';
|
||||
import { IStorageFile } from 'core-app/core/state/storage-files/storage-file.model';
|
||||
import {
|
||||
FilePickerBaseModalComponent,
|
||||
} from 'core-app/shared/components/storages/file-picker-base-modal/file-picker-base-modal.component';
|
||||
import { FilePickerBaseModalComponent, } from 'core-app/shared/components/storages/file-picker-base-modal/file-picker-base-modal.component';
|
||||
import { StorageFileListItem } from 'core-app/shared/components/storages/storage-file-list-item/storage-file-list-item';
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
class TestFilePickerBaseModalComponent extends FilePickerBaseModalComponent {
|
||||
constructor(
|
||||
locals:OpModalLocalsMap,
|
||||
elementRef:ElementRef,
|
||||
cdRef:ChangeDetectorRef,
|
||||
sortFilesPipe:SortFilesPipe,
|
||||
storageFilesResourceService:StorageFilesResourceService,
|
||||
) {
|
||||
constructor(locals:OpModalLocalsMap, elementRef:ElementRef, cdRef:ChangeDetectorRef, sortFilesPipe:SortFilesPipe, storageFilesResourceService:StorageFilesResourceService) {
|
||||
super(locals, elementRef, cdRef, sortFilesPipe, storageFilesResourceService);
|
||||
}
|
||||
|
||||
@@ -59,10 +52,10 @@ class TestFilePickerBaseModalComponent extends FilePickerBaseModalComponent {
|
||||
|
||||
describe('FilePickerBaseModalComponent', () => {
|
||||
interface Spies {
|
||||
detectChanges:jasmine.Spy;
|
||||
close:jasmine.Spy;
|
||||
files:jasmine.Spy;
|
||||
reset:jasmine.Spy;
|
||||
detectChanges:Mock;
|
||||
close:Mock;
|
||||
files:Mock;
|
||||
reset:Mock;
|
||||
}
|
||||
|
||||
function buildComponent(spies:Spies) {
|
||||
@@ -84,13 +77,7 @@ describe('FilePickerBaseModalComponent', () => {
|
||||
files: spies.files,
|
||||
reset: spies.reset,
|
||||
} as unknown as StorageFilesResourceService;
|
||||
const component = new TestFilePickerBaseModalComponent(
|
||||
locals,
|
||||
elementRef,
|
||||
cdRef,
|
||||
sortFilesPipe,
|
||||
storageFilesResourceService,
|
||||
);
|
||||
const component = new TestFilePickerBaseModalComponent(locals, elementRef, cdRef, sortFilesPipe, storageFilesResourceService);
|
||||
|
||||
component.ngOnInit();
|
||||
|
||||
@@ -98,15 +85,15 @@ describe('FilePickerBaseModalComponent', () => {
|
||||
}
|
||||
|
||||
it('cancels pending directory loading on destroy', () => {
|
||||
const teardown = jasmine.createSpy('teardown');
|
||||
const teardown = vi.fn();
|
||||
const files$ = new Observable(() => teardown);
|
||||
const directory = { location: '/folder', mimeType: 'application/x-op-directory' } as IStorageFile;
|
||||
const files = jasmine.createSpy('files').and.returnValue(files$);
|
||||
const files = vi.fn().mockReturnValue(files$);
|
||||
const { component } = buildComponent({
|
||||
detectChanges: jasmine.createSpy('detectChanges'),
|
||||
close: jasmine.createSpy('close'),
|
||||
detectChanges: vi.fn(),
|
||||
close: vi.fn(),
|
||||
files,
|
||||
reset: jasmine.createSpy('reset'),
|
||||
reset: vi.fn(),
|
||||
});
|
||||
|
||||
component.loadDirectory(directory);
|
||||
@@ -121,15 +108,15 @@ describe('FilePickerBaseModalComponent', () => {
|
||||
|
||||
it('does not report directory loading errors as unhandled async exceptions', async () => {
|
||||
const previousUnhandledError = config.onUnhandledError;
|
||||
const onUnhandledError = jasmine.createSpy('onUnhandledError');
|
||||
const onUnhandledError = vi.fn();
|
||||
const files$ = throwError(() => new Error('boom'));
|
||||
const detectChanges = jasmine.createSpy('detectChanges');
|
||||
const detectChanges = vi.fn();
|
||||
const directory = { location: '/folder', mimeType: 'application/x-op-directory' } as IStorageFile;
|
||||
const { component } = buildComponent({
|
||||
detectChanges,
|
||||
close: jasmine.createSpy('close'),
|
||||
files: jasmine.createSpy('files').and.returnValue(files$),
|
||||
reset: jasmine.createSpy('reset'),
|
||||
close: vi.fn(),
|
||||
files: vi.fn().mockReturnValue(files$),
|
||||
reset: vi.fn(),
|
||||
});
|
||||
|
||||
config.onUnhandledError = onUnhandledError;
|
||||
|
||||
@@ -26,12 +26,7 @@
|
||||
// See COPYRIGHT and LICENSE files for more details.
|
||||
//++
|
||||
|
||||
import {
|
||||
toggleElement,
|
||||
toggleElementByClass,
|
||||
toggleElementByVisibility,
|
||||
attributeTokenList,
|
||||
} from './dom-helpers';
|
||||
import { toggleElement, toggleElementByClass, toggleElementByVisibility, attributeTokenList, } from './dom-helpers';
|
||||
|
||||
describe('dom-helpers', () => {
|
||||
describe('toggleElement', () => {
|
||||
@@ -197,13 +192,13 @@ describe('dom-helpers', () => {
|
||||
it('mimics DOMTokenList over an attribute', () => {
|
||||
const list = attributeTokenList(el, attr);
|
||||
|
||||
expect(list.contains('a')).toBeFalse();
|
||||
expect(list.contains('a')).toBe(false);
|
||||
expect(el.getAttribute(attr)).toBeNull();
|
||||
|
||||
list.add('a', 'b');
|
||||
|
||||
expect(list.contains('a')).toBeTrue();
|
||||
expect(list.contains('b')).toBeTrue();
|
||||
expect(list.contains('a')).toBe(true);
|
||||
expect(list.contains('b')).toBe(true);
|
||||
expect(el.getAttribute(attr)).toBe('a b');
|
||||
|
||||
// adding duplicates is idempotent
|
||||
@@ -214,25 +209,25 @@ describe('dom-helpers', () => {
|
||||
// remove works
|
||||
list.remove('a');
|
||||
|
||||
expect(list.contains('a')).toBeFalse();
|
||||
expect(list.contains('a')).toBe(false);
|
||||
expect(el.getAttribute(attr)).toBe('b');
|
||||
|
||||
// toggle without force flips presence and returns the new state
|
||||
expect(list.toggle('b')).toBeFalse(); // removed
|
||||
expect(list.toggle('b')).toBe(false); // removed
|
||||
expect(el.getAttribute(attr)).toBe('');
|
||||
expect(list.toggle('c')).toBeTrue(); // added
|
||||
expect(list.toggle('c')).toBe(true); // added
|
||||
expect(el.getAttribute(attr)).toBe('c');
|
||||
|
||||
// forced toggle honors force
|
||||
expect(list.toggle('x', true)).toBeTrue();
|
||||
expect(list.contains('x')).toBeTrue();
|
||||
expect(list.toggle('x', false)).toBeFalse();
|
||||
expect(list.contains('x')).toBeFalse();
|
||||
expect(list.toggle('x', true)).toBe(true);
|
||||
expect(list.contains('x')).toBe(true);
|
||||
expect(list.toggle('x', false)).toBe(false);
|
||||
expect(list.contains('x')).toBe(false);
|
||||
|
||||
// replace swaps tokens and returns true when old exists
|
||||
expect(list.replace('c', 'd')).toBeTrue();
|
||||
expect(list.contains('c')).toBeFalse();
|
||||
expect(list.contains('d')).toBeTrue();
|
||||
expect(list.replace('c', 'd')).toBe(true);
|
||||
expect(list.contains('c')).toBe(false);
|
||||
expect(list.contains('d')).toBe(true);
|
||||
|
||||
// iterator yields tokens
|
||||
expect([...list]).toEqual(['d']);
|
||||
@@ -241,15 +236,15 @@ describe('dom-helpers', () => {
|
||||
list.value = 'e f';
|
||||
|
||||
expect(el.getAttribute(attr)).toBe('e f');
|
||||
expect(list.contains('e')).toBeTrue();
|
||||
expect(list.contains('f')).toBeTrue();
|
||||
expect(list.contains('e')).toBe(true);
|
||||
expect(list.contains('f')).toBe(true);
|
||||
});
|
||||
|
||||
it('replace on non-existent token returns false and does not change tokens', () => {
|
||||
const list = attributeTokenList(el, attr);
|
||||
list.add('a', 'b');
|
||||
|
||||
expect(list.replace('x', 'y')).toBeFalse();
|
||||
expect(list.replace('x', 'y')).toBe(false);
|
||||
expect([...list]).toEqual(['a', 'b']);
|
||||
expect(el.getAttribute(attr)).toBe('a b');
|
||||
});
|
||||
@@ -326,7 +321,8 @@ describe('dom-helpers', () => {
|
||||
const tokens:string[] = [];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const token = list.item(i);
|
||||
if (token) tokens.push(token);
|
||||
if (token)
|
||||
tokens.push(token);
|
||||
}
|
||||
|
||||
expect(tokens).toEqual(['alpha', 'beta', 'gamma']);
|
||||
|
||||
@@ -54,19 +54,19 @@ describe('CheckAllController', () => {
|
||||
});
|
||||
|
||||
const checkAllTemplate = `
|
||||
<div data-controller="check-all" data-check-all-checkable-outlet="#checkables">
|
||||
<button id="check-all" data-action="check-all#checkAll">Check all</button>
|
||||
<button id="uncheck-all" data-action="check-all#uncheckAll">Uncheck all</button>
|
||||
</div>
|
||||
`;
|
||||
<div data-controller="check-all" data-check-all-checkable-outlet="#checkables">
|
||||
<button id="check-all" data-action="check-all#checkAll">Check all</button>
|
||||
<button id="uncheck-all" data-action="check-all#uncheckAll">Uncheck all</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const checkableTemplate = `
|
||||
<div id="checkables" data-controller="checkable">
|
||||
<input type="checkbox" data-checkable-target="checkbox">
|
||||
<input type="checkbox" data-checkable-target="checkbox">
|
||||
<input type="checkbox" data-checkable-target="checkbox">
|
||||
</div>
|
||||
`;
|
||||
<div id="checkables" data-controller="checkable">
|
||||
<input type="checkbox" data-checkable-target="checkbox">
|
||||
<input type="checkbox" data-checkable-target="checkbox">
|
||||
<input type="checkbox" data-checkable-target="checkbox">
|
||||
</div>
|
||||
`;
|
||||
|
||||
function appendTemplate(html:string) {
|
||||
const template = document.createElement('template');
|
||||
@@ -98,18 +98,18 @@ describe('CheckAllController', () => {
|
||||
it('toggles checkboxes', async () => {
|
||||
const inputs = Array.from(document.querySelectorAll<HTMLInputElement>('input[type="checkbox"]'));
|
||||
|
||||
expect(inputs).toHaveSize(3);
|
||||
expect(inputs.every((i) => !i.checked)).toBeTrue();
|
||||
expect(inputs).toHaveLength(3);
|
||||
expect(inputs.every((i) => !i.checked)).toBe(true);
|
||||
|
||||
document.getElementById('check-all')!.click();
|
||||
await nextFrame();
|
||||
|
||||
expect(inputs.every((i) => i.checked)).toBeTrue();
|
||||
expect(inputs.every((i) => i.checked)).toBe(true);
|
||||
|
||||
document.getElementById('uncheck-all')!.click();
|
||||
await nextFrame();
|
||||
|
||||
expect(inputs.every((i) => !i.checked)).toBeTrue();
|
||||
expect(inputs.every((i) => !i.checked)).toBe(true);
|
||||
});
|
||||
|
||||
it('applies aria-controls for connected outlet', () => {
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('CheckableController', () => {
|
||||
it('checks all when none are checked', () => {
|
||||
controller.toggleAll(new Event('click'));
|
||||
|
||||
expect(inputs.every((i) => i.checked)).toBeTrue();
|
||||
expect(inputs.every((i) => i.checked)).toBe(true);
|
||||
});
|
||||
|
||||
it('checks all when some are checked (mixed state)', () => {
|
||||
@@ -61,7 +61,7 @@ describe('CheckableController', () => {
|
||||
|
||||
controller.toggleAll(new Event('click'));
|
||||
|
||||
expect(inputs.every((i) => i.checked)).toBeTrue();
|
||||
expect(inputs.every((i) => i.checked)).toBe(true);
|
||||
});
|
||||
|
||||
it('unchecks all when all are checked', () => {
|
||||
@@ -69,17 +69,17 @@ describe('CheckableController', () => {
|
||||
|
||||
controller.toggleAll(new Event('click'));
|
||||
|
||||
expect(inputs.every((i) => !i.checked)).toBeTrue();
|
||||
expect(inputs.every((i) => !i.checked)).toBe(true);
|
||||
});
|
||||
|
||||
it('dispatches input event', () => {
|
||||
const dispatchSpy = spyOn(inputs[0], 'dispatchEvent').and.callThrough();
|
||||
const dispatchSpy = vi.spyOn(inputs[0], 'dispatchEvent');
|
||||
|
||||
controller.toggleAll(new Event('click'));
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
const eventArg = dispatchSpy.calls.mostRecent().args[0];
|
||||
const eventArg = vi.mocked(dispatchSpy).mock.lastCall![0];
|
||||
|
||||
expect(eventArg.type).toBe('input');
|
||||
expect(eventArg.bubbles).toBe(false);
|
||||
@@ -87,7 +87,7 @@ describe('CheckableController', () => {
|
||||
});
|
||||
|
||||
it('checkAll calls toggleChecked(true)', () => {
|
||||
spyOn(controller, 'toggleChecked').and.callFake(() => {});
|
||||
vi.spyOn(controller, 'toggleChecked').mockImplementation(() => { });
|
||||
|
||||
controller.checkAll(new Event('click'));
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('CheckableController', () => {
|
||||
});
|
||||
|
||||
it('uncheckAll calls toggleChecked(false)', () => {
|
||||
spyOn(controller, 'toggleChecked').and.callFake(() => {});
|
||||
vi.spyOn(controller, 'toggleChecked').mockImplementation(() => { });
|
||||
|
||||
controller.uncheckAll(new Event('click'));
|
||||
|
||||
@@ -139,9 +139,9 @@ describe('CheckableController', () => {
|
||||
controller.toggleSelection(event);
|
||||
|
||||
// Only admin checkboxes should be checked
|
||||
expect(inputs[0].checked).toBeTrue();
|
||||
expect(inputs[1].checked).toBeFalse();
|
||||
expect(inputs[2].checked).toBeTrue();
|
||||
expect(inputs[0].checked).toBe(true);
|
||||
expect(inputs[1].checked).toBe(false);
|
||||
expect(inputs[2].checked).toBe(true);
|
||||
});
|
||||
|
||||
it('unchecks all matching checkboxes when all are checked', () => {
|
||||
@@ -157,9 +157,9 @@ describe('CheckableController', () => {
|
||||
controller.toggleSelection(event);
|
||||
|
||||
// Only admin checkboxes should be unchecked
|
||||
expect(inputs[0].checked).toBeFalse();
|
||||
expect(inputs[1].checked).toBeTrue(); // member stays checked
|
||||
expect(inputs[2].checked).toBeFalse();
|
||||
expect(inputs[0].checked).toBe(false);
|
||||
expect(inputs[1].checked).toBe(true); // member stays checked
|
||||
expect(inputs[2].checked).toBe(false);
|
||||
});
|
||||
|
||||
it('works with numeric value params (converted to string)', () => {
|
||||
@@ -172,9 +172,9 @@ describe('CheckableController', () => {
|
||||
|
||||
controller.toggleSelection(event);
|
||||
|
||||
expect(inputs[0].checked).toBeTrue();
|
||||
expect(inputs[1].checked).toBeFalse();
|
||||
expect(inputs[2].checked).toBeTrue();
|
||||
expect(inputs[0].checked).toBe(true);
|
||||
expect(inputs[1].checked).toBe(false);
|
||||
expect(inputs[2].checked).toBe(true);
|
||||
});
|
||||
|
||||
it('works with boolean value params (converted to string)', () => {
|
||||
@@ -187,9 +187,9 @@ describe('CheckableController', () => {
|
||||
|
||||
controller.toggleSelection(event);
|
||||
|
||||
expect(inputs[0].checked).toBeTrue();
|
||||
expect(inputs[1].checked).toBeFalse();
|
||||
expect(inputs[2].checked).toBeTrue();
|
||||
expect(inputs[0].checked).toBe(true);
|
||||
expect(inputs[1].checked).toBe(false);
|
||||
expect(inputs[2].checked).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+11
-20
@@ -37,7 +37,7 @@ describe('GenericDragAndDropController', () => {
|
||||
controller = Object.create(GenericDragAndDropController.prototype) as GenericDragAndDropController;
|
||||
});
|
||||
|
||||
function setValue(name:'handleValue'|'handleSelectorValue', value:boolean|string) {
|
||||
function setValue(name:'handleValue' | 'handleSelectorValue', value:boolean | string) {
|
||||
Object.defineProperty(controller, name, { value, configurable: true });
|
||||
}
|
||||
|
||||
@@ -51,29 +51,20 @@ describe('GenericDragAndDropController', () => {
|
||||
return row;
|
||||
}
|
||||
|
||||
function callCanStartDrag(el:Element|null|undefined, handle:Element|null|undefined):boolean {
|
||||
const canStartDrag = Reflect.get(controller, 'canStartDrag') as (
|
||||
this:GenericDragAndDropController,
|
||||
el:Element|null|undefined,
|
||||
handle:Element|null|undefined
|
||||
) => boolean;
|
||||
function callCanStartDrag(el:Element | null | undefined, handle:Element | null | undefined):boolean {
|
||||
const canStartDrag = Reflect.get(controller, 'canStartDrag') as (this:GenericDragAndDropController, el:Element | null | undefined, handle:Element | null | undefined) => boolean;
|
||||
|
||||
return canStartDrag.call(controller, el, handle);
|
||||
}
|
||||
|
||||
function callAriaPressedTarget(el:Element):Element|null {
|
||||
const ariaPressedTarget = Reflect.get(controller, 'ariaPressedTarget') as (
|
||||
this:GenericDragAndDropController,
|
||||
el:Element
|
||||
) => Element|null;
|
||||
function callAriaPressedTarget(el:Element):Element | null {
|
||||
const ariaPressedTarget = Reflect.get(controller, 'ariaPressedTarget') as (this:GenericDragAndDropController, el:Element) => Element | null;
|
||||
|
||||
return ariaPressedTarget.call(controller, el);
|
||||
}
|
||||
|
||||
function callResolveMirrorContainer():Element {
|
||||
const resolveMirrorContainer = Reflect.get(controller, 'resolveMirrorContainer') as (
|
||||
this:GenericDragAndDropController
|
||||
) => Element;
|
||||
const resolveMirrorContainer = Reflect.get(controller, 'resolveMirrorContainer') as (this:GenericDragAndDropController) => Element;
|
||||
|
||||
return resolveMirrorContainer.call(controller);
|
||||
}
|
||||
@@ -85,7 +76,7 @@ describe('GenericDragAndDropController', () => {
|
||||
setValue('handleValue', false);
|
||||
setValue('handleSelectorValue', '.DragHandle');
|
||||
|
||||
expect(callCanStartDrag(row, row)).toBeTrue();
|
||||
expect(callCanStartDrag(row, row)).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects rows that are not draggable in handle-less mode', () => {
|
||||
@@ -96,7 +87,7 @@ describe('GenericDragAndDropController', () => {
|
||||
setValue('handleValue', false);
|
||||
setValue('handleSelectorValue', '.DragHandle');
|
||||
|
||||
expect(callCanStartDrag(row, row)).toBeFalse();
|
||||
expect(callCanStartDrag(row, row)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects empty placeholder rows in handle-less mode', () => {
|
||||
@@ -106,7 +97,7 @@ describe('GenericDragAndDropController', () => {
|
||||
setValue('handleValue', false);
|
||||
setValue('handleSelectorValue', '.DragHandle');
|
||||
|
||||
expect(callCanStartDrag(row, row)).toBeFalse();
|
||||
expect(callCanStartDrag(row, row)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects interactive descendants in handle-less mode', () => {
|
||||
@@ -117,7 +108,7 @@ describe('GenericDragAndDropController', () => {
|
||||
setValue('handleValue', false);
|
||||
setValue('handleSelectorValue', '.DragHandle');
|
||||
|
||||
expect(callCanStartDrag(row, button)).toBeFalse();
|
||||
expect(callCanStartDrag(row, button)).toBe(false);
|
||||
});
|
||||
|
||||
it('allows drag handles in handle mode', () => {
|
||||
@@ -129,7 +120,7 @@ describe('GenericDragAndDropController', () => {
|
||||
setValue('handleValue', true);
|
||||
setValue('handleSelectorValue', '.DragHandle');
|
||||
|
||||
expect(callCanStartDrag(row, handle)).toBeTrue();
|
||||
expect(callCanStartDrag(row, handle)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -68,17 +68,17 @@ describe('TruncationController', () => {
|
||||
});
|
||||
|
||||
const truncationTemplate = `
|
||||
<div data-controller="truncation" data-truncation-expanded-value="false">
|
||||
<div data-truncation-target="truncate" style="width: 200px; overflow: hidden;">
|
||||
<span class="Truncate-text" style="display: inline-block; white-space: nowrap;">
|
||||
This is a very long text that should be truncated when it exceeds the container width
|
||||
</span>
|
||||
</div>
|
||||
<div data-truncation-target="expander">
|
||||
<button type="button">Toggle</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<div data-controller="truncation" data-truncation-expanded-value="false">
|
||||
<div data-truncation-target="truncate" style="width: 200px; overflow: hidden;">
|
||||
<span class="Truncate-text" style="display: inline-block; white-space: nowrap;">
|
||||
This is a very long text that should be truncated when it exceeds the container width
|
||||
</span>
|
||||
</div>
|
||||
<div data-truncation-target="expander">
|
||||
<button type="button">Toggle</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function appendTemplate(html:string) {
|
||||
const template = document.createElement('template');
|
||||
@@ -93,10 +93,7 @@ describe('TruncationController', () => {
|
||||
});
|
||||
|
||||
it('connects successfully', () => {
|
||||
const controller = Stimulus.getControllerForElementAndIdentifier(
|
||||
document.querySelector('[data-controller="truncation"]')!,
|
||||
'truncation',
|
||||
);
|
||||
const controller = Stimulus.getControllerForElementAndIdentifier(document.querySelector('[data-controller="truncation"]')!, 'truncation');
|
||||
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
@@ -111,17 +108,14 @@ describe('TruncationController', () => {
|
||||
it('adds Truncate--expanded class when expanded value is true', async () => {
|
||||
const truncateEl = document.querySelector<HTMLElement>('[data-truncation-target="truncate"]')!;
|
||||
|
||||
expect(truncateEl.classList.contains('Truncate--expanded')).toBeFalse();
|
||||
expect(truncateEl.classList.contains('Truncate--expanded')).toBe(false);
|
||||
|
||||
const controller:any = Stimulus.getControllerForElementAndIdentifier(
|
||||
document.querySelector('[data-controller="truncation"]')!,
|
||||
'truncation',
|
||||
);
|
||||
const controller:any = Stimulus.getControllerForElementAndIdentifier(document.querySelector('[data-controller="truncation"]')!, 'truncation');
|
||||
|
||||
controller.expandedValue = true;
|
||||
await nextFrame();
|
||||
|
||||
expect(truncateEl.classList.contains('Truncate--expanded')).toBeTrue();
|
||||
expect(truncateEl.classList.contains('Truncate--expanded')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -135,20 +129,20 @@ describe('TruncationController', () => {
|
||||
const button = document.querySelector<HTMLButtonElement>('[data-truncation-target="expander"] button')!;
|
||||
const truncateEl = document.querySelector<HTMLElement>('[data-truncation-target="truncate"]')!;
|
||||
|
||||
expect(truncateEl.classList.contains('Truncate--expanded')).toBeFalse();
|
||||
expect(truncateEl.classList.contains('Truncate--expanded')).toBe(false);
|
||||
expect(button.getAttribute('aria-expanded')).toBe('false');
|
||||
|
||||
button.click();
|
||||
await nextFrame();
|
||||
|
||||
expect(truncateEl.classList.contains('Truncate--expanded')).toBeTrue();
|
||||
expect(truncateEl.classList.contains('Truncate--expanded')).toBe(true);
|
||||
expect(button.getAttribute('aria-expanded')).toBe('true');
|
||||
expect(button.getAttribute('aria-label')).toBe('Collapse text');
|
||||
|
||||
button.click();
|
||||
await nextFrame();
|
||||
|
||||
expect(truncateEl.classList.contains('Truncate--expanded')).toBeFalse();
|
||||
expect(truncateEl.classList.contains('Truncate--expanded')).toBe(false);
|
||||
expect(button.getAttribute('aria-expanded')).toBe('false');
|
||||
expect(button.getAttribute('aria-label')).toBe('Expand text');
|
||||
});
|
||||
@@ -162,10 +156,7 @@ describe('TruncationController', () => {
|
||||
|
||||
it('updates aria-label when expanded', async () => {
|
||||
const button = document.querySelector<HTMLButtonElement>('[data-truncation-target="expander"] button')!;
|
||||
const controller:any = Stimulus.getControllerForElementAndIdentifier(
|
||||
document.querySelector('[data-controller="truncation"]')!,
|
||||
'truncation',
|
||||
);
|
||||
const controller:any = Stimulus.getControllerForElementAndIdentifier(document.querySelector('[data-controller="truncation"]')!, 'truncation');
|
||||
|
||||
expect(button.getAttribute('aria-label')).toBe('Expand text');
|
||||
|
||||
@@ -177,10 +168,7 @@ describe('TruncationController', () => {
|
||||
|
||||
it('updates aria-expanded attribute', async () => {
|
||||
const button = document.querySelector<HTMLButtonElement>('[data-truncation-target="expander"] button')!;
|
||||
const controller:any = Stimulus.getControllerForElementAndIdentifier(
|
||||
document.querySelector('[data-controller="truncation"]')!,
|
||||
'truncation',
|
||||
);
|
||||
const controller:any = Stimulus.getControllerForElementAndIdentifier(document.querySelector('[data-controller="truncation"]')!, 'truncation');
|
||||
|
||||
expect(button.getAttribute('aria-expanded')).toBe('false');
|
||||
|
||||
@@ -192,22 +180,19 @@ describe('TruncationController', () => {
|
||||
|
||||
it('toggles Truncate--expanded class', async () => {
|
||||
const truncateEl = document.querySelector<HTMLElement>('[data-truncation-target="truncate"]')!;
|
||||
const controller:any = Stimulus.getControllerForElementAndIdentifier(
|
||||
document.querySelector('[data-controller="truncation"]')!,
|
||||
'truncation',
|
||||
);
|
||||
const controller:any = Stimulus.getControllerForElementAndIdentifier(document.querySelector('[data-controller="truncation"]')!, 'truncation');
|
||||
|
||||
expect(truncateEl.classList.contains('Truncate--expanded')).toBeFalse();
|
||||
expect(truncateEl.classList.contains('Truncate--expanded')).toBe(false);
|
||||
|
||||
controller.expandedValue = true;
|
||||
await nextFrame();
|
||||
|
||||
expect(truncateEl.classList.contains('Truncate--expanded')).toBeTrue();
|
||||
expect(truncateEl.classList.contains('Truncate--expanded')).toBe(true);
|
||||
|
||||
controller.expandedValue = false;
|
||||
await nextFrame();
|
||||
|
||||
expect(truncateEl.classList.contains('Truncate--expanded')).toBeFalse();
|
||||
expect(truncateEl.classList.contains('Truncate--expanded')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -221,17 +206,17 @@ describe('TruncationController', () => {
|
||||
|
||||
it('hides expander when content is not truncated', async () => {
|
||||
const shortTextTemplate = `
|
||||
<div data-controller="truncation" data-truncation-expanded-value="false">
|
||||
<div data-truncation-target="truncate" style="width: 500px; overflow: hidden;">
|
||||
<span class="Truncate-text" style="display: inline-block; white-space: nowrap;">
|
||||
Short text
|
||||
</span>
|
||||
</div>
|
||||
<div data-truncation-target="expander">
|
||||
<button type="button">Toggle</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<div data-controller="truncation" data-truncation-expanded-value="false">
|
||||
<div data-truncation-target="truncate" style="width: 500px; overflow: hidden;">
|
||||
<span class="Truncate-text" style="display: inline-block; white-space: nowrap;">
|
||||
Short text
|
||||
</span>
|
||||
</div>
|
||||
<div data-truncation-target="expander">
|
||||
<button type="button">Toggle</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
appendTemplate(shortTextTemplate);
|
||||
await waitForResize();
|
||||
@@ -239,58 +224,60 @@ describe('TruncationController', () => {
|
||||
const expander = document.querySelector<HTMLElement>('[data-truncation-target="expander"]')!;
|
||||
|
||||
// When content is not truncated, expander should be hidden
|
||||
expect(expander.hidden).toBeTrue();
|
||||
expect(expander.hidden).toBe(true);
|
||||
});
|
||||
|
||||
it('shows expander when content is truncated', async () => {
|
||||
const longTextTemplate = `
|
||||
<div data-controller="truncation" data-truncation-expanded-value="false">
|
||||
<div data-truncation-target="truncate" style="width: 50px; overflow: hidden;">
|
||||
<span class="Truncate-text" style="display: inline-block; white-space: nowrap; width: 300px;">
|
||||
This is a very long text that should definitely be truncated
|
||||
</span>
|
||||
</div>
|
||||
<div data-truncation-target="expander">
|
||||
<button type="button">Toggle</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<div data-controller="truncation" data-truncation-expanded-value="false">
|
||||
<div data-truncation-target="truncate" style="width: 50px; overflow: hidden;">
|
||||
<span class="Truncate-text" style="display: inline-block; white-space: nowrap; width: 300px;">
|
||||
This is a very long text that should definitely be truncated
|
||||
</span>
|
||||
</div>
|
||||
<div data-truncation-target="expander">
|
||||
<button type="button">Toggle</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
appendTemplate(longTextTemplate);
|
||||
|
||||
const truncateText = document.querySelector<HTMLElement>('.Truncate-text')!;
|
||||
Object.defineProperty(truncateText, 'scrollWidth', { value: 300, configurable: true });
|
||||
Object.defineProperty(truncateText, 'clientWidth', { value: 50, configurable: true });
|
||||
|
||||
await waitForResize();
|
||||
|
||||
const expander = document.querySelector<HTMLElement>('[data-truncation-target="expander"]')!;
|
||||
|
||||
// When content is truncated, expander should be visible
|
||||
expect(expander.hidden).toBeFalse();
|
||||
expect(expander.hidden).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resize() method', () => {
|
||||
it('calls update() when resize is triggered', async () => {
|
||||
const template = `
|
||||
<div data-controller="truncation" data-truncation-expanded-value="false">
|
||||
<div data-truncation-target="truncate" style="width: 100px; overflow: hidden;">
|
||||
<span class="Truncate-text" style="display: inline-block; white-space: nowrap;">
|
||||
Test text
|
||||
</span>
|
||||
</div>
|
||||
<div data-truncation-target="expander">
|
||||
<button type="button">Toggle</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<div data-controller="truncation" data-truncation-expanded-value="false">
|
||||
<div data-truncation-target="truncate" style="width: 100px; overflow: hidden;">
|
||||
<span class="Truncate-text" style="display: inline-block; white-space: nowrap;">
|
||||
Test text
|
||||
</span>
|
||||
</div>
|
||||
<div data-truncation-target="expander">
|
||||
<button type="button">Toggle</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
appendTemplate(template);
|
||||
await nextFrame();
|
||||
|
||||
const controller:any = Stimulus.getControllerForElementAndIdentifier(
|
||||
document.querySelector('[data-controller="truncation"]')!,
|
||||
'truncation',
|
||||
);
|
||||
const controller:any = Stimulus.getControllerForElementAndIdentifier(document.querySelector('[data-controller="truncation"]')!, 'truncation');
|
||||
|
||||
// Spy on the private update method to verify resize() calls it
|
||||
const updateSpy = spyOn<any>(controller, 'update').and.callThrough();
|
||||
const updateSpy = vi.spyOn(controller, 'update');
|
||||
|
||||
controller.resize();
|
||||
|
||||
@@ -299,25 +286,22 @@ describe('TruncationController', () => {
|
||||
|
||||
it('updates expander visibility when content dimensions change', async () => {
|
||||
const template = `
|
||||
<div data-controller="truncation" data-truncation-expanded-value="false">
|
||||
<div data-truncation-target="truncate" style="width: 100px; overflow: hidden;">
|
||||
<span class="Truncate-text" style="display: inline-block; white-space: nowrap;">
|
||||
Test
|
||||
</span>
|
||||
</div>
|
||||
<div data-truncation-target="expander">
|
||||
<button type="button">Toggle</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<div data-controller="truncation" data-truncation-expanded-value="false">
|
||||
<div data-truncation-target="truncate" style="width: 100px; overflow: hidden;">
|
||||
<span class="Truncate-text" style="display: inline-block; white-space: nowrap;">
|
||||
Test
|
||||
</span>
|
||||
</div>
|
||||
<div data-truncation-target="expander">
|
||||
<button type="button">Toggle</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
appendTemplate(template);
|
||||
await nextFrame();
|
||||
|
||||
const controller:any = Stimulus.getControllerForElementAndIdentifier(
|
||||
document.querySelector('[data-controller="truncation"]')!,
|
||||
'truncation',
|
||||
);
|
||||
const controller:any = Stimulus.getControllerForElementAndIdentifier(document.querySelector('[data-controller="truncation"]')!, 'truncation');
|
||||
const expander = document.querySelector<HTMLElement>('[data-truncation-target="expander"]')!;
|
||||
const truncateText = document.querySelector<HTMLElement>('.Truncate-text')!;
|
||||
|
||||
@@ -330,21 +314,21 @@ describe('TruncationController', () => {
|
||||
Object.defineProperty(truncateText, 'clientWidth', { configurable: true, value: 100 });
|
||||
controller.resize();
|
||||
|
||||
expect(expander.hidden).toBeTrue();
|
||||
expect(expander.hidden).toBe(true);
|
||||
|
||||
// Simulate truncated: scrollWidth > clientWidth
|
||||
Object.defineProperty(truncateText, 'scrollWidth', { configurable: true, value: 200 });
|
||||
Object.defineProperty(truncateText, 'clientWidth', { configurable: true, value: 100 });
|
||||
controller.resize();
|
||||
|
||||
expect(expander.hidden).toBeFalse();
|
||||
expect(expander.hidden).toBe(false);
|
||||
|
||||
// Simulate not truncated again
|
||||
Object.defineProperty(truncateText, 'scrollWidth', { configurable: true, value: 50 });
|
||||
Object.defineProperty(truncateText, 'clientWidth', { configurable: true, value: 50 });
|
||||
controller.resize();
|
||||
|
||||
expect(expander.hidden).toBeTrue();
|
||||
expect(expander.hidden).toBe(true);
|
||||
|
||||
// Restore original descriptors
|
||||
if (originalScrollWidth) {
|
||||
@@ -357,38 +341,35 @@ describe('TruncationController', () => {
|
||||
|
||||
it('keeps expander visible when expanded even if not truncated', async () => {
|
||||
const template = `
|
||||
<div data-controller="truncation" data-truncation-expanded-value="false">
|
||||
<div data-truncation-target="truncate" style="width: 200px; overflow: hidden;">
|
||||
<span class="Truncate-text" style="display: inline-block; white-space: nowrap;">
|
||||
Short
|
||||
</span>
|
||||
</div>
|
||||
<div data-truncation-target="expander">
|
||||
<button type="button">Toggle</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<div data-controller="truncation" data-truncation-expanded-value="false">
|
||||
<div data-truncation-target="truncate" style="width: 200px; overflow: hidden;">
|
||||
<span class="Truncate-text" style="display: inline-block; white-space: nowrap;">
|
||||
Short
|
||||
</span>
|
||||
</div>
|
||||
<div data-truncation-target="expander">
|
||||
<button type="button">Toggle</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
appendTemplate(template);
|
||||
await nextFrame();
|
||||
|
||||
const controller:any = Stimulus.getControllerForElementAndIdentifier(
|
||||
document.querySelector('[data-controller="truncation"]')!,
|
||||
'truncation',
|
||||
);
|
||||
const controller:any = Stimulus.getControllerForElementAndIdentifier(document.querySelector('[data-controller="truncation"]')!, 'truncation');
|
||||
const expander = document.querySelector<HTMLElement>('[data-truncation-target="expander"]')!;
|
||||
|
||||
// Initially short text, expander should be hidden
|
||||
controller.resize();
|
||||
|
||||
expect(expander.hidden).toBeTrue();
|
||||
expect(expander.hidden).toBe(true);
|
||||
|
||||
// Expand the text
|
||||
controller.expandedValue = true;
|
||||
await nextFrame();
|
||||
|
||||
// When expanded, expander should remain visible even if not truncated
|
||||
expect(expander.hidden).toBeFalse();
|
||||
expect(expander.hidden).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { I18n } from 'i18n-js';
|
||||
import lodash from 'lodash';
|
||||
import { registerDialogStreamAction } from 'core-turbo/dialog-stream-action';
|
||||
|
||||
registerDialogStreamAction();
|
||||
@@ -7,3 +8,42 @@ registerDialogStreamAction();
|
||||
(window as any).global = window;
|
||||
|
||||
window.I18n = new I18n();
|
||||
|
||||
// Production code expects `_` to be available globally (set in init-vendors.ts).
|
||||
// Mirror that here so production modules pulled in by spec compilation can run.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any)._ = lodash;
|
||||
|
||||
// jsdom does not implement CSS.escape; production helpers (e.g. getMetaElement)
|
||||
// call it unconditionally.
|
||||
if (typeof CSS === 'undefined' || typeof CSS.escape !== 'function') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).CSS = (globalThis as any).CSS || {};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).CSS.escape = (value:string) => String(value).replace(/[^a-zA-Z0-9_\-]/g, (ch) => `\\${ch}`);
|
||||
}
|
||||
|
||||
// jsdom does not implement ResizeObserver.
|
||||
if (typeof (globalThis as any).ResizeObserver === 'undefined') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).ResizeObserver = class {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
};
|
||||
}
|
||||
|
||||
// jsdom does not implement HTMLDialogElement.showModal/close.
|
||||
if (typeof HTMLDialogElement !== 'undefined') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const proto = HTMLDialogElement.prototype as any;
|
||||
if (typeof proto.showModal !== 'function') {
|
||||
proto.showModal = function showModal() { this.open = true; };
|
||||
}
|
||||
if (typeof proto.show !== 'function') {
|
||||
proto.show = function show() { this.open = true; };
|
||||
}
|
||||
if (typeof proto.close !== 'function') {
|
||||
proto.close = function close() { this.open = false; };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,13 @@
|
||||
},
|
||||
"files": [
|
||||
"src/test-setup.ts",
|
||||
"src/test-providers.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
"src/**/*.d.ts",
|
||||
"src/main.ts",
|
||||
"src/app/**/*.module.ts"
|
||||
]
|
||||
}
|
||||
|
||||
+28
-23
@@ -1,23 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { GitActionsMenuComponent } from "./git-actions-menu.component";
|
||||
import { GitActionsService } from "../git-actions/git-actions.service";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { OpIconComponent } from "core-app/shared/components/icon/icon.component";
|
||||
import { I18nService } from "core-app/core/i18n/i18n.service";
|
||||
import { OpContextMenuLocalsToken } from "core-app/shared/components/op-context-menu/op-context-menu.types";
|
||||
import { GitActionsMenuComponent } from './git-actions-menu.component';
|
||||
import { GitActionsService } from '../git-actions/git-actions.service';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { OpIconComponent } from 'core-app/shared/components/icon/icon.component';
|
||||
import { I18nService } from 'core-app/core/i18n/i18n.service';
|
||||
import { OpContextMenuLocalsToken } from 'core-app/shared/components/op-context-menu/op-context-menu.types';
|
||||
|
||||
|
||||
describe('GitActionsMenuComponent', () => {
|
||||
let component:GitActionsMenuComponent;
|
||||
let fixture:ComponentFixture<GitActionsMenuComponent>;
|
||||
let element:DebugElement;
|
||||
let gitActionsService:jasmine.SpyObj<GitActionsService>;
|
||||
let gitActionsService:{ gitCommand:ReturnType<typeof vi.fn>; commitMessage:ReturnType<typeof vi.fn>; commitMessageDisplayText:ReturnType<typeof vi.fn>; branchName:ReturnType<typeof vi.fn> };
|
||||
const I18nServiceStub = {
|
||||
t: function(key:string) {
|
||||
t: function (key:string) {
|
||||
return 'test translation';
|
||||
}
|
||||
}
|
||||
};
|
||||
const localsStub = {
|
||||
workPackage: 1,
|
||||
items: [
|
||||
@@ -28,23 +28,28 @@ describe('GitActionsMenuComponent', () => {
|
||||
linkText: 'linkText',
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const gitActionsServiceSpy = jasmine.createSpyObj('GitActionsService', ['gitCommand', 'commitMessage', 'commitMessageDisplayText', 'branchName']);
|
||||
const gitActionsServiceSpy = {
|
||||
gitCommand: vi.fn().mockName('GitActionsService.gitCommand'),
|
||||
commitMessage: vi.fn().mockName('GitActionsService.commitMessage'),
|
||||
commitMessageDisplayText: vi.fn().mockName('GitActionsService.commitMessageDisplayText'),
|
||||
branchName: vi.fn().mockName('GitActionsService.branchName')
|
||||
};
|
||||
|
||||
await TestBed
|
||||
.configureTestingModule({
|
||||
declarations: [
|
||||
GitActionsMenuComponent,
|
||||
OpIconComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: I18nServiceStub },
|
||||
{ provide: OpContextMenuLocalsToken, useValue: localsStub },
|
||||
{ provide: GitActionsService, useValue: gitActionsServiceSpy },
|
||||
],
|
||||
})
|
||||
declarations: [
|
||||
GitActionsMenuComponent,
|
||||
OpIconComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: I18nServiceStub },
|
||||
{ provide: OpContextMenuLocalsToken, useValue: localsStub },
|
||||
{ provide: GitActionsService, useValue: gitActionsServiceSpy },
|
||||
],
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
@@ -52,7 +57,7 @@ describe('GitActionsMenuComponent', () => {
|
||||
fixture = TestBed.createComponent(GitActionsMenuComponent);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.debugElement;
|
||||
gitActionsService = fixture.debugElement.injector.get(GitActionsService) as jasmine.SpyObj<GitActionsService>;
|
||||
gitActionsService = fixture.debugElement.injector.get(GitActionsService) as unknown as typeof gitActionsService;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -64,7 +69,7 @@ describe('GitActionsMenuComponent', () => {
|
||||
it('should generate the branch name on copy button click', () => {
|
||||
const copyButton = fixture.debugElement.query(By.css('.copy-button')).nativeElement;
|
||||
|
||||
gitActionsService.branchName.and.returnValue('test branch');
|
||||
gitActionsService.branchName.mockReturnValue('test branch');
|
||||
copyButton.click();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
+14
-15
@@ -27,10 +27,10 @@
|
||||
//++
|
||||
|
||||
import { GitActionsService } from './git-actions.service';
|
||||
import { WorkPackageResource } from "core-app/features/hal/resources/work-package-resource";
|
||||
import { PathHelperService } from "core-app/core/path-helper/path-helper.service";
|
||||
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
|
||||
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
|
||||
|
||||
describe('GitActionsService', function() {
|
||||
describe('GitActionsService', function () {
|
||||
let service:GitActionsService;
|
||||
|
||||
const createWorkPackage = (overrides = {}) => {
|
||||
@@ -44,7 +44,7 @@ describe('GitActionsService', function() {
|
||||
pathHelper: new PathHelperService()
|
||||
};
|
||||
const workPackage = { ...defaults, ...overrides };
|
||||
return(workPackage as WorkPackageResource);
|
||||
return (workPackage as WorkPackageResource);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -54,21 +54,20 @@ describe('GitActionsService', function() {
|
||||
|
||||
it('produces a branch name, commit message, and a git command', () => {
|
||||
const wp = createWorkPackage();
|
||||
expect(service.branchName(wp)).toEqual('user-story/42-find-the-question-or-don-t');
|
||||
expect(service.commitMessage(wp)).toEqual(
|
||||
`[#42] Find the question, or don't
|
||||
const origin = window.location.origin;
|
||||
|
||||
http://localhost:9876/wp/42`
|
||||
);
|
||||
expect(service.gitCommand(wp)).toEqual(
|
||||
`git checkout -b 'user-story/42-find-the-question-or-don-t' && git commit --allow-empty -m '[#42] Find the question, or don'\\''t' -m 'http://localhost:9876/wp/42'`
|
||||
);
|
||||
expect(service.branchName(wp)).toEqual('user-story/42-find-the-question-or-don-t');
|
||||
expect(service.commitMessage(wp)).toEqual(`[#42] Find the question, or don't
|
||||
|
||||
${origin}/wp/42`);
|
||||
|
||||
expect(service.gitCommand(wp)).toEqual(`git checkout -b 'user-story/42-find-the-question-or-don-t' && git commit --allow-empty -m '[#42] Find the question, or don'\\''t' -m '${origin}/wp/42'`);
|
||||
});
|
||||
|
||||
it('shell-escapes output for the git-command', () => {
|
||||
const wp = createWorkPackage({ subject: "' && rm -rf / #" });
|
||||
expect(service.gitCommand(wp)).toEqual(
|
||||
`git checkout -b 'user-story/42-and-and-rm-rf' && git commit --allow-empty -m '[#42] '\\'' && rm -rf / #' -m 'http://localhost:9876/wp/42'`
|
||||
);
|
||||
const origin = window.location.origin;
|
||||
|
||||
expect(service.gitCommand(wp)).toEqual(`git checkout -b 'user-story/42-and-and-rm-rf' && git commit --allow-empty -m '[#42] '\\'' && rm -rf / #' -m '${origin}/wp/42'`);
|
||||
});
|
||||
});
|
||||
|
||||
+46
-19
@@ -1,37 +1,55 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { GitHubTabComponent } from "core-app/features/plugins/linked/openproject-github_integration/github-tab/github-tab.component";
|
||||
import { TabPrsComponent } from "core-app/features/plugins/linked/openproject-github_integration/tab-prs/tab-prs.component";
|
||||
import { TabHeaderComponent } from "core-app/features/plugins/linked/openproject-github_integration/tab-header/tab-header.component";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { I18nService } from "core-app/core/i18n/i18n.service";
|
||||
import { PathHelperService } from "core-app/core/path-helper/path-helper.service";
|
||||
import { Component, DebugElement, Input } from '@angular/core';
|
||||
import { GitHubTabComponent } from 'core-app/features/plugins/linked/openproject-github_integration/github-tab/github-tab.component';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { I18nService } from 'core-app/core/i18n/i18n.service';
|
||||
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
|
||||
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
|
||||
|
||||
@Component({
|
||||
selector: 'tab-header',
|
||||
template: '',
|
||||
standalone: false,
|
||||
})
|
||||
class TabHeaderStubComponent {
|
||||
@Input() workPackage:WorkPackageResource;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'op-tab-prs',
|
||||
template: '',
|
||||
standalone: false,
|
||||
})
|
||||
class TabPrsStubComponent {
|
||||
@Input() workPackage:WorkPackageResource;
|
||||
}
|
||||
|
||||
describe('GitHubTabComponent.', () => {
|
||||
let component:GitHubTabComponent;
|
||||
let fixture:ComponentFixture<GitHubTabComponent>;
|
||||
let element:DebugElement;
|
||||
const workPackage = { id: 'testId' } as WorkPackageResource;
|
||||
const apiV3Base = 'http://www.openproject.com/api/v3/';
|
||||
const IPathHelperServiceStub = { api:{ v3: { apiV3Base }}};
|
||||
const IPathHelperServiceStub = { api: { v3: { apiV3Base } } };
|
||||
const I18nServiceStub = {
|
||||
t: function(key:string) {
|
||||
t: function (key:string) {
|
||||
return 'test translation';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed
|
||||
.configureTestingModule({
|
||||
declarations: [
|
||||
TabPrsComponent,
|
||||
TabHeaderComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: I18nServiceStub },
|
||||
{ provide: PathHelperService, useValue: IPathHelperServiceStub },
|
||||
],
|
||||
})
|
||||
declarations: [
|
||||
GitHubTabComponent,
|
||||
TabHeaderStubComponent,
|
||||
TabPrsStubComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: I18nServiceStub },
|
||||
{ provide: PathHelperService, useValue: IPathHelperServiceStub },
|
||||
],
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
@@ -39,6 +57,7 @@ describe('GitHubTabComponent.', () => {
|
||||
fixture = TestBed.createComponent(GitHubTabComponent);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.debugElement;
|
||||
component.workPackage = workPackage;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -54,4 +73,12 @@ describe('GitHubTabComponent.', () => {
|
||||
expect(tabHeader).toBeTruthy();
|
||||
expect(tabPrs).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should pass the work package to the child components', () => {
|
||||
const tabHeader = fixture.debugElement.query(By.directive(TabHeaderStubComponent));
|
||||
const tabPrs = fixture.debugElement.query(By.directive(TabPrsStubComponent));
|
||||
|
||||
expect(tabHeader.componentInstance.workPackage).toBe(workPackage);
|
||||
expect(tabPrs.componentInstance.workPackage).toBe(workPackage);
|
||||
});
|
||||
});
|
||||
|
||||
+16
-15
@@ -1,18 +1,19 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Component, DebugElement, Input } from '@angular/core';
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { PullRequestComponent } from "./pull-request.component";
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { PullRequestComponent } from './pull-request.component';
|
||||
import { OpIconComponent } from 'core-app/shared/components/icon/icon.component';
|
||||
import { IGithubCheckRunResource, IGithubPullRequest, IGithubUserResource } from '../state/github-pull-request.model';
|
||||
import { PullRequestStateComponent } from './pull-request-state.component';
|
||||
|
||||
@Component({
|
||||
selector: 'op-date-time',
|
||||
template: ``,
|
||||
template: '',
|
||||
standalone: false,
|
||||
})
|
||||
class OpDateTimeComponent {
|
||||
@Input('dateTimeValue') dateTimeValue:any;
|
||||
@Input()
|
||||
dateTimeValue:any;
|
||||
}
|
||||
|
||||
describe('PullRequestComponent', () => {
|
||||
@@ -39,10 +40,10 @@ describe('PullRequestComponent', () => {
|
||||
const pullRequestStub:IGithubPullRequest = {
|
||||
id: 3,
|
||||
additionsCount: 3,
|
||||
body:{
|
||||
body: {
|
||||
format: '',
|
||||
raw: 'test raw',
|
||||
html:'<p>test</p>',
|
||||
html: '<p>test</p>',
|
||||
},
|
||||
changedFilesCount: 3,
|
||||
commentsCount: 3,
|
||||
@@ -74,20 +75,20 @@ describe('PullRequestComponent', () => {
|
||||
_embedded: {
|
||||
githubUser,
|
||||
mergedBy: githubUser,
|
||||
checkRuns:[checkRun],
|
||||
checkRuns: [checkRun],
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed
|
||||
.configureTestingModule({
|
||||
declarations: [
|
||||
PullRequestComponent,
|
||||
OpDateTimeComponent,
|
||||
OpIconComponent,
|
||||
PullRequestStateComponent,
|
||||
],
|
||||
})
|
||||
declarations: [
|
||||
PullRequestComponent,
|
||||
OpDateTimeComponent,
|
||||
OpIconComponent,
|
||||
PullRequestStateComponent,
|
||||
],
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
|
||||
+24
-22
@@ -1,11 +1,11 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { TabHeaderComponent } from "core-app/features/plugins/linked/openproject-github_integration/tab-header/tab-header.component";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { OpIconComponent } from "core-app/shared/components/icon/icon.component";
|
||||
import { GitActionsMenuDirective } from "core-app/features/plugins/linked/openproject-github_integration/git-actions-menu/git-actions-menu.directive";
|
||||
import { OPContextMenuService } from "core-app/shared/components/op-context-menu/op-context-menu.service";
|
||||
import { I18nService } from "core-app/core/i18n/i18n.service";
|
||||
import { TabHeaderComponent } from 'core-app/features/plugins/linked/openproject-github_integration/tab-header/tab-header.component';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { OpIconComponent } from 'core-app/shared/components/icon/icon.component';
|
||||
import { GitActionsMenuDirective } from 'core-app/features/plugins/linked/openproject-github_integration/git-actions-menu/git-actions-menu.directive';
|
||||
import { OPContextMenuService } from 'core-app/shared/components/op-context-menu/op-context-menu.service';
|
||||
import { I18nService } from 'core-app/core/i18n/i18n.service';
|
||||
|
||||
|
||||
describe('TabHeaderComponent', () => {
|
||||
@@ -13,29 +13,31 @@ describe('TabHeaderComponent', () => {
|
||||
let fixture:ComponentFixture<TabHeaderComponent>;
|
||||
let element:DebugElement;
|
||||
const I18nServiceStub = {
|
||||
t: function(key:string) {
|
||||
t: function (key:string) {
|
||||
return 'test translation';
|
||||
}
|
||||
}
|
||||
let oPContextMenuService:jasmine.SpyObj<OPContextMenuService>;
|
||||
};
|
||||
let oPContextMenuService:{ show:ReturnType<typeof vi.fn> };
|
||||
// @ts-ignore
|
||||
window.Mousetrap = () => () => {};
|
||||
window.Mousetrap = () => () => { };
|
||||
|
||||
beforeEach(async () => {
|
||||
const oPContextMenuServiceSpy = jasmine.createSpyObj('OPContextMenuService', ['show']);
|
||||
const oPContextMenuServiceSpy = {
|
||||
show: vi.fn().mockName('OPContextMenuService.show')
|
||||
};
|
||||
|
||||
await TestBed
|
||||
.configureTestingModule({
|
||||
declarations: [
|
||||
TabHeaderComponent,
|
||||
OpIconComponent,
|
||||
GitActionsMenuDirective,
|
||||
],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: I18nServiceStub },
|
||||
{ provide: OPContextMenuService, useValue: oPContextMenuServiceSpy },
|
||||
],
|
||||
})
|
||||
declarations: [
|
||||
TabHeaderComponent,
|
||||
OpIconComponent,
|
||||
GitActionsMenuDirective,
|
||||
],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: I18nServiceStub },
|
||||
{ provide: OPContextMenuService, useValue: oPContextMenuServiceSpy },
|
||||
],
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
@@ -43,7 +45,7 @@ describe('TabHeaderComponent', () => {
|
||||
fixture = TestBed.createComponent(TabHeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.debugElement;
|
||||
oPContextMenuService = fixture.debugElement.injector.get(OPContextMenuService) as jasmine.SpyObj<OPContextMenuService>;
|
||||
oPContextMenuService = fixture.debugElement.injector.get(OPContextMenuService) as unknown as typeof oPContextMenuService;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ChangeDetectorRef, Component, DebugElement, Input } from '@angular/core';
|
||||
import { OpIconComponent } from "core-app/shared/components/icon/icon.component";
|
||||
import { GitActionsMenuDirective } from "core-app/features/plugins/linked/openproject-github_integration/git-actions-menu/git-actions-menu.directive";
|
||||
import { TabPrsComponent } from "core-app/features/plugins/linked/openproject-github_integration/tab-prs/tab-prs.component";
|
||||
import { OpIconComponent } from 'core-app/shared/components/icon/icon.component';
|
||||
import { GitActionsMenuDirective } from 'core-app/features/plugins/linked/openproject-github_integration/git-actions-menu/git-actions-menu.directive';
|
||||
import { TabPrsComponent } from 'core-app/features/plugins/linked/openproject-github_integration/tab-prs/tab-prs.component';
|
||||
import { GithubPullRequestResourceService } from '../state/github-pull-request.service';
|
||||
import { ApiV3Service } from "core-app/core/apiv3/api-v3.service";
|
||||
import { of } from "rxjs";
|
||||
import { PullRequestComponent } from "core-app/features/plugins/linked/openproject-github_integration/pull-request/pull-request.component";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { I18nService } from "core-app/core/i18n/i18n.service";
|
||||
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
|
||||
import { of } from 'rxjs';
|
||||
import { PullRequestComponent } from 'core-app/features/plugins/linked/openproject-github_integration/pull-request/pull-request.component';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { I18nService } from 'core-app/core/i18n/i18n.service';
|
||||
import { IGithubPullRequest } from '../state/github-pull-request.model';
|
||||
import { PullRequestStateComponent } from '../pull-request/pull-request-state.component';
|
||||
|
||||
@@ -26,18 +26,18 @@ describe('TabPrsComponent', () => {
|
||||
let component:TabPrsComponent;
|
||||
let fixture:ComponentFixture<TabPrsComponent>;
|
||||
let element:DebugElement;
|
||||
let githubPullRequestResourceServiceSpy:jasmine.SpyObj<GithubPullRequestResourceService>;
|
||||
let changeDetectorRef: jasmine.SpyObj<ChangeDetectorRef>;
|
||||
let githubPullRequestResourceServiceSpy:{ ofWorkPackage:ReturnType<typeof vi.fn> };
|
||||
let changeDetectorRef:ChangeDetectorRef & { detectChanges:ReturnType<typeof vi.fn> };
|
||||
const I18nServiceStub = {
|
||||
t: function(key:string) {
|
||||
t: function (key:string) {
|
||||
return 'test translation';
|
||||
}
|
||||
}
|
||||
};
|
||||
const ApiV3Stub = {
|
||||
work_packages: {
|
||||
id: () => ({github_pull_requests: 'prpath'})
|
||||
id: () => ({ github_pull_requests: 'prpath' })
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const pullRequests:IGithubPullRequest[] = [
|
||||
{
|
||||
@@ -123,28 +123,32 @@ describe('TabPrsComponent', () => {
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
const changeDetectorSpy = jasmine.createSpyObj('ChangeDetectorRef', ['detectChanges']);
|
||||
githubPullRequestResourceServiceSpy = jasmine.createSpyObj('GithubPullRequestResourceService', ['ofWorkPackage']);
|
||||
const changeDetectorSpy = {
|
||||
detectChanges: vi.fn().mockName('ChangeDetectorRef.detectChanges')
|
||||
};
|
||||
githubPullRequestResourceServiceSpy = {
|
||||
ofWorkPackage: vi.fn().mockName('GithubPullRequestResourceService.ofWorkPackage')
|
||||
};
|
||||
// @ts-ignore
|
||||
githubPullRequestResourceServiceSpy.ofWorkPackage.and.returnValue(of(pullRequests));
|
||||
githubPullRequestResourceServiceSpy.ofWorkPackage.mockReturnValue(of(pullRequests));
|
||||
|
||||
await TestBed
|
||||
.configureTestingModule({
|
||||
declarations: [
|
||||
TabPrsComponent,
|
||||
OpIconComponent,
|
||||
GitActionsMenuDirective,
|
||||
PullRequestComponent,
|
||||
PullRequestStateComponent,
|
||||
OpDateTimeComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: I18nServiceStub },
|
||||
{ provide: ApiV3Service, useValue: ApiV3Stub },
|
||||
{ provide: ChangeDetectorRef, useValue: changeDetectorSpy },
|
||||
{ provide: GithubPullRequestResourceService, useValue: githubPullRequestResourceServiceSpy },
|
||||
],
|
||||
})
|
||||
declarations: [
|
||||
TabPrsComponent,
|
||||
OpIconComponent,
|
||||
GitActionsMenuDirective,
|
||||
PullRequestComponent,
|
||||
PullRequestStateComponent,
|
||||
OpDateTimeComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: I18nServiceStub },
|
||||
{ provide: ApiV3Service, useValue: ApiV3Stub },
|
||||
{ provide: ChangeDetectorRef, useValue: changeDetectorSpy },
|
||||
{ provide: GithubPullRequestResourceService, useValue: githubPullRequestResourceServiceSpy },
|
||||
],
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
@@ -152,7 +156,7 @@ describe('TabPrsComponent', () => {
|
||||
fixture = TestBed.createComponent(TabPrsComponent);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.debugElement;
|
||||
changeDetectorRef = fixture.debugElement.injector.get(ChangeDetectorRef) as jasmine.SpyObj<ChangeDetectorRef>;
|
||||
changeDetectorRef = fixture.debugElement.injector.get(ChangeDetectorRef) as ChangeDetectorRef & { detectChanges:ReturnType<typeof vi.fn> };
|
||||
// @ts-ignore
|
||||
component.workPackage = { id: 'testId' };
|
||||
|
||||
|
||||
+14
-15
@@ -27,10 +27,10 @@
|
||||
//++
|
||||
|
||||
import { GitActionsService } from './git-actions.service';
|
||||
import { WorkPackageResource } from "core-app/features/hal/resources/work-package-resource";
|
||||
import { PathHelperService } from "core-app/core/path-helper/path-helper.service";
|
||||
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
|
||||
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
|
||||
|
||||
describe('GitActionsService', function() {
|
||||
describe('GitActionsService', function () {
|
||||
let service:GitActionsService;
|
||||
|
||||
const createWorkPackage = (overrides = {}) => {
|
||||
@@ -44,7 +44,7 @@ describe('GitActionsService', function() {
|
||||
pathHelper: new PathHelperService()
|
||||
};
|
||||
const workPackage = { ...defaults, ...overrides };
|
||||
return(workPackage as WorkPackageResource);
|
||||
return (workPackage as WorkPackageResource);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -54,21 +54,20 @@ describe('GitActionsService', function() {
|
||||
|
||||
it('produces a branch name, commit message, and a git command', () => {
|
||||
const wp = createWorkPackage();
|
||||
expect(service.branchName(wp)).toEqual('user-story/42-find-the-question-or-don-t');
|
||||
expect(service.commitMessage(wp)).toEqual(
|
||||
`OP#42 Find the question, or don't
|
||||
const origin = window.location.origin;
|
||||
|
||||
http://localhost:9876/work_packages/42`
|
||||
);
|
||||
expect(service.gitCommand(wp)).toEqual(
|
||||
`git checkout -b 'user-story/42-find-the-question-or-don-t' && git commit --allow-empty -m 'OP#42 Find the question, or don'\\''t' -m 'http://localhost:9876/work_packages/42'`
|
||||
);
|
||||
expect(service.branchName(wp)).toEqual('user-story/42-find-the-question-or-don-t');
|
||||
expect(service.commitMessage(wp)).toEqual(`OP#42 Find the question, or don't
|
||||
|
||||
${origin}/work_packages/42`);
|
||||
|
||||
expect(service.gitCommand(wp)).toEqual(`git checkout -b 'user-story/42-find-the-question-or-don-t' && git commit --allow-empty -m 'OP#42 Find the question, or don'\\''t' -m '${origin}/work_packages/42'`);
|
||||
});
|
||||
|
||||
it('shell-escapes output for the git-command', () => {
|
||||
const wp = createWorkPackage({ subject: "' && rm -rf / #" });
|
||||
expect(service.gitCommand(wp)).toEqual(
|
||||
`git checkout -b 'user-story/42-and-and-rm-rf' && git commit --allow-empty -m 'OP#42 '\\'' && rm -rf / #' -m 'http://localhost:9876/work_packages/42'`
|
||||
);
|
||||
const origin = window.location.origin;
|
||||
|
||||
expect(service.gitCommand(wp)).toEqual(`git checkout -b 'user-story/42-and-and-rm-rf' && git commit --allow-empty -m 'OP#42 '\\'' && rm -rf / #' -m '${origin}/work_packages/42'`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user