| import { test, expect } from '@playwright/test' |
| import { setupSite, populateStats, register } from '../fixtures' |
| import { randomID } from '../test-utils' |
|
|
| test.use({ locale: 'en-GB' }) |
|
|
| test('user can create annotations across granularities, edit, and delete them', async ({ |
| page, |
| request |
| }) => { |
| const user = { |
| email: `site-owner-for-annotations-${randomID()}@example.com`, |
| |
| name: 'J R.R. Smith', |
| password: 'VeryStrongVerySecret' |
| } |
| await register({ |
| page, |
| request, |
| user |
| }) |
| const { domain } = await setupSite({ page, request, user }) |
|
|
| await populateStats({ |
| request, |
| domain, |
| events: [ |
| { |
| user_id: 0, |
| name: 'pageview', |
| pathname: '/', |
| timestamp: '2026-04-01 00:00:00' |
| }, |
| { |
| user_id: 1, |
| name: 'pageview', |
| pathname: '/', |
| timestamp: '2026-06-29 05:00:00' |
| }, |
| { |
| user_id: 2, |
| name: 'pageview', |
| pathname: '/', |
| timestamp: '2026-06-29 10:00:00' |
| } |
| ] |
| }) |
|
|
| await page.goto(`/${domain}?period=7d&date=2026-07-06&comparison=off`, { |
| waitUntil: 'commit' |
| }) |
|
|
| const mondayIn7d = page.getByTestId('graph-dot-series-1-bucket-0') |
| const mondayMarker7d = page.getByTestId('annotation-marker-on-bucket-0') |
|
|
| const tooltip = page.getByTestId('graph-tooltip') |
|
|
| |
| const noteField = page.getByRole('textbox', { name: 'Note' }) |
| const saveButton = page.getByRole('button', { name: 'Save' }) |
|
|
| const siteNoteText = 'Company-wide: launch day' |
| const personalNoteText = 'Feature available for 50% of customers' |
| const updatedNoteText = 'Feature fully released (correction)' |
|
|
| const personalNoteAttribution = 'Personal note • 29 Jun 10:00' |
| const siteNoteAttribution = `${user.name} • 29 Jun` |
|
|
| |
| const expectTooltipAnnotations = async ( |
| expected: Array<{ note: string; attribution: string }> |
| ) => { |
| const rows = tooltip.locator('div.group') |
| await expect(rows).toHaveCount(expected.length) |
| for (const [i, { note, attribution }] of expected.entries()) { |
| const row = rows.nth(i) |
| await expect(row).toContainText(note) |
| await expect(row.getByText(attribution, { exact: true })).toBeVisible() |
| } |
| } |
|
|
| |
| const setGraphInterval = async (label: 'Hours' | 'Days' | 'Months') => { |
| await page.getByTestId('dashboard-options-menu').click() |
| await page.getByRole('button', { name: label, exact: true }).click() |
| await page.keyboard.press('Escape') |
| } |
|
|
| await test.step('in 7d view, create date-granularity site annotation on Monday', async () => { |
| await setGraphInterval('Days') |
|
|
| |
| await mondayIn7d.hover() |
| await expect(tooltip).toBeVisible() |
| await expectTooltipAnnotations([]) |
|
|
| await expect(tooltip).toContainText('Click to view day') |
| await expect(tooltip).toContainText('Right click for more actions') |
|
|
| |
| await mondayIn7d.click({ button: 'right' }) |
| await expect(tooltip).toBeVisible() |
|
|
| await expect(tooltip.locator('button')).toContainText([ |
| 'Add note', |
| 'View day' |
| ]) |
| await tooltip.getByRole('button', { name: 'Add note' }).click() |
|
|
| const createModalHeading = page.getByRole('heading', { |
| name: 'Add note for Mon, 29 Jun', |
| exact: true |
| }) |
| await expect(createModalHeading).toBeVisible() |
|
|
| await noteField.fill(siteNoteText) |
| await page.getByRole('radio', { name: /Site note/ }).check() |
| await saveButton.click() |
|
|
| await expect(createModalHeading).toBeHidden() |
| await expect(mondayMarker7d).toBeVisible() |
| }) |
|
|
| await test.step('right-click Monday and zoom in via "View day"', async () => { |
| await mondayIn7d.hover() |
| await expectTooltipAnnotations([ |
| { note: siteNoteText, attribution: siteNoteAttribution } |
| ]) |
|
|
| await mondayIn7d.click({ button: 'right' }) |
|
|
| await tooltip.getByRole('button', { name: 'View day' }).click() |
| await expect(page).toHaveURL(/period=day&date=2026-06-29/) |
|
|
| |
| await expect(page.getByTestId(/^annotation-marker-on-bucket-/)).toHaveCount( |
| 0 |
| ) |
| }) |
|
|
| await test.step('in day view, create minute-granularity personal annotation at 10:00', async () => { |
| await setGraphInterval('Hours') |
|
|
| const hour10InDay = page.getByTestId('graph-dot-series-1-bucket-10') |
| await hour10InDay.hover() |
| await hour10InDay.click({ button: 'right' }) |
| await expect(tooltip).toBeVisible() |
|
|
| await tooltip.getByRole('button', { name: 'Add note' }).click() |
|
|
| const createModalHeading = page.getByRole('heading', { |
| name: 'Add note for Mon, 29 Jun at 10:00', |
| exact: true |
| }) |
| await expect(createModalHeading).toBeVisible() |
| await expect(tooltip).toBeHidden() |
|
|
| await noteField.fill(personalNoteText) |
| await saveButton.click() |
|
|
| await expect(createModalHeading).toBeHidden() |
| await expect( |
| page.getByTestId('annotation-marker-on-bucket-10') |
| ).toBeVisible() |
| }) |
|
|
| await test.step('zoom back out to 7d, both annotations are visible on Monday', async () => { |
| await page.goBack({ waitUntil: 'commit' }) |
| await setGraphInterval('Days') |
|
|
| await expect(mondayMarker7d).toBeVisible() |
|
|
| const expectedOnMonday = [ |
| { note: siteNoteText, attribution: siteNoteAttribution }, |
| { note: personalNoteText, attribution: personalNoteAttribution } |
| ] |
|
|
| await mondayIn7d.hover() |
| await expect(tooltip).toBeVisible() |
| await expectTooltipAnnotations(expectedOnMonday) |
|
|
| await mondayIn7d.click({ button: 'right' }) |
| await expect(tooltip).toBeVisible() |
| await expectTooltipAnnotations(expectedOnMonday) |
| }) |
|
|
| await test.step('edit personal annotation', async () => { |
| const personalRow = tooltip.locator('div.group', { |
| hasText: personalNoteText |
| }) |
| await personalRow.hover() |
| await personalRow.getByRole('button', { name: 'Edit note' }).click() |
|
|
| const updateModalHeading = page.getByRole('heading', { |
| name: 'Update note for Mon, 29 Jun at 10:00', |
| exact: true |
| }) |
| await expect(updateModalHeading).toBeVisible() |
| await expect(tooltip).toBeHidden() |
| await expect(noteField).toHaveValue(personalNoteText) |
|
|
| await noteField.fill(updatedNoteText) |
| await saveButton.click() |
|
|
| await expect(updateModalHeading).toBeHidden() |
|
|
| await mondayIn7d.hover() |
| await mondayIn7d.click({ button: 'right' }) |
| await expectTooltipAnnotations([ |
| { note: siteNoteText, attribution: siteNoteAttribution }, |
| { note: updatedNoteText, attribution: personalNoteAttribution } |
| ]) |
| }) |
|
|
| await test.step('delete personal annotation', async () => { |
| const personalNote = tooltip.locator('div.group', { |
| hasText: updatedNoteText |
| }) |
| await personalNote.hover() |
| await personalNote.getByRole('button', { name: 'Edit note' }).click() |
|
|
| const updateModalHeading = page.getByRole('heading', { |
| name: 'Update note for Mon, 29 Jun at 10:00', |
| exact: true |
| }) |
| await expect(updateModalHeading).toBeVisible() |
|
|
| await page.getByRole('button', { name: 'Delete note' }).click() |
|
|
| const deleteModalHeading = page.getByRole('heading', { |
| name: `Delete personal note "${updatedNoteText}"?`, |
| exact: true |
| }) |
| await expect(deleteModalHeading).toBeVisible() |
| await expect(updateModalHeading).toBeHidden() |
|
|
| await page.getByRole('button', { name: 'Delete', exact: true }).click() |
|
|
| await expect(deleteModalHeading).toBeHidden() |
| await mondayIn7d.hover() |
| await expectTooltipAnnotations([ |
| { note: siteNoteText, attribution: siteNoteAttribution } |
| ]) |
| await expect(mondayMarker7d).toBeVisible() |
| }) |
|
|
| await test.step('date annotations are shown for All period', async () => { |
| await page.getByTestId('current-query-period').click() |
| await page |
| .getByTestId('query-period-picker') |
| .getByRole('link', { name: 'All time' }) |
| .click() |
| await expect(page).toHaveURL(/period=all/) |
|
|
| const marker = page.getByTestId(/^annotation-marker-on-bucket-/) |
| await expect(marker).toHaveCount(1) |
| await expect(marker).toBeVisible() |
|
|
| await setGraphInterval('Months') |
| await page.getByText('June 2026', { exact: true }).hover() |
| await expectTooltipAnnotations([ |
| { note: siteNoteText, attribution: siteNoteAttribution } |
| ]) |
| }) |
| }) |
|
|