From fb8663583cde9a990b1da79d8a4b4fe33caa183e Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Wed, 14 Oct 2026 16:48:45 +0200 Subject: [PATCH 1/2] fix(frontend): refresh feedback context when opening the link --- frontend/src/__tests__/app.test.ts | 57 ++++++++++++++++++++++++ frontend/src/app.ts | 13 ++++-- frontend/tests-e2e/feedback-link.spec.ts | 52 +++++++++++++++++++++ 3 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 frontend/tests-e2e/feedback-link.spec.ts diff --git a/frontend/src/__tests__/app.test.ts b/frontend/src/__tests__/app.test.ts index 7a59b771..731f4eaf 100644 --- a/frontend/src/__tests__/app.test.ts +++ b/frontend/src/__tests__/app.test.ts @@ -163,6 +163,63 @@ describe('App Module', () => { }); describe('setupEventListeners', () => { + describe('feedback context', () => { + let originalURL: string; + let link: HTMLAnchorElement; + + beforeEach(() => { + originalURL = window.location.href; + jest.useFakeTimers().setSystemTime(new Date('2026-10-13T23:59:00Z')); + window.history.replaceState(null, '', '/purchases'); + document.body.innerHTML = 'Feedback'; + link = document.getElementById('feedback-link') as HTMLAnchorElement; + link.addEventListener('click', event => event.preventDefault()); + setupEventListeners(); + }); + + afterEach(() => { + jest.useRealTimers(); + jest.restoreAllMocks(); + window.history.replaceState(null, '', originalURL); + }); + + function expectContext(date: string): void { + const params = new URLSearchParams(link.href.split('?')[1]); + expect(link.href).toMatch(/^mailto:/); + expect(params.get('subject')).toBe('CUDly Feedback'); + expect(params.get('body')).toContain('## Steps to Reproduce (for bugs)'); + expect(params.get('body')).toContain(`- Browser: ${navigator.userAgent}`); + expect(params.get('body')).toContain(`- URL: ${window.location.href}`); + expect(params.get('body')).toContain(`- Date: ${date}`); + } + + test('provides initial feedback context', () => { + expectContext('2026-10-13T23:59:00.000Z'); + }); + + test.each(['/plans', '/home', '/opportunities?filter=a%26b%2Bc#details'])('uses the current %s URL and date when activated', path => { + link.focus(); + window.history.pushState(null, '', path); + jest.setSystemTime(new Date('2026-10-14T00:01:00Z')); + link.click(); + expectContext('2026-10-14T00:01:00.000Z'); + }); + + test.each(['focus', 'contextmenu'])('refreshes context on %s for link inspection', event => { + window.history.pushState(null, '', '/plans'); + jest.setSystemTime(new Date('2026-10-14T00:02:00Z')); + link.dispatchEvent(new Event(event, { cancelable: true })); + expectContext('2026-10-14T00:02:00.000Z'); + }); + + test('refreshes once after repeated setup', () => { + setupEventListeners(); + const writes = jest.spyOn(link, 'href', 'set'); + link.click(); + expect(writes).toHaveBeenCalledTimes(1); + }); + }); + test('sets up tab button click handlers', () => { document.body.innerHTML = ` diff --git a/frontend/src/app.ts b/frontend/src/app.ts index b69b8205..f90a482d 100644 --- a/frontend/src/app.ts +++ b/frontend/src/app.ts @@ -919,7 +919,8 @@ function setupFeedbackLink(): void { const feedbackEmail = 'contact@leanercloud.com'; const subject = 'CUDly Feedback'; - const body = `Hi CUDly Team, + const refreshFeedbackLink = (): void => { + const body = `Hi CUDly Team, I'd like to share some feedback about CUDly: @@ -967,6 +968,12 @@ Please attach any relevant screenshots to this email. Thank you for helping us improve CUDly! `; - const mailtoUrl = `mailto:${feedbackEmail}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; - feedbackLink.href = mailtoUrl; + const mailtoUrl = `mailto:${feedbackEmail}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; + feedbackLink.href = mailtoUrl; + }; + + refreshFeedbackLink(); + feedbackLink.onclick = refreshFeedbackLink; + feedbackLink.onfocus = refreshFeedbackLink; + feedbackLink.oncontextmenu = refreshFeedbackLink; } diff --git a/frontend/tests-e2e/feedback-link.spec.ts b/frontend/tests-e2e/feedback-link.spec.ts new file mode 100644 index 00000000..e43d0aa5 --- /dev/null +++ b/frontend/tests-e2e/feedback-link.spec.ts @@ -0,0 +1,52 @@ +import { test, expect, type Page } from '@playwright/test'; +import { mockApi, seedAuth } from './fixtures/recs'; + +async function expectFeedbackContext(page: Page, date: string): Promise { + const href = await page.locator('#feedback-link').getAttribute('href'); + const params = new URLSearchParams(href?.split('?')[1]); + expect(href).toMatch(/^mailto:/); + expect(params.get('subject')).toBe('CUDly Feedback'); + expect(params.get('body')).toContain(`- URL: ${page.url()}`); + expect(params.get('body')).toContain(`- Date: ${date}`); + expect(params.get('body')).toContain('## Steps to Reproduce (for bugs)'); +} + +test('feedback uses the current SPA route and time on mouse, keyboard, focus and context menu', async ({ page }) => { + const pageErrors: string[] = []; + page.on('pageerror', error => pageErrors.push(String(error))); + await seedAuth(page); + await mockApi(page); + await page.clock.setFixedTime(new Date('2026-10-13T23:59:00Z')); + await page.goto('/purchases'); + await expect(page.locator('#purchases-tab.active')).toBeVisible(); + const originalDocument = await page.evaluate(() => performance.timeOrigin); + const feedback = page.locator('#feedback-link'); + await expectFeedbackContext(page, '2026-10-13T23:59:00.000Z'); + await feedback.evaluate(link => { + link.addEventListener('click', event => event.preventDefault()); + link.addEventListener('contextmenu', event => event.preventDefault()); + }); + + for (const tab of ['plans', 'home', 'opportunities']) { + await page.locator(`#sidebar [data-tab="${tab}"]`).click(); + await expect(page.locator(`#${tab}-tab.active`)).toBeVisible(); + await feedback.focus(); + await page.clock.setFixedTime(new Date('2026-10-14T00:01:00Z')); + await feedback.press('Enter'); + await expectFeedbackContext(page, '2026-10-14T00:01:00.000Z'); + await page.clock.setFixedTime(new Date('2026-10-14T00:02:00Z')); + await feedback.click(); + await expectFeedbackContext(page, '2026-10-14T00:02:00.000Z'); + expect(await page.evaluate(() => performance.timeOrigin)).toBe(originalDocument); + } + + await page.evaluate(() => window.history.pushState(null, '', '/opportunities?filter=a%26b%2Bc#details')); + await page.clock.setFixedTime(new Date('2026-10-14T00:03:00Z')); + await feedback.click({ button: 'right' }); + await expectFeedbackContext(page, '2026-10-14T00:03:00.000Z'); + await page.locator('#sidebar [data-tab="home"]').focus(); + await page.clock.setFixedTime(new Date('2026-10-14T00:04:00Z')); + await feedback.focus(); + await expectFeedbackContext(page, '2026-10-14T00:04:00.000Z'); + expect(pageErrors).toEqual([]); +}); From 42bc34b8e59551b25d6ede5161182c80319bcb95 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Wed, 14 Oct 2026 16:55:18 +0200 Subject: [PATCH 2/2] test(frontend): assert feedback smoke console and network health --- frontend/tests-e2e/feedback-link.spec.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/frontend/tests-e2e/feedback-link.spec.ts b/frontend/tests-e2e/feedback-link.spec.ts index e43d0aa5..63bf5a72 100644 --- a/frontend/tests-e2e/feedback-link.spec.ts +++ b/frontend/tests-e2e/feedback-link.spec.ts @@ -13,9 +13,30 @@ async function expectFeedbackContext(page: Page, date: string): Promise { test('feedback uses the current SPA route and time on mouse, keyboard, focus and context menu', async ({ page }) => { const pageErrors: string[] = []; + const consoleErrors: string[] = []; + const failedRequests: string[] = []; page.on('pageerror', error => pageErrors.push(String(error))); + page.on('console', message => { + if (message.type() === 'error') consoleErrors.push(message.text()); + }); + page.on('response', response => { + if (response.status() >= 400) failedRequests.push(`${response.status()} ${new URL(response.url()).pathname}`); + }); + page.on('requestfailed', request => failedRequests.push(new URL(request.url()).pathname)); await seedAuth(page); await mockApi(page); + await page.route('**/api/history?*', route => route.fulfill({ json: [] })); + await page.route('**/api/history/analytics?*', route => route.fulfill({ json: { + start: '2026-10-01T00:00:00Z', end: '2026-10-14T00:00:00Z', interval: 'day', + summary: { total_period_savings: 0, total_upfront_spent: 0, purchase_count: 0, average_savings_per_period: 0, peak_savings: 0 }, + data_points: [], + } })); + await page.route('**/api/purchases/planned', route => route.fulfill({ json: { purchases: [] } })); + await page.route('**/api/dashboard/upcoming', route => route.fulfill({ json: [] })); + await page.route('**/api/dashboard/summary?*', route => route.fulfill({ json: { + potential_monthly_savings: 0, total_recommendations: 0, active_commitments: 0, + committed_monthly: 0, current_coverage: 0, target_coverage: 0, ytd_savings: 0, by_service: {}, + } })); await page.clock.setFixedTime(new Date('2026-10-13T23:59:00Z')); await page.goto('/purchases'); await expect(page.locator('#purchases-tab.active')).toBeVisible(); @@ -49,4 +70,6 @@ test('feedback uses the current SPA route and time on mouse, keyboard, focus and await feedback.focus(); await expectFeedbackContext(page, '2026-10-14T00:04:00.000Z'); expect(pageErrors).toEqual([]); + expect(failedRequests).toEqual([]); + expect(consoleErrors).toEqual([]); });