From 31e75f7c7ca072fdd0cb354b9066394af51771d7 Mon Sep 17 00:00:00 2001 From: aasandei-vsp Date: Thu, 2 Oct 2025 15:45:59 +0300 Subject: [PATCH 1/4] [PER-10311] Add unlisted share for users with no account --- .../share-preview.component.html | 2 +- .../share-preview.component.spec.ts | 29 +++++++++++++++++++ .../share-preview/share-preview.component.ts | 2 +- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/app/share-preview/components/share-preview/share-preview.component.html b/src/app/share-preview/components/share-preview/share-preview.component.html index 3c71b8c15..91fc25705 100644 --- a/src/app/share-preview/components/share-preview/share-preview.component.html +++ b/src/app/share-preview/components/share-preview/share-preview.component.html @@ -327,7 +327,7 @@ diff --git a/src/app/share-preview/components/share-preview/share-preview.component.spec.ts b/src/app/share-preview/components/share-preview/share-preview.component.spec.ts index c5a4e0623..f7562611e 100644 --- a/src/app/share-preview/components/share-preview/share-preview.component.spec.ts +++ b/src/app/share-preview/components/share-preview/share-preview.component.spec.ts @@ -20,6 +20,7 @@ import { DialogCdkService } from '@root/app/dialog-cdk/dialog-cdk.service'; import { AccountVO, ArchiveVO, RecordVO } from '@root/app/models'; import { AuthResponse } from '@shared/services/api/auth.repo'; import { Subject } from 'rxjs'; +import { ShareLinksService } from '@root/app/share-links/services/share-links.service'; import { CreateAccountDialogComponent } from '../create-account-dialog/create-account-dialog.component'; import { SharePreviewComponent } from './share-preview.component'; @@ -59,6 +60,11 @@ mockAccountService.setRedirect.and.stub(); mockAccountService.archiveChange = new Subject(); mockAccountService.accountChange = new Subject(); +const mockShareLinksService = { + currentShareToken: null, + isUnlistedShare: () => true, +}; + describe('SharePreviewComponent', () => { let component: SharePreviewComponent; let fixture: ComponentFixture; @@ -98,6 +104,11 @@ describe('SharePreviewComponent', () => { useValue: mockRoute, }); + config.providers.push({ + provide: ShareLinksService, + useValue: mockShareLinksService, + }); + await TestBed.configureTestingModule(config).compileComponents(); dialog = TestBed.inject(DialogCdkService); @@ -113,6 +124,16 @@ describe('SharePreviewComponent', () => { expect(component).toBeTruthy(); }); + it('should mark it as unlisted share if restrictions are none', fakeAsync(() => { + spyOn(mockShareLinksService, 'isUnlistedShare').and.returnValue(true); + component.ngOnInit(); + + expect(mockShareLinksService.isUnlistedShare).toHaveBeenCalled(); + tick(1005); + + expect(component.isUnlistedShare).toEqual(true); + })); + it('should open dialog shortly after loading if user is logged out', fakeAsync(() => { const dialogRefSpy = jasmine.createSpyObj('DialogRef', ['close']); const dialogSpy = spyOn(dialog, 'open').and.returnValue(dialogRefSpy); @@ -135,6 +156,14 @@ describe('SharePreviewComponent', () => { expect(dialogSpy).not.toHaveBeenCalled(); }); + it('should not open dialog shortly after loading if share is unlisted', fakeAsync(() => { + const dialogSpy = spyOn(dialog, 'open'); + component.isUnlistedShare = true; + tick(1005); + + expect(dialogSpy).not.toHaveBeenCalled(); + })); + it('should not open dialog shortly after loading if user is logged in', fakeAsync(() => { const dialogSpy = spyOn(dialog, 'open'); component.isLoggedIn = true; diff --git a/src/app/share-preview/components/share-preview/share-preview.component.ts b/src/app/share-preview/components/share-preview/share-preview.component.ts index 343ad38d5..623c7630a 100644 --- a/src/app/share-preview/components/share-preview/share-preview.component.ts +++ b/src/app/share-preview/components/share-preview/share-preview.component.ts @@ -196,7 +196,7 @@ export class SharePreviewComponent implements OnInit, OnDestroy { this.sendGaEvent('previewed'); } - if (!this.isLoggedIn) { + if (!this.isLoggedIn && !this.isUnlistedShare) { setTimeout(() => { this.showCreateAccountDialog(); }, 1000); From 77f4ce96ee783da231b702ced1c6cfa9371ba22f Mon Sep 17 00:00:00 2001 From: aasandei-vsp Date: Fri, 10 Oct 2025 14:09:57 +0300 Subject: [PATCH 2/4] [PER-10311] Add missing unit test --- .../share-preview/share-preview.component.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/app/share-preview/components/share-preview/share-preview.component.spec.ts b/src/app/share-preview/components/share-preview/share-preview.component.spec.ts index f7562611e..494cd2a29 100644 --- a/src/app/share-preview/components/share-preview/share-preview.component.spec.ts +++ b/src/app/share-preview/components/share-preview/share-preview.component.spec.ts @@ -147,6 +147,18 @@ describe('SharePreviewComponent', () => { }); })); + it('should not open dialog if user is logged out, but it is an unlisted share', fakeAsync(() => { + const dialogRefSpy = jasmine.createSpyObj('DialogRef', ['close']); + const dialogSpy = spyOn(dialog, 'open').and.returnValue(dialogRefSpy); + + component.isLoggedIn = false; + component.isUnlistedShare = true; + component.ngOnInit(); + tick(1005); + + expect(dialogSpy).not.toHaveBeenCalled(); + })); + it('should not open dialog if already open', () => { const dialogSpy = spyOn(dialog, 'open'); component.createAccountDialogIsOpen = true; From 7416decbabf214e529d24da121846fbd56f8255a Mon Sep 17 00:00:00 2001 From: aasandei-vsp Date: Mon, 13 Oct 2025 11:13:35 +0300 Subject: [PATCH 3/4] [PER-10311] Update unit tests for unlisted share create account dialog --- .../share-preview.component.spec.ts | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/app/share-preview/components/share-preview/share-preview.component.spec.ts b/src/app/share-preview/components/share-preview/share-preview.component.spec.ts index 494cd2a29..dac219c72 100644 --- a/src/app/share-preview/components/share-preview/share-preview.component.spec.ts +++ b/src/app/share-preview/components/share-preview/share-preview.component.spec.ts @@ -134,11 +134,12 @@ describe('SharePreviewComponent', () => { expect(component.isUnlistedShare).toEqual(true); })); - it('should open dialog shortly after loading if user is logged out', fakeAsync(() => { + it('should open dialog shortly after loading if user is logged out and it is not an unlisted share', fakeAsync(() => { const dialogRefSpy = jasmine.createSpyObj('DialogRef', ['close']); const dialogSpy = spyOn(dialog, 'open').and.returnValue(dialogRefSpy); component.isLoggedIn = false; + component.isUnlistedShare = false; component.showCreateAccountDialog(); tick(1005); @@ -159,30 +160,35 @@ describe('SharePreviewComponent', () => { expect(dialogSpy).not.toHaveBeenCalled(); })); - it('should not open dialog if already open', () => { - const dialogSpy = spyOn(dialog, 'open'); - component.createAccountDialogIsOpen = true; + it('should not open dialog if user is logged in and it is not an unlisted share', fakeAsync(() => { + const dialogRefSpy = jasmine.createSpyObj('DialogRef', ['close']); + const dialogSpy = spyOn(dialog, 'open').and.returnValue(dialogRefSpy); - component.showCreateAccountDialog(); + component.isLoggedIn = true; + component.isUnlistedShare = false; + component.ngOnInit(); + tick(1005); expect(dialogSpy).not.toHaveBeenCalled(); - }); + })); - it('should not open dialog shortly after loading if share is unlisted', fakeAsync(() => { + it('should not open dialog shortly after loading if user is logged in and share is unlisted', fakeAsync(() => { const dialogSpy = spyOn(dialog, 'open'); + component.isLoggedIn = true; component.isUnlistedShare = true; tick(1005); expect(dialogSpy).not.toHaveBeenCalled(); })); - it('should not open dialog shortly after loading if user is logged in', fakeAsync(() => { + it('should not open dialog if already open', () => { const dialogSpy = spyOn(dialog, 'open'); - component.isLoggedIn = true; - tick(1005); + component.createAccountDialogIsOpen = true; + + component.showCreateAccountDialog(); expect(dialogSpy).not.toHaveBeenCalled(); - })); + }); it('should open dialog when a thumbnail is clicked', fakeAsync(() => { const dialogRefSpy = jasmine.createSpyObj('DialogRef', ['close']); From 7cafd9fd39541a5281326f76602060bc075dc6eb Mon Sep 17 00:00:00 2001 From: aasandei-vsp Date: Mon, 13 Oct 2025 12:45:02 +0300 Subject: [PATCH 4/4] [PER-10311] Add unit tests for onRequestAccessClick and reloadSharePreviewData methods --- .../share-preview.component.spec.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/app/share-preview/components/share-preview/share-preview.component.spec.ts b/src/app/share-preview/components/share-preview/share-preview.component.spec.ts index dac219c72..2d0af6e97 100644 --- a/src/app/share-preview/components/share-preview/share-preview.component.spec.ts +++ b/src/app/share-preview/components/share-preview/share-preview.component.spec.ts @@ -21,6 +21,9 @@ import { AccountVO, ArchiveVO, RecordVO } from '@root/app/models'; import { AuthResponse } from '@shared/services/api/auth.repo'; import { Subject } from 'rxjs'; import { ShareLinksService } from '@root/app/share-links/services/share-links.service'; +import { ApiService } from '@shared/services/api/api.service'; +import { GoogleAnalyticsService } from '@shared/services/google-analytics/google-analytics.service'; +import { ShareResponse } from '@shared/services/api/share.repo'; import { CreateAccountDialogComponent } from '../create-account-dialog/create-account-dialog.component'; import { SharePreviewComponent } from './share-preview.component'; @@ -41,6 +44,10 @@ export const mockAccountService = jasmine.createSpyObj('AccountService', [ const defaultAccount = new AccountVO({ primaryEmail: 'test@example.com' }); const defaultArchive = new ArchiveVO({ archiveId: 123 }); +const mockGoogleAnalyticsService = { + sendEvent: jasmine.createSpy(), +}; + mockAccountService.getAccount.and.returnValue(defaultAccount); mockAccountService.getArchive.and.returnValue(defaultArchive); mockAccountService.isLoggedIn.and.returnValue(true); @@ -70,6 +77,7 @@ describe('SharePreviewComponent', () => { let fixture: ComponentFixture; let dialog: DialogCdkService; let router: Router; + let apiService: ApiService; beforeEach(async () => { const config: TestModuleMetadata = cloneDeep(Testing.BASE_TEST_CONFIG); @@ -109,10 +117,16 @@ describe('SharePreviewComponent', () => { useValue: mockShareLinksService, }); + config.providers.push({ + provide: GoogleAnalyticsService, + useValue: mockGoogleAnalyticsService, + }); + await TestBed.configureTestingModule(config).compileComponents(); dialog = TestBed.inject(DialogCdkService); router = TestBed.inject(Router); + apiService = TestBed.inject(ApiService); spyOn(router, 'navigate'); fixture = TestBed.createComponent(SharePreviewComponent); @@ -259,4 +273,61 @@ describe('SharePreviewComponent', () => { expect(router.navigate).toHaveBeenCalledWith(['/app', 'auth', 'signup']); }); + + it('should reload share preview data for link share', fakeAsync(() => { + component.isLinkShare = true; + component.isRelationshipShare = false; + + const mockVO = { ShareVO: { status: 'ok', accessRole: 'editor' } }; + spyOn(apiService.share, 'checkShareLink').and.returnValue( + Promise.resolve({ + isSuccessful: true, + getShareByUrlVO: () => mockVO, + } as unknown as ShareResponse), + ); + + spyOn(component, 'checkAccess'); + + component.reloadSharePreviewData(); + tick(1005); + + expect(apiService.share.checkShareLink).toHaveBeenCalled(); + expect(component.sharePreviewVO).toEqual(mockVO); + expect(component.checkAccess).toHaveBeenCalled(); + })); + + it('should reload share preview data for relationship share', fakeAsync(() => { + const mockVO = { ShareVO: { status: 'ok', accessRole: 'owner' } }; + spyOn(apiService.share, 'getShareForPreview').and.returnValue( + Promise.resolve({ + getShareVO: () => mockVO, + } as unknown as ShareResponse), + ); + component.isLinkShare = false; + component.isRelationshipShare = true; + + spyOn(component, 'checkAccess'); + + component.reloadSharePreviewData(); + tick(1005); + + expect(apiService.share.getShareForPreview).toHaveBeenCalled(); + expect(component.sharePreviewVO).toEqual(mockVO); + expect(component.checkAccess).toHaveBeenCalled(); + })); + + it('should request access and not show cover', fakeAsync(() => { + component.archiveConfirmed = false; + component.chooseArchiveText = 'Choose archive'; + component.shareToken = 'mock-token'; + component.shareAccount = { + fullName: 'Sharer Name', + } as unknown as AccountVO; + + component.onRequestAccessClick(); + tick(2005); + + expect(component.hasRequested).toBeTrue(); + expect(component.showCover).toBeFalse(); + })); });