| import { |
| AnnotationGranularity, |
| AnnotationType, |
| canAddAnnotation, |
| canEditAnnotation, |
| getAnnotationAuthorship, |
| getAnnotationGranularity, |
| getAnnotationTimeLabel, |
| groupAnnotationsByTimeLabel |
| } from './annotations' |
| import { Interval } from '../stats/graph/intervals' |
| import { Role, UserContextValue } from '../user-context' |
|
|
| const loggedInUser = (role: Role, id: number = 42): UserContextValue => ({ |
| loggedIn: true, |
| id, |
| role, |
| team: { identifier: null, hasConsolidatedView: false } |
| }) |
|
|
| const publicUser: UserContextValue = { |
| loggedIn: false, |
| id: null, |
| role: Role.public, |
| team: { identifier: null, hasConsolidatedView: false } |
| } |
|
|
| describe(`${getAnnotationAuthorship.name}`, () => { |
| it('returns the owner name for a site annotation with an owner', () => { |
| expect( |
| getAnnotationAuthorship({ |
| type: AnnotationType.site, |
| owner_name: 'Alice' |
| }) |
| ).toBe('Alice') |
| }) |
|
|
| it('returns "Site note" for a site annotation with a dangling owner', () => { |
| expect( |
| getAnnotationAuthorship({ |
| type: AnnotationType.site, |
| owner_name: null |
| }) |
| ).toBe('Site note') |
| }) |
|
|
| it('returns "Personal note" for a personal annotation regardless of owner (we assume personal notes are served only to the author)', () => { |
| expect( |
| getAnnotationAuthorship({ |
| type: AnnotationType.personal, |
| owner_name: 'Alice' |
| }) |
| ).toBe('Personal note') |
| }) |
| }) |
|
|
| describe(`${canAddAnnotation.name}`, () => { |
| it.each([[Role.admin], [Role.editor], [Role.owner]])( |
| 'allows adding a site annotation if the user is logged in, in role %p, and the site-annotations feature is available', |
| (role) => { |
| expect( |
| canAddAnnotation({ |
| type: AnnotationType.site, |
| user: loggedInUser(role), |
| siteAnnotationsAvailable: true |
| }) |
| ).toBe(true) |
| } |
| ) |
|
|
| it.each([[Role.admin], [Role.editor], [Role.owner]])( |
| 'forbids adding a site annotation in role %p when the feature is unavailable (billing gate)', |
| (role) => { |
| expect( |
| canAddAnnotation({ |
| type: AnnotationType.site, |
| user: loggedInUser(role), |
| siteAnnotationsAvailable: false |
| }) |
| ).toBe(false) |
| } |
| ) |
|
|
| it.each([[Role.viewer], [Role.billing]])( |
| 'forbids adding a site annotation in role %p even when the feature is available', |
| (role) => { |
| expect( |
| canAddAnnotation({ |
| type: AnnotationType.site, |
| user: loggedInUser(role), |
| siteAnnotationsAvailable: true |
| }) |
| ).toBe(false) |
| } |
| ) |
|
|
| it.each([ |
| [Role.viewer], |
| [Role.billing], |
| [Role.editor], |
| [Role.admin], |
| [Role.owner] |
| ])( |
| 'allows adding a personal annotation in role %p regardless of site-annotations availability', |
| (role) => { |
| expect( |
| canAddAnnotation({ |
| type: AnnotationType.personal, |
| user: loggedInUser(role), |
| siteAnnotationsAvailable: true |
| }) |
| ).toBe(true) |
| expect( |
| canAddAnnotation({ |
| type: AnnotationType.personal, |
| user: loggedInUser(role), |
| siteAnnotationsAvailable: false |
| }) |
| ).toBe(true) |
| } |
| ) |
|
|
| it.each([[AnnotationType.personal], [AnnotationType.site]])( |
| 'forbids the public role from adding %s annotations', |
| (type) => { |
| expect( |
| canAddAnnotation({ |
| type, |
| user: publicUser, |
| siteAnnotationsAvailable: true |
| }) |
| ).toBe(false) |
| } |
| ) |
| }) |
|
|
| describe(`${canEditAnnotation.name}`, () => { |
| it.each([[Role.admin], [Role.editor], [Role.owner]])( |
| 'allows editing a site annotation if the user is logged in and in role %p', |
| (role) => { |
| expect( |
| canEditAnnotation({ |
| type: AnnotationType.site, |
| user: loggedInUser(role) |
| }) |
| ).toBe(true) |
| } |
| ) |
|
|
| it.each([[Role.viewer], [Role.billing]])( |
| 'forbids editing site annotations in role %p', |
| (role) => { |
| expect( |
| canEditAnnotation({ |
| type: AnnotationType.site, |
| user: loggedInUser(role) |
| }) |
| ).toBe(false) |
| } |
| ) |
|
|
| it.each([ |
| [Role.viewer], |
| [Role.billing], |
| [Role.editor], |
| [Role.admin], |
| [Role.owner] |
| ])('allows editing a personal annotation when in role %p', (role) => { |
| expect( |
| canEditAnnotation({ |
| type: AnnotationType.personal, |
| user: loggedInUser(role) |
| }) |
| ).toBe(true) |
| }) |
|
|
| it.each([[AnnotationType.personal], [AnnotationType.site]])( |
| 'forbids the public role from editing %s annotations', |
| (type) => { |
| expect( |
| canEditAnnotation({ |
| type, |
| user: publicUser |
| }) |
| ).toBe(false) |
| } |
| ) |
| }) |
|
|
| describe(`${getAnnotationGranularity.name}`, () => { |
| it.each<[Interval, AnnotationGranularity]>([ |
| [Interval.minute, AnnotationGranularity.minute], |
| [Interval.hour, AnnotationGranularity.minute], |
| [Interval.day, AnnotationGranularity.date], |
| [Interval.week, AnnotationGranularity.date], |
| [Interval.month, AnnotationGranularity.date] |
| ])('maps interval %s to granularity %s', (interval, granularity) => { |
| expect(getAnnotationGranularity(interval)).toBe(granularity) |
| }) |
| }) |
|
|
| describe(`${getAnnotationTimeLabel.name}`, () => { |
| |
| const dateAnnotation = { |
| datetime: '2025-02-26', |
| granularity: AnnotationGranularity.date |
| } |
|
|
| it.each<[Interval, string]>([ |
| [Interval.minute, '2025-02-26'], |
| [Interval.hour, '2025-02-26'], |
| [Interval.day, '2025-02-26'], |
| [Interval.week, '2025-02-24'], |
| [Interval.month, '2025-02-01'] |
| ])( |
| `date-granularity annotation on ${dateAnnotation.datetime} bucketed to %s yields %s`, |
| (interval, expected) => { |
| expect(getAnnotationTimeLabel(dateAnnotation, interval)).toBe(expected) |
| } |
| ) |
|
|
| const minuteAnnotation = { |
| datetime: '2025-02-26T10:30:00', |
| granularity: AnnotationGranularity.minute |
| } |
| it.each<[Interval, string]>([ |
| [Interval.month, '2025-02-01'], |
| [Interval.week, '2025-02-24'], |
| [Interval.day, '2025-02-26'], |
| [Interval.hour, '2025-02-26 10:00:00'], |
| [Interval.minute, '2025-02-26 10:30:00'] |
| ])( |
| `minute granularity annotation with datetime ${minuteAnnotation.datetime} bucketed to %s yields %s`, |
| (interval, expected) => { |
| expect(getAnnotationTimeLabel(minuteAnnotation, interval)).toBe(expected) |
| } |
| ) |
| }) |
|
|
| describe(`${groupAnnotationsByTimeLabel.name}`, () => { |
| const dateGranularity = AnnotationGranularity.date |
| const annotations = [ |
| { id: 1, datetime: '2025-02-24 00:00:00', granularity: dateGranularity }, |
| { id: 2, datetime: '2025-02-26 00:00:00', granularity: dateGranularity }, |
| { id: 3, datetime: '2025-03-05 00:00:00', granularity: dateGranularity } |
| ] |
|
|
| it('groups annotations by day when the interval is day', () => { |
| const grouped = groupAnnotationsByTimeLabel(annotations, Interval.day) |
|
|
| expect(Object.keys(grouped).sort()).toEqual([ |
| '2025-02-24', |
| '2025-02-26', |
| '2025-03-05' |
| ]) |
| expect(grouped['2025-02-24']!.map((a) => a.id)).toEqual([1]) |
| expect(grouped['2025-02-26']!.map((a) => a.id)).toEqual([2]) |
| expect(grouped['2025-03-05']!.map((a) => a.id)).toEqual([3]) |
| }) |
|
|
| it('collapses annotations from the same week into one bucket', () => { |
| const grouped = groupAnnotationsByTimeLabel(annotations, Interval.week) |
|
|
| expect(Object.keys(grouped).sort()).toEqual(['2025-02-24', '2025-03-03']) |
| expect(grouped['2025-02-24']!.map((a) => a.id)).toEqual([1, 2]) |
| expect(grouped['2025-03-03']!.map((a) => a.id)).toEqual([3]) |
| }) |
|
|
| it('collapses annotations from the same month into one bucket', () => { |
| const grouped = groupAnnotationsByTimeLabel(annotations, Interval.month) |
|
|
| expect(Object.keys(grouped).sort()).toEqual(['2025-02-01', '2025-03-01']) |
| expect(grouped['2025-02-01']!.map((a) => a.id)).toEqual([1, 2]) |
| expect(grouped['2025-03-01']!.map((a) => a.id)).toEqual([3]) |
| }) |
|
|
| it('preserves insertion order within a bucket', () => { |
| const sameDay = [ |
| { id: 10, datetime: '2025-02-26 08:00:00', granularity: dateGranularity }, |
| { id: 11, datetime: '2025-02-26 09:00:00', granularity: dateGranularity }, |
| { id: 12, datetime: '2025-02-26 10:00:00', granularity: dateGranularity } |
| ] |
|
|
| const grouped = groupAnnotationsByTimeLabel(sameDay, Interval.day) |
|
|
| expect(grouped['2025-02-26']!.map((a) => a.id)).toEqual([10, 11, 12]) |
| }) |
|
|
| it('returns an empty object when there are no annotations', () => { |
| expect( |
| groupAnnotationsByTimeLabel( |
| [] as { datetime: string; granularity: AnnotationGranularity }[], |
| Interval.day |
| ) |
| ).toEqual({}) |
| }) |
| }) |
|
|