From 5de4038429ee937a132b5f1cee04f1cfbd775e13 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Wed, 4 Mar 2026 12:39:13 -0500 Subject: [PATCH 01/69] stopping point to work on FECFILE-2923 --- .../e2e-extended/contacts/contacts.a11y.cy.ts | 2 +- .../contacts/contacts.delete.cy.ts | 6 +- .../e2e-extended/contacts/contacts.helpers.ts | 34 +- .../e2e-extended/contacts/contacts.list.cy.ts | 2 +- .../contacts/contacts.transactions.cy.ts | 96 +- .../f3x/aggregation-schedule-a.cy.ts | 126 ++ .../f3x/aggregation-schedule-b.cy.ts | 100 ++ .../f3x/aggregation-schedule-c-c1-c2.cy.ts | 213 ++++ .../f3x/aggregation-schedule-d.cy.ts | 132 +++ .../f3x/aggregation-schedule-e.cy.ts | 85 ++ .../f3x/aggregation-schedule-f.cy.ts | 55 + .../f3x/cross-committee-aggregation.cy.ts | 101 ++ .../f3x/f3x-aggregation.helpers.ts | 1022 +++++++++++++++++ .../f3x/itemization-cascades.cy.ts | 165 +++ .../e2e-extended/utils/shared.helpers.ts | 2 +- .../e2e-smoke/F3X/aggregate-calculation.cy.ts | 107 +- .../e2e-smoke/F3X/aggregate-schedule-f.cy.ts | 24 + front-end/cypress/e2e-smoke/F3X/debts.cy.ts | 94 +- .../cypress/e2e-smoke/F3X/loans-bank.cy.ts | 2 +- .../e2e-smoke/F3X/loans-committee.cy.ts | 2 +- .../cypress/e2e-smoke/F3X/receipts.cy.ts | 74 +- .../cypress/e2e-smoke/pages/contactLookup.ts | 32 +- .../e2e-smoke/pages/f3xCreateReportPage.ts | 2 +- .../cypress/e2e-smoke/pages/pageUtils.ts | 218 +++- .../e2e-smoke/pages/transactionDetailPage.ts | 56 +- .../cypress/e2e-smoke/requests/methods.ts | 115 +- front-end/package-lock.json | 198 +++- front-end/package.json | 4 +- 28 files changed, 2839 insertions(+), 230 deletions(-) create mode 100644 front-end/cypress/e2e-extended/f3x/aggregation-schedule-a.cy.ts create mode 100644 front-end/cypress/e2e-extended/f3x/aggregation-schedule-b.cy.ts create mode 100644 front-end/cypress/e2e-extended/f3x/aggregation-schedule-c-c1-c2.cy.ts create mode 100644 front-end/cypress/e2e-extended/f3x/aggregation-schedule-d.cy.ts create mode 100644 front-end/cypress/e2e-extended/f3x/aggregation-schedule-e.cy.ts create mode 100644 front-end/cypress/e2e-extended/f3x/aggregation-schedule-f.cy.ts create mode 100644 front-end/cypress/e2e-extended/f3x/cross-committee-aggregation.cy.ts create mode 100644 front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts create mode 100644 front-end/cypress/e2e-extended/f3x/itemization-cascades.cy.ts diff --git a/front-end/cypress/e2e-extended/contacts/contacts.a11y.cy.ts b/front-end/cypress/e2e-extended/contacts/contacts.a11y.cy.ts index 277c494e2c..042ad598ea 100644 --- a/front-end/cypress/e2e-extended/contacts/contacts.a11y.cy.ts +++ b/front-end/cypress/e2e-extended/contacts/contacts.a11y.cy.ts @@ -131,7 +131,7 @@ describe('Contacts - axe smoke (critical)', () => { .should('be.visible') .then(checkCritical); - cy.contains('button', /^Back$/).should('be.visible').click({ force: true }); + cy.contains('button', /^Back$/).should('be.visible').click(); cy.wait(`@${CONTACTS_LIST_ALIAS}`); }); diff --git a/front-end/cypress/e2e-extended/contacts/contacts.delete.cy.ts b/front-end/cypress/e2e-extended/contacts/contacts.delete.cy.ts index 087825fc88..ce869199cf 100644 --- a/front-end/cypress/e2e-extended/contacts/contacts.delete.cy.ts +++ b/front-end/cypress/e2e-extended/contacts/contacts.delete.cy.ts @@ -155,7 +155,7 @@ const openDeleteAction = (contactName: string, assertEnabled = true) => { ContactsDeleteHelpers.assertActionButtonEnabled($button, 'Delete'); } }) - .click({ force: true }); + .click(); }; const waitForLinkedContactStatus = ( @@ -331,7 +331,7 @@ describe('Contacts - delete guard', () => { cy.wait('@getDeletedContacts'); ContactsHelpers.assertSuccessToastMessage(); - cy.contains('button', /^Back$/).should('be.visible').click({ force: true }); + cy.contains('button', /^Back$/).should('be.visible').click(); cy.wait('@getContactsList'); cy.contains('tbody tr', UNLINKED_CONTACT).should('be.visible'); cy.contains('button,a', 'Restore deleted contacts').should('not.exist'); @@ -397,7 +397,7 @@ describe('Contacts - delete guard', () => { ContactsDeleteHelpers.assertActionButtonDisabled($deleteBtn, 'Delete'); } cy.wrap($deleteBtn).invoke('removeAttr', 'disabled'); - cy.wrap($deleteBtn).click({ force: true }); + cy.wrap($deleteBtn).click(); ContactsDeleteHelpers.confirmDeleteModalIfPresent(action); ContactsDeleteHelpers.assertNoConfirmDeleteModal(); }); diff --git a/front-end/cypress/e2e-extended/contacts/contacts.helpers.ts b/front-end/cypress/e2e-extended/contacts/contacts.helpers.ts index 10a9ba3cb3..ea41f93b17 100644 --- a/front-end/cypress/e2e-extended/contacts/contacts.helpers.ts +++ b/front-end/cypress/e2e-extended/contacts/contacts.helpers.ts @@ -83,7 +83,7 @@ export class ContactsHelpers { static setDropdownByLabel(labelRegex: RegExp, optionText: string, root = ContactsHelpers.DIALOG) { ContactsHelpers.fieldForLabel(labelRegex, root).within(() => { - cy.get('.p-select, .p-dropdown, .p-inputwrapper').first().click({ force: true }); + cy.get('.p-select, .p-dropdown, .p-inputwrapper').first().click(); }); cy.get('body') @@ -94,7 +94,7 @@ export class ContactsHelpers { 'i', ), ) - .click({ force: true }); + .click(); } static setDropdownByLabelIfPresent( @@ -113,13 +113,13 @@ export class ContactsHelpers { .contains('label', labelRegex) .closest('.field, .p-field, div.field') .within(() => { - cy.get('.p-select, .p-dropdown, .p-inputwrapper').first().click({ force: true }); + cy.get('.p-select, .p-dropdown, .p-inputwrapper').first().click(); }); cy.get('body') .find('.p-select-option, .p-dropdown-item') .contains(new RegExp(String.raw`^\\s*${ContactsHelpers.escapeRegExp(optionText)}\\s*$`)) - .click({ force: true }); + .click(); }); } @@ -176,14 +176,14 @@ export class ContactsHelpers { .contains(selectableOptionSel, rx, { timeout: 20000 }) .first() .scrollIntoView() - .click({ force: true }); + .click(); } else { list() .find(selectableOptionSel) .filter(':visible') .eq(index) .scrollIntoView() - .click({ force: true }); + .click(); } }); }); @@ -538,7 +538,7 @@ export class ContactsHelpers { .find('.p-autocomplete-option') .should('have.length.at.least', 1) .first() - .click({ force: true }); + .click(); cy.wait('@entityDetails'); cy.intercept('POST', '**/api/v1/contacts/').as('createContact'); @@ -767,7 +767,7 @@ export class ContactsDeleteHelpers { if (normalized === 'Close') { const $close = $dialog.find('[aria-label="Cancel"]').first(); if ($close.length) { - return cy.wrap($close).click({ force: true }); + return cy.wrap($close).click(); } throw new Error('Confirm delete dialog close icon not found.'); } @@ -777,12 +777,12 @@ export class ContactsDeleteHelpers { .filter((_, el) => (el.textContent || '').trim() === normalized) .first(); if ($button.length) { - return cy.wrap($button).click({ force: true }); + return cy.wrap($button).click(); } if (normalized === 'Cancel') { const $close = $dialog.find('[aria-label="Cancel"]').first(); if ($close.length) { - return cy.wrap($close).click({ force: true }); + return cy.wrap($close).click(); } } throw new Error(`Confirm delete dialog "${normalized}" button not found.`); @@ -891,7 +891,7 @@ export class ContactsDeleteHelpers { if (label === 'Close') { const $close = $dialog.find('[aria-label="Cancel"]').first(); if ($close.length) { - return cy.wrap($close).click({ force: true }); + return cy.wrap($close).click(); } throw new Error('Confirm delete dialog close icon not found.'); } @@ -901,12 +901,12 @@ export class ContactsDeleteHelpers { .filter((_, el) => (el.textContent || '').trim() === label) .first(); if ($button.length) { - return cy.wrap($button).click({ force: true }); + return cy.wrap($button).click(); } if (label === 'Cancel') { const $close = $dialog.find('[aria-label="Cancel"]').first(); if ($close.length) { - return cy.wrap($close).click({ force: true }); + return cy.wrap($close).click(); } } throw new Error(`Confirm delete dialog is visible but "${label}" action was not found.`); @@ -914,7 +914,7 @@ export class ContactsDeleteHelpers { } static openRestoreDeletedContactsModal() { - cy.contains('button,a', 'Restore deleted contacts').should('be.visible').click({ force: true }); + cy.contains('button,a', 'Restore deleted contacts').should('be.visible').click(); const dialog = ContactsDeleteHelpers.getRestoreDeletedContactsDialog(); dialog.contains('button', /Restore selected/i).should('be.visible'); return dialog; @@ -937,7 +937,7 @@ export class ContactsDeleteHelpers { .first(); if ($root.length) return cy.wrap($root).scrollIntoView().click(); const $trigger = $wrap.find('.p-select-dropdown, [aria-label="dropdown trigger"]').first(); - if ($trigger.length) return cy.wrap($trigger).scrollIntoView().click({ force: true }); + if ($trigger.length) return cy.wrap($trigger).scrollIntoView().click(); throw new Error('Results-per-page select not found in restore dialog.'); }); } @@ -982,7 +982,7 @@ export class ContactsDeleteHelpers { if ($row.length) { return cy.wrap($row) .within(() => { - cy.get('input[type="checkbox"], .p-checkbox-box').first().click({ force: true }); + cy.get('input[type="checkbox"], .p-checkbox-box').first().click(); }) .then(() => $row); } @@ -995,7 +995,7 @@ export class ContactsDeleteHelpers { throw new Error(`Could not find deleted contact "${contactName}" before last page.`); } - cy.wrap($next).click({ force: true }); + cy.wrap($next).click(); return waitForPageLoad().then(() => tryPage(page + 1)); }); }; diff --git a/front-end/cypress/e2e-extended/contacts/contacts.list.cy.ts b/front-end/cypress/e2e-extended/contacts/contacts.list.cy.ts index 783e95e41c..7cf699854c 100644 --- a/front-end/cypress/e2e-extended/contacts/contacts.list.cy.ts +++ b/front-end/cypress/e2e-extended/contacts/contacts.list.cy.ts @@ -155,7 +155,7 @@ describe('Contacts List (/contacts)', () => { cy.get('button[aria-label="Next Page"], .p-paginator-next') .first() .should('not.be.disabled') - .click({ force: true }); + .click(); cy.contains(pageTextRx(21, 21), { timeout: 15000 }).should('be.visible'); cy.get('tbody tr', { timeout: 15000 }).should('have.length', 1); diff --git a/front-end/cypress/e2e-extended/contacts/contacts.transactions.cy.ts b/front-end/cypress/e2e-extended/contacts/contacts.transactions.cy.ts index 87c0a612a0..3407a7e1d4 100644 --- a/front-end/cypress/e2e-extended/contacts/contacts.transactions.cy.ts +++ b/front-end/cypress/e2e-extended/contacts/contacts.transactions.cy.ts @@ -60,13 +60,13 @@ const openCreateContactModal = (which: CreateContactWhich = 'first') => { cy.wrap($target) .contains(/Create a new contact/i, { timeout: DEFAULT_TIMEOUT }) .scrollIntoView() - .click({ force: true }); + .click(); return; } cy.contains(/Create a new contact/i, { timeout: DEFAULT_TIMEOUT }) .scrollIntoView() - .click({ force: true }); + .click(); }); cy.contains('.p-dialog', /Create a new contact/i, { timeout: DEFAULT_TIMEOUT }) @@ -85,7 +85,7 @@ const fillCommonRequiredAddressInDialog = (dialogAlias: string, address: Address .then(($state) => { if ($state.length === 0) return; cy.wrap($state) - .click({ force: true }) + .click() .type('{downarrow}{enter}', { force: true }); }); }; @@ -94,74 +94,60 @@ const saveCreateContactDialog = () => { cy.get('@createContactDialog') .contains('button', /Save\s*&\s*continue/i) .should('be.enabled') - .click({ force: true }); + .click(); cy.get('body', { timeout: DEFAULT_TIMEOUT }).should(($body) => { expect(hasVisibleDialogMatching($body, /Create a new contact/i)).to.eq(false); }); }; -const findFirstAnchorMatching = ($body: JQuery, rx: RegExp): HTMLAnchorElement | null => { - const anchors = $body.find('a').toArray(); - for (const a of anchors) { - const txt = (a.textContent || '').trim(); - if (rx.test(txt)) return a as HTMLAnchorElement; - } - return null; -}; +const clickTransactionLinkOnSelectPage = (txnLinkRx: RegExp): Cypress.Chainable => { + return cy.get('p-accordion-panel', { timeout: DEFAULT_TIMEOUT }).then(($panels) => { + let targetPanel: HTMLElement | null = null; + + $panels.each((_, panel) => { + const hasMatch = Cypress.$(panel) + .find('.accordion-content-wrapper a') + .toArray() + .some((link) => txnLinkRx.test((link.textContent || '').trim())); + if (hasMatch) { + targetPanel = panel as HTMLElement; + return false; + } + return undefined; + }); -const findAndClickTransactionLink = (txnLinkRx: RegExp): Cypress.Chainable => { - return cy.get('body').then(($body) => { - const link = findFirstAnchorMatching($body, txnLinkRx); + if (!targetPanel) { + throw new Error(`Could not find transaction link matching: ${txnLinkRx}`); + } - if (!link) { - return cy.wrap(false, { log: false }); + const $targetPanel = Cypress.$(targetPanel); + const $header = $targetPanel.find('p-accordion-header, .p-accordionheader').first(); + if ($header.attr('aria-expanded') === 'false') { + cy.wrap($header).scrollIntoView().click(); } - return cy - .wrap(link) + cy.wrap($targetPanel) + .find('.accordion-content-wrapper a') + .filter((_, link) => txnLinkRx.test((link.textContent || '').trim()) && Cypress.dom.isVisible(link)) + .should('have.length', 1) + .first() .scrollIntoView() - .click({ force: true }) - .then(() => true); + .click(); }); }; -const expandAccordionsAndTryClickLink = (txnLinkRx: RegExp): Cypress.Chainable => { - return cy - .get('body') - .then(($body): void => { - const $headers = $body.find('p-accordion-header'); - - if ($headers.length === 0) return; - - cy.wrap($headers).each(($h) => { - if ($h.attr('aria-expanded') === 'false') { - cy.wrap($h).scrollIntoView().click({ force: true }); - } - }); - }) - .then(() => findAndClickTransactionLink(txnLinkRx)); -}; - -const clickTransactionLinkOnSelectPage = (txnLinkRx: RegExp): Cypress.Chainable => { - return findAndClickTransactionLink(txnLinkRx) - .then((found): Cypress.Chainable => { - if (found) return cy.wrap(true, { log: false }); - return expandAccordionsAndTryClickLink(txnLinkRx); - }) - .then((found) => { - if (!found) throw new Error(`Could not find transaction link matching: ${txnLinkRx}`); - return true; - }); -}; - const goToTransactionCreateFromList = (panelMenuRx: RegExp, txnLinkRx: RegExp) => { cy.get('p-panelmenu', { timeout: DEFAULT_TIMEOUT }).should('exist'); - cy.contains('a', panelMenuRx, { timeout: DEFAULT_TIMEOUT }) - .first() - .scrollIntoView() - .click({ force: true }); + cy.get('p-panelmenu').then(($panelMenu) => { + const matches = $panelMenu + .find('a.p-panelmenu-item-link') + .filter((_, link) => panelMenuRx.test((link.textContent || '').trim()) && Cypress.dom.isVisible(link)); + + expect(matches.length, `visible panel menu matches for ${panelMenuRx}`).to.eq(1); + cy.wrap(matches.first()).scrollIntoView().click(); + }); cy.url({ timeout: DEFAULT_TIMEOUT }).should('include', '/select/'); clickTransactionLinkOnSelectPage(txnLinkRx); @@ -436,7 +422,7 @@ describe('Contacts: Transactions integration', () => { // OPERATING EXPENDITURE ReportListPage.goToReportList(rid); - goToTransactionCreateFromList(/Add a disbursement/i, /Operating Expenditure/i); + goToTransactionCreateFromList(/Add a disbursement/i, /^Operating Expenditure$/i); cy.contains(/Operating Expenditure/i).should('exist'); openCreateContactModal('first'); diff --git a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-a.cy.ts b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-a.cy.ts new file mode 100644 index 0000000000..a72140e88d --- /dev/null +++ b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-a.cy.ts @@ -0,0 +1,126 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Initialize } from '../../e2e-smoke/pages/loginPage'; +import { currentYear } from '../../e2e-smoke/pages/pageUtils'; +import { F3X_Q1, F3X_Q2 } from '../../e2e-smoke/requests/library/reports'; +import { buildScheduleA } from '../../e2e-smoke/requests/library/transactions'; +import { DataSetup } from '../../e2e-smoke/F3X/setup'; +import { F3XAggregationHelpers } from './f3x-aggregation.helpers'; + +describe('Extended F3X Schedule A Aggregation', () => { + beforeEach(() => { + Initialize(); + }); + + it('A4 insert-between transaction recalculates downstream chain without double-counting', () => { + cy.wrap(DataSetup({ individual: true })).then((result: any) => { + F3XAggregationHelpers.seedScheduleAChain(result.report, result.individual, [ + { amount: 100, date: `${currentYear}-04-10` }, + { amount: 150, date: `${currentYear}-04-20` }, + ]).then(([firstId, lastId]) => { + F3XAggregationHelpers.createTransaction( + buildScheduleA('INDIVIDUAL_RECEIPT', 75, `${currentYear}-04-15`, result.individual, result.report), + ).then((middle) => { + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.assertReceiptAggregate(firstId, '$100.00'); + F3XAggregationHelpers.assertReceiptAggregate(middle.id, '$175.00'); + F3XAggregationHelpers.assertReceiptAggregate(lastId, '$325.00'); + + F3XAggregationHelpers.openReceipt(middle.id); + F3XAggregationHelpers.assertAggregateField('$175.00'); + }); + }); + }); + }); + + it('A6/A7 earlier-report create then report deletion reaggregates remaining report', () => { + const individualSeed = F3XAggregationHelpers.uniqueIndividualSeed(); + let contact: any; + let q1 = ''; + let q2 = ''; + let q1TxnId = ''; + let q2TxnId = ''; + + F3XAggregationHelpers.createContact(individualSeed) + .then((created) => { + contact = created; + }) + .then(() => F3XAggregationHelpers.createReport(F3X_Q1)) + .then((reportId) => { + q1 = reportId; + return F3XAggregationHelpers.createReport(F3X_Q2); + }) + .then((reportId) => { + q2 = reportId; + }) + .then(() => + F3XAggregationHelpers.createTransaction( + buildScheduleA('INDIVIDUAL_RECEIPT', 100, `${currentYear}-03-20`, contact, q1), + ), + ) + .then((created) => { + q1TxnId = created.id; + }) + .then(() => + F3XAggregationHelpers.createTransaction( + buildScheduleA('INDIVIDUAL_RECEIPT', 50, `${currentYear}-04-20`, contact, q2), + ), + ) + .then((created) => { + q2TxnId = created.id; + }) + .then(() => { + F3XAggregationHelpers.goToReport(q2); + F3XAggregationHelpers.assertReceiptAggregate(q2TxnId, '$150.00'); + F3XAggregationHelpers.openReceipt(q2TxnId); + F3XAggregationHelpers.assertAggregateField('$150.00'); + F3XAggregationHelpers.clickSave(); + + F3XAggregationHelpers.deleteReport(q1); + F3XAggregationHelpers.goToReport(q2); + F3XAggregationHelpers.assertReceiptAggregate(q2TxnId, '$50.00'); + F3XAggregationHelpers.openReceipt(q2TxnId); + F3XAggregationHelpers.assertAggregateField('$50.00'); + F3XAggregationHelpers.assertReceiptTransactionAbsent(q1TxnId); + }); + }); + + it('A8 itemize/unitemize and aggregate/unaggregate row actions persist after reload', () => { + cy.wrap(DataSetup({ individual: true })).then((result: any) => { + F3XAggregationHelpers.seedScheduleAChain(result.report, result.individual, [ + { amount: 100, date: `${currentYear}-04-12` }, + { amount: 75, date: `${currentYear}-04-20` }, + ]).then(([firstId, secondId]) => { + F3XAggregationHelpers.goToReport(result.report); + + F3XAggregationHelpers.assertReceiptRowStatus(firstId, 'Unitemized', true); + F3XAggregationHelpers.itemizeRowById(F3XAggregationHelpers.receiptsTableRoot, firstId); + F3XAggregationHelpers.assertReceiptRowStatus(firstId, 'Unitemized', false); + F3XAggregationHelpers.assertStatusPersistsAfterReload( + result.report, + F3XAggregationHelpers.receiptsTableRoot, + firstId, + 'Unitemized', + false, + ); + + F3XAggregationHelpers.unitemizeRowById(F3XAggregationHelpers.receiptsTableRoot, firstId); + F3XAggregationHelpers.assertReceiptRowStatus(firstId, 'Unitemized', true); + + F3XAggregationHelpers.assertReceiptAggregate(secondId, '$175.00'); + F3XAggregationHelpers.unaggregateRowById(F3XAggregationHelpers.receiptsTableRoot, secondId); + F3XAggregationHelpers.assertReceiptRowStatus(secondId, 'Unaggregated', true); + F3XAggregationHelpers.assertStatusPersistsAfterReload( + result.report, + F3XAggregationHelpers.receiptsTableRoot, + secondId, + 'Unaggregated', + true, + ); + + F3XAggregationHelpers.aggregateRowById(F3XAggregationHelpers.receiptsTableRoot, secondId); + F3XAggregationHelpers.assertReceiptRowStatus(secondId, 'Unaggregated', false); + F3XAggregationHelpers.assertReceiptAggregate(secondId, '$175.00'); + }); + }); + }); +}); diff --git a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-b.cy.ts b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-b.cy.ts new file mode 100644 index 0000000000..c1a4024779 --- /dev/null +++ b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-b.cy.ts @@ -0,0 +1,100 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Initialize } from '../../e2e-smoke/pages/loginPage'; +import { currentYear } from '../../e2e-smoke/pages/pageUtils'; +import { DataSetup } from '../../e2e-smoke/F3X/setup'; +import { buildContributionToCandidate } from '../../e2e-smoke/requests/library/transactions'; +import { F3XAggregationHelpers } from './f3x-aggregation.helpers'; + +describe('Extended F3X Schedule B Aggregation', () => { + beforeEach(() => { + Initialize(); + }); + + it('B1-B5 same-payee chain recalculates after payee switch, insert, and delete', () => { + cy.wrap(DataSetup({ committee: true, candidate: true })).then((result: any) => { + const newPayeeSeed = F3XAggregationHelpers.uniqueCommitteeSeed(); + F3XAggregationHelpers.createContact(newPayeeSeed).then((newPayee) => { + F3XAggregationHelpers.seedScheduleBChain(result.report, result.committee, result.candidate, [ + { amount: 100, date: `${currentYear}-04-10` }, + { amount: 60, date: `${currentYear}-04-15` }, + { amount: 40, date: `${currentYear}-04-20` }, + ]).then(([firstId, secondId, thirdId]) => { + F3XAggregationHelpers.getTransaction(firstId).its('aggregate').should('equal', '100.00'); + F3XAggregationHelpers.getTransaction(secondId).its('aggregate').should('equal', '160.00'); + F3XAggregationHelpers.getTransaction(thirdId).its('aggregate').should('equal', '200.00'); + + F3XAggregationHelpers.deleteTransactionById(secondId).then(() => { + F3XAggregationHelpers.createTransaction( + buildContributionToCandidate(60, `${currentYear}-04-15`, [newPayee, result.candidate], result.report, { + election_code: `P${currentYear}`, + support_oppose_code: 'S', + date_signed: `${currentYear}-04-10`, + }), + ).then((recreatedSecondId) => { + F3XAggregationHelpers.getTransaction(recreatedSecondId.id).its('aggregate').should('equal', '60.00'); + F3XAggregationHelpers.getTransaction(thirdId).its('aggregate').should('equal', '140.00'); + + F3XAggregationHelpers.seedScheduleBChain(result.report, result.committee, result.candidate, [ + { amount: 20, date: `${currentYear}-04-12` }, + ]).then(([insertedId]) => { + F3XAggregationHelpers.getTransaction(insertedId).its('aggregate').should('equal', '120.00'); + F3XAggregationHelpers.getTransaction(thirdId).its('aggregate').should('equal', '160.00'); + + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.deleteRowById(F3XAggregationHelpers.disbursementsTableRoot, insertedId); + F3XAggregationHelpers.getTransaction(thirdId).its('aggregate').should('equal', '140.00'); + }); + }); + }); + }); + }); + }); + }); + + it('B6 itemize and unitemize row actions persist for schedule B transaction', () => { + cy.wrap(DataSetup({ committee: true, candidate: true })).then((result: any) => { + F3XAggregationHelpers.seedScheduleBChain(result.report, result.committee, result.candidate, [ + { amount: 120, date: `${currentYear}-04-10` }, + ]).then(([txnId]) => { + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.rowById(F3XAggregationHelpers.disbursementsTableRoot, txnId) + .find('td') + .eq(1) + .invoke('text') + .then((statusText) => { + const startsUnitemized = statusText.includes('Unitemized'); + const firstAction = startsUnitemized ? 'itemize' : 'unitemize'; + const secondAction = startsUnitemized ? 'unitemize' : 'itemize'; + + if (firstAction === 'itemize') { + F3XAggregationHelpers.itemizeRowById(F3XAggregationHelpers.disbursementsTableRoot, txnId); + } else { + F3XAggregationHelpers.unitemizeRowById(F3XAggregationHelpers.disbursementsTableRoot, txnId); + } + F3XAggregationHelpers.assertDisbursementRowStatus(txnId, 'Unitemized', !startsUnitemized); + F3XAggregationHelpers.assertStatusPersistsAfterReload( + result.report, + F3XAggregationHelpers.disbursementsTableRoot, + txnId, + 'Unitemized', + !startsUnitemized, + ); + + if (secondAction === 'itemize') { + F3XAggregationHelpers.itemizeRowById(F3XAggregationHelpers.disbursementsTableRoot, txnId); + } else { + F3XAggregationHelpers.unitemizeRowById(F3XAggregationHelpers.disbursementsTableRoot, txnId); + } + F3XAggregationHelpers.assertDisbursementRowStatus(txnId, 'Unitemized', startsUnitemized); + F3XAggregationHelpers.assertStatusPersistsAfterReload( + result.report, + F3XAggregationHelpers.disbursementsTableRoot, + txnId, + 'Unitemized', + startsUnitemized, + ); + }); + }); + }); + }); +}); diff --git a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-c-c1-c2.cy.ts b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-c-c1-c2.cy.ts new file mode 100644 index 0000000000..512a842cb4 --- /dev/null +++ b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-c-c1-c2.cy.ts @@ -0,0 +1,213 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Initialize } from '../../e2e-smoke/pages/loginPage'; +import { currentYear, PageUtils } from '../../e2e-smoke/pages/pageUtils'; +import { TransactionDetailPage } from '../../e2e-smoke/pages/transactionDetailPage'; +import { ContactLookup } from '../../e2e-smoke/pages/contactLookup'; +import { ReportListPage } from '../../e2e-smoke/pages/reportListPage'; +import { DataSetup } from '../../e2e-smoke/F3X/setup'; +import { StartTransaction } from '../../e2e-smoke/F3X/utils/start-transaction/start-transaction'; +import { defaultLoanFormData } from '../../e2e-smoke/models/TransactionFormModel'; +import { F3XAggregationHelpers } from './f3x-aggregation.helpers'; + +function parseCurrency(value: string): number { + return Number((value || '').replace(/[^0-9.-]/g, '')); +} + +function readLoanBalanceValueInList(loanId: string): Cypress.Chainable { + return F3XAggregationHelpers.rowById(F3XAggregationHelpers.loansAndDebtsTableRoot, loanId) + .find('td') + .eq(5) + .invoke('text') + .then((text) => { + return parseCurrency(text); + }); +} + +function assertLoanBalanceValueInList(loanId: string, expected: number): void { + readLoanBalanceValueInList(loanId).then((actual) => { + expect(actual).to.equal(expected); + }); +} + +function executeLoanRepaymentLifecycleWithIntegrity( + reportId: string, + loanId: string, + assertBalanceRestore: boolean, +): void { + let initialBalance = 0; + F3XAggregationHelpers.goToReport(reportId); + readLoanBalanceValueInList(loanId).then((balance) => { + initialBalance = balance; + expect(initialBalance).to.be.greaterThan(0); + }); + + F3XAggregationHelpers.listLoanRepaymentIdsForLoan(reportId, loanId).then((repaymentIdsBeforeCreate) => { + F3XAggregationHelpers.clickRowActionById( + F3XAggregationHelpers.loansAndDebtsTableRoot, + loanId, + 'Make loan repayment', + ); + PageUtils.urlCheck('LOAN_REPAYMENT_MADE?loan='); + + cy.intercept( + { + method: 'POST', + pathname: '/api/v1/transactions/', + }, + (req) => { + if (req.body?.transaction_type_identifier === 'LOAN_REPAYMENT_MADE') { + req.alias = 'CreateLoanRepayment'; + } + }, + ); + + TransactionDetailPage.enterDate('[data-cy="expenditure_date"]', new Date(currentYear, 4 - 1, 25)); + cy.get('#amount').safeType(1000); + PageUtils.clickButton('Save'); + + cy.wait('@CreateLoanRepayment'); + F3XAggregationHelpers.goToReport(reportId); + assertLoanBalanceValueInList(loanId, initialBalance - 1000); + + F3XAggregationHelpers.listLoanRepaymentIdsForLoan(reportId, loanId).then((repaymentIdsAfterCreate) => { + const createdRepaymentIds = repaymentIdsAfterCreate.filter((id) => !repaymentIdsBeforeCreate.includes(id)); + expect(createdRepaymentIds.length, 'C1-C5 created repayment ids after save').to.be.greaterThan(0); + cy.log(`C1-C5 guard: using API delete for repayment ids ${createdRepaymentIds.join(', ')}`); + + F3XAggregationHelpers.deleteTransactionsAndVerify404(createdRepaymentIds) + .then(() => { + return F3XAggregationHelpers.listLoanRepaymentIdsForLoan(reportId, loanId).then((repaymentIdsAfterDelete) => { + const sortedBefore = [...repaymentIdsBeforeCreate].sort(); + const sortedAfterDelete = [...repaymentIdsAfterDelete].sort(); + expect( + sortedAfterDelete, + 'C1-C5 repayment ids after delete should return to pre-create baseline', + ).to.deep.equal(sortedBefore); + }); + }) + .then(() => { + if (assertBalanceRestore) { + return F3XAggregationHelpers.getTransaction(loanId) + .then((loanAfterDelete) => { + expect( + loanAfterDelete, + 'C1-C5 strict diagnostic: parent loan payload should include loan_payment_to_date', + ).to.have.property('loan_payment_to_date'); + expect( + loanAfterDelete, + 'C1-C5 strict diagnostic: parent loan payload should include loan_balance', + ).to.have.property('loan_balance'); + + const loanPaymentToDate = parseCurrency(String(loanAfterDelete.loan_payment_to_date ?? '')); + const loanBalance = parseCurrency(String(loanAfterDelete.loan_balance ?? '')); + expect(Number.isNaN(loanPaymentToDate), 'C1-C5 strict diagnostic loan_payment_to_date parse').to.equal(false); + expect(Number.isNaN(loanBalance), 'C1-C5 strict diagnostic loan_balance parse').to.equal(false); + cy.log( + `C1-C5 strict diagnostic after repayment delete: loan_payment_to_date=${loanPaymentToDate}, loan_balance=${loanBalance}`, + ); + }) + .then(() => { + cy.log(`C1-C5 strict starting restore poll: loanId=${loanId}, expected_initial_balance=${initialBalance}`); + return F3XAggregationHelpers.waitForLoanBalanceRestoreByApi(loanId, initialBalance, { + maxAttempts: 8, + intervalMs: 500, + }); + }) + .then(() => { + F3XAggregationHelpers.goToReport(reportId); + assertLoanBalanceValueInList(loanId, initialBalance); + }); + } else { + F3XAggregationHelpers.goToReport(reportId); + F3XAggregationHelpers.assertRowExists(F3XAggregationHelpers.loansAndDebtsTableRoot, loanId); + } + }); + }); + }); +} + +describe('Extended F3X Schedule C/C1/C2 Aggregation', () => { + beforeEach(() => { + Initialize(); + }); + + it('C1-C5 loan repayment creation decreases balance and deletion preserves repayment integrity', () => { + cy.wrap(DataSetup({ organization: true })).then((result: any) => { + F3XAggregationHelpers.seedLoanFromBank(result.report, result.organization).then((loanId) => { + executeLoanRepaymentLifecycleWithIntegrity(result.report, loanId, false); + }); + }); + }); + + it('C1-C5 strict deleting loan repayment restores parent loan balance to initial', () => { + // Contract: keep this as a hard-fail signal. + // If this fails after repayment deletion integrity checks pass, it indicates a backend + // loan delete-reaggregation regression (not Cypress flake). Do not soften this assertion. + cy.wrap(DataSetup({ organization: true })).then((result: any) => { + F3XAggregationHelpers.seedLoanFromBank(result.report, result.organization).then((loanId) => { + executeLoanRepaymentLifecycleWithIntegrity(result.report, loanId, true); + }); + }); + }); + + it('C2 guarantor add/delete flow keeps parent loan available and recalculates child membership', () => { + cy.wrap(DataSetup({ committee: true, individual: true })).then((result: any) => { + ReportListPage.goToReportList(result.report); + StartTransaction.Loans().ByCommittee(); + ContactLookup.getCommittee(result.committee); + + TransactionDetailPage.enterLoanFormData( + { + ...defaultLoanFormData, + purpose_description: 'Loan by committee', + memo_text: 'Loan note', + first_name: 'Robin', + last_name: 'Taylor', + authorized_first_name: 'Alex', + authorized_last_name: 'Morgan', + authorized_title: 'Manager', + date_received: undefined, + amount: 5000, + }, + false, + '', + '#loan-info-amount', + ); + + TransactionDetailPage.addGuarantor(result.individual.last_name, 1000, result.report); + cy.contains('Loan By Committee').first().click(); + PageUtils.urlCheck('/list'); + + cy.get(`${F3XAggregationHelpers.loansAndDebtsTableRoot} tbody tr`) + .first() + .find('td') + .eq(1) + .find('a') + .first() + .invoke('attr', 'href') + .then((href) => { + const loanId = (href ?? '').split('/').filter(Boolean).pop() ?? ''; + if (!loanId) { + throw new Error('Loan id from list row href is missing'); + } + + F3XAggregationHelpers.openLoanOrDebt(loanId); + cy.contains('Guarantors').should('exist'); + + const guarantorDisplayName = [result.individual.last_name, result.individual.first_name] + .filter(Boolean) + .join(', '); + + cy.intercept('DELETE', '**/api/v1/transactions/**').as('DeleteGuarantor'); + F3XAggregationHelpers.clickRowActionByCellText( + 'app-transaction-guarantors p-table', + guarantorDisplayName, + 'Delete', + ); + F3XAggregationHelpers.confirmDialog(); + cy.wait('@DeleteGuarantor'); + cy.contains(guarantorDisplayName).should('not.exist'); + }); + }); + }); +}); diff --git a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-d.cy.ts b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-d.cy.ts new file mode 100644 index 0000000000..aa844b755e --- /dev/null +++ b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-d.cy.ts @@ -0,0 +1,132 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Initialize } from '../../e2e-smoke/pages/loginPage'; +import { currentYear, PageUtils } from '../../e2e-smoke/pages/pageUtils'; +import { TransactionDetailPage } from '../../e2e-smoke/pages/transactionDetailPage'; +import { ContactLookup } from '../../e2e-smoke/pages/contactLookup'; +import { ReportListPage } from '../../e2e-smoke/pages/reportListPage'; +import { DataSetup } from '../../e2e-smoke/F3X/setup'; +import { StartTransaction } from '../../e2e-smoke/F3X/utils/start-transaction/start-transaction'; +import { defaultDebtFormData, defaultScheduleFormData } from '../../e2e-smoke/models/TransactionFormModel'; +import { F3XAggregationHelpers } from './f3x-aggregation.helpers'; + +describe('Extended F3X Schedule D Aggregation', () => { + beforeEach(() => { + Initialize(); + }); + + it('D1-D5 deleting a debt repayment recomputes debt balance_at_close for surviving debt', () => { + cy.wrap(DataSetup({ committee: true, individual: true })).then((result: any) => { + ReportListPage.goToReportList(result.report); + StartTransaction.Debts().ToCommittee(); + ContactLookup.getCommittee(result.committee); + + cy.intercept({ + method: 'POST', + pathname: '/api/v1/transactions/', + }).as('CreateDebtToCommittee'); + + TransactionDetailPage.enterLoanFormData( + { + ...defaultDebtFormData, + amount: 6000, + }, + false, + '', + '#amount', + ); + PageUtils.clickButton('Save'); + cy.wait('@CreateDebtToCommittee').then((interception) => { + const debtId = F3XAggregationHelpers.transactionIdFromPayload( + interception.response?.body, + 'D1-D5 create debt to committee', + ); + + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.clickRowActionById( + F3XAggregationHelpers.loansAndDebtsTableRoot, + debtId, + 'Report debt repayment', + ); + PageUtils.urlCheck('select/receipt?debt='); + PageUtils.clickAccordion('CONTRIBUTIONS FROM INDIVIDUALS/PERSONS'); + PageUtils.clickLink('Individual Receipt'); + ContactLookup.getContact(result.individual.last_name); + + cy.intercept({ + method: 'POST', + pathname: '/api/v1/transactions/', + }).as('CreateDebtRepaymentReceipt'); + + TransactionDetailPage.enterScheduleFormData( + { + ...defaultScheduleFormData, + electionType: undefined, + electionYear: undefined, + date_received: new Date(currentYear, 4 - 1, 20), + amount: 1000, + }, + false, + '', + true, + 'contribution_date', + ); + PageUtils.clickButton('Save'); + cy.wait('@CreateDebtRepaymentReceipt').then((repaymentInterception) => { + const repaymentId = F3XAggregationHelpers.transactionIdFromPayload( + repaymentInterception.response?.body, + 'D1-D5 create debt repayment receipt', + ); + + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.assertDebtBalanceFieldOnOpen(debtId, '$5,000.00'); + F3XAggregationHelpers.clickSave(); + + F3XAggregationHelpers.deleteTransactionById(repaymentId); + F3XAggregationHelpers.goToReport(result.report); + + F3XAggregationHelpers.assertDebtBalanceFieldOnOpen(debtId, '$6,000.00'); + }); + }); + }); + }); + + it('D3 editing debt amount updates running debt balance_at_close', () => { + cy.wrap(DataSetup({ committee: true })).then((result: any) => { + ReportListPage.goToReportList(result.report); + StartTransaction.Debts().ByCommittee(); + ContactLookup.getCommittee(result.committee, [], [], '', 'Committee'); + + cy.intercept({ + method: 'POST', + pathname: '/api/v1/transactions/', + }).as('CreateDebtByCommittee'); + + TransactionDetailPage.enterLoanFormData( + { + ...defaultDebtFormData, + amount: 3000, + }, + false, + '', + '#amount', + ); + cy.get('#balance_at_close').should('have.value', '$3,000.00'); + PageUtils.clickButton('Save'); + + cy.wait('@CreateDebtByCommittee').then((interception) => { + const debtId = F3XAggregationHelpers.transactionIdFromPayload( + interception.response?.body, + 'D3 create debt by committee', + ); + + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.openLoanOrDebt(debtId); + cy.get('#amount').clear().safeType('5500').blur(); + cy.get('#balance_at_close').should('have.value', '$5,500.00'); + F3XAggregationHelpers.clickSave(); + + F3XAggregationHelpers.assertLoansBalance(debtId, '$5,500.00'); + }); + }); + }); +}); diff --git a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-e.cy.ts b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-e.cy.ts new file mode 100644 index 0000000000..9704fbe238 --- /dev/null +++ b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-e.cy.ts @@ -0,0 +1,85 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Initialize } from '../../e2e-smoke/pages/loginPage'; +import { currentYear, PageUtils } from '../../e2e-smoke/pages/pageUtils'; +import { DataSetup } from '../../e2e-smoke/F3X/setup'; +import { ContactLookup } from '../../e2e-smoke/pages/contactLookup'; +import { F3XAggregationHelpers } from './f3x-aggregation.helpers'; + +describe('Extended F3X Schedule E Aggregation', () => { + beforeEach(() => { + Initialize(); + }); + + it('E1-E4 candidate context switch reaggregates old and new partitions', () => { + cy.wrap(DataSetup({ individual: true, candidate: true, candidateSenate: true })).then((result: any) => { + F3XAggregationHelpers.createIndependentExpenditureViaUI({ + reportId: result.report, + payeeContactName: result.individual.last_name, + candidate: result.candidate, + amount: 100, + disbursementDate: new Date(currentYear, 4 - 1, 5), + }).then((firstId) => { + F3XAggregationHelpers.createIndependentExpenditureViaUI({ + reportId: result.report, + payeeContactName: result.individual.last_name, + candidate: result.candidate, + amount: 50, + disbursementDate: new Date(currentYear, 4 - 1, 20), + }).then((secondId) => { + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.assertScheduleEAggregateFieldOnOpen(secondId, '$150.00'); + F3XAggregationHelpers.clickSave(); + + F3XAggregationHelpers.openDisbursement(secondId); + ContactLookup.getCandidate(result.candidateSenate, [], [], '#contact_2_lookup'); + PageUtils.blurActiveField(); + F3XAggregationHelpers.assertCalendarYtdField('$50.00'); + F3XAggregationHelpers.clickSave(); + + F3XAggregationHelpers.assertScheduleEAggregateFieldOnOpen(firstId, '$100.00'); + F3XAggregationHelpers.clickSave(); + F3XAggregationHelpers.assertScheduleEAggregateFieldOnOpen(secondId, '$50.00'); + }); + }); + }); + }); + + it('E3-E5 election code switch and delete retain correct calendar_ytd partition totals', () => { + cy.wrap(DataSetup({ individual: true, candidate: true })).then((result: any) => { + F3XAggregationHelpers.createIndependentExpenditureViaUI({ + reportId: result.report, + payeeContactName: result.individual.last_name, + candidate: result.candidate, + amount: 80, + disbursementDate: new Date(currentYear, 4 - 1, 8), + electionCode: `P${currentYear}`, + }).then((firstId) => { + F3XAggregationHelpers.createIndependentExpenditureViaUI({ + reportId: result.report, + payeeContactName: result.individual.last_name, + candidate: result.candidate, + amount: 40, + disbursementDate: new Date(currentYear, 4 - 1, 22), + electionCode: `P${currentYear}`, + }).then((secondId) => { + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.assertScheduleEAggregateFieldOnOpen(secondId, '$120.00'); + F3XAggregationHelpers.clickSave(); + + F3XAggregationHelpers.openDisbursement(secondId); + PageUtils.selectDropdownSetValue('[inputid="electionType"]', 'G'); + F3XAggregationHelpers.clearAndType('#electionYear', `${currentYear}`); + PageUtils.blurActiveField(); + F3XAggregationHelpers.assertCalendarYtdField('$40.00'); + F3XAggregationHelpers.clickSave(); + + F3XAggregationHelpers.assertScheduleEAggregateFieldOnOpen(firstId, '$80.00'); + F3XAggregationHelpers.clickSave(); + + F3XAggregationHelpers.deleteRowById(F3XAggregationHelpers.disbursementsTableRoot, firstId); + F3XAggregationHelpers.assertScheduleEAggregateFieldOnOpen(secondId, '$40.00'); + }); + }); + }); + }); +}); diff --git a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-f.cy.ts b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-f.cy.ts new file mode 100644 index 0000000000..f59482ab4b --- /dev/null +++ b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-f.cy.ts @@ -0,0 +1,55 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Initialize, setCommitteeToPTY } from '../../e2e-smoke/pages/loginPage'; +import { currentYear, PageUtils } from '../../e2e-smoke/pages/pageUtils'; +import { DataSetup } from '../../e2e-smoke/F3X/setup'; +import { F3XAggregationHelpers } from './f3x-aggregation.helpers'; + +describe('Extended F3X Schedule F Aggregation', () => { + beforeEach(() => { + Initialize(); + setCommitteeToPTY(); + }); + + it('F1-F5 delete middle transaction reaggregates downstream aggregate_general_elec_expended', () => { + cy.wrap(DataSetup({ individual: true, candidate: true, committee: true })).then((result: any) => { + F3XAggregationHelpers.seedScheduleFChain(result.report, result.individual, result.candidate, result.committee, [ + { amount: 100, date: `${currentYear}-04-10` }, + { amount: 50, date: `${currentYear}-04-15` }, + { amount: 25, date: `${currentYear}-04-20` }, + ]).then(([, middleId, finalId]) => { + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.assertScheduleFAggregateFieldOnOpen(finalId, '$175.00'); + F3XAggregationHelpers.clickSave(); + + F3XAggregationHelpers.deleteRowById(F3XAggregationHelpers.disbursementsTableRoot, middleId); + F3XAggregationHelpers.assertScheduleFAggregateFieldOnOpen(finalId, '$125.00'); + }); + }); + }); + + it('F3 year partition switch recalculates old and new schedule F chains', () => { + cy.wrap(DataSetup({ individual: true, candidate: true, committee: true })).then((result: any) => { + F3XAggregationHelpers.seedScheduleFChain(result.report, result.individual, result.candidate, result.committee, [ + { amount: 100, date: `${currentYear}-04-10`, extra: { general_election_year: '2024' } }, + { amount: 60, date: `${currentYear}-04-20`, extra: { general_election_year: '2024' } }, + { amount: 40, date: `${currentYear}-04-25`, extra: { general_election_year: '2026' } }, + ]).then(([firstId, secondId, thirdId]) => { + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.assertScheduleFAggregateFieldOnOpen(secondId, '$160.00'); + F3XAggregationHelpers.clickSave(); + F3XAggregationHelpers.assertScheduleFAggregateFieldOnOpen(thirdId, '$40.00'); + F3XAggregationHelpers.clickSave(); + + F3XAggregationHelpers.openDisbursement(secondId); + F3XAggregationHelpers.clearAndType('#general_election_year', '2026'); + PageUtils.blurActiveField(); + F3XAggregationHelpers.assertScheduleFAggregateField('$60.00'); + F3XAggregationHelpers.clickSave(); + + F3XAggregationHelpers.assertScheduleFAggregateFieldOnOpen(firstId, '$100.00'); + F3XAggregationHelpers.clickSave(); + F3XAggregationHelpers.assertScheduleFAggregateFieldOnOpen(thirdId, '$100.00'); + }); + }); + }); +}); diff --git a/front-end/cypress/e2e-extended/f3x/cross-committee-aggregation.cy.ts b/front-end/cypress/e2e-extended/f3x/cross-committee-aggregation.cy.ts new file mode 100644 index 0000000000..4748c519a8 --- /dev/null +++ b/front-end/cypress/e2e-extended/f3x/cross-committee-aggregation.cy.ts @@ -0,0 +1,101 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Initialize } from '../../e2e-smoke/pages/loginPage'; +import { ContactListPage } from '../../e2e-smoke/pages/contactListPage'; +import { ReportListPage } from '../../e2e-smoke/pages/reportListPage'; +import { currentYear } from '../../e2e-smoke/pages/pageUtils'; +import { F3X_Q1, F3X_Q2 } from '../../e2e-smoke/requests/library/reports'; +import { F3XAggregationHelpers } from './f3x-aggregation.helpers'; + +describe('Extended F3X Cross-Committee Aggregation Isolation', () => { + beforeEach(() => { + Initialize(); + }); + + it('schedule E aggregation remains committee-scoped across committee switches', () => { + let primaryReportId = ''; + let primaryTxnId = ''; + let secondaryReportId = ''; + let secondaryTxnId = ''; + + const primaryPayeeSeed = F3XAggregationHelpers.uniqueIndividualSeed(); + const primaryCandidateSeed = F3XAggregationHelpers.uniqueHouseCandidateSeed(); + const secondaryPayeeSeed = F3XAggregationHelpers.uniqueIndividualSeed(); + const secondaryCandidateSeed = F3XAggregationHelpers.uniqueHouseCandidateSeed(); + + let primaryPayee: any; + let primaryCandidate: any; + let secondaryPayee: any; + let secondaryCandidate: any; + + F3XAggregationHelpers.switchCommittee(F3XAggregationHelpers.committeePrimaryId); + ReportListPage.deleteAllReports(); + ContactListPage.deleteAllContacts(); + + F3XAggregationHelpers.createContact(primaryPayeeSeed) + .then((created) => { + primaryPayee = created; + }) + .then(() => F3XAggregationHelpers.createContact(primaryCandidateSeed)) + .then((created) => { + primaryCandidate = created; + }) + .then(() => F3XAggregationHelpers.createReport(F3X_Q1)) + .then((reportId) => { + primaryReportId = reportId; + }) + .then(() => + F3XAggregationHelpers.createIndependentExpenditureViaUI({ + reportId: primaryReportId, + payeeContactName: primaryPayee.last_name, + candidate: primaryCandidate, + amount: 100, + disbursementDate: new Date(currentYear, 2 - 1, 10), + }), + ) + .then((txnId) => { + primaryTxnId = txnId; + }) + .then(() => { + F3XAggregationHelpers.goToReport(primaryReportId); + F3XAggregationHelpers.assertScheduleEAggregateFieldOnOpen(primaryTxnId, '$100.00'); + F3XAggregationHelpers.clickSave(); + }) + .then(() => { + F3XAggregationHelpers.switchCommittee(F3XAggregationHelpers.committeeSecondaryId); + }) + .then(() => F3XAggregationHelpers.createContact(secondaryPayeeSeed)) + .then((created) => { + secondaryPayee = created; + }) + .then(() => F3XAggregationHelpers.createContact(secondaryCandidateSeed)) + .then((created) => { + secondaryCandidate = created; + }) + .then(() => F3XAggregationHelpers.createReport(F3X_Q2)) + .then((reportId) => { + secondaryReportId = reportId; + }) + .then(() => + F3XAggregationHelpers.createIndependentExpenditureViaUI({ + reportId: secondaryReportId, + payeeContactName: secondaryPayee.last_name, + candidate: secondaryCandidate, + amount: 50, + disbursementDate: new Date(currentYear, 4 - 1, 12), + }), + ) + .then((txnId) => { + secondaryTxnId = txnId; + }) + .then(() => { + F3XAggregationHelpers.goToReport(secondaryReportId); + F3XAggregationHelpers.assertScheduleEAggregateFieldOnOpen(secondaryTxnId, '$50.00'); + F3XAggregationHelpers.clickSave(); + }) + .then(() => { + F3XAggregationHelpers.switchCommittee(F3XAggregationHelpers.committeePrimaryId); + F3XAggregationHelpers.goToReport(primaryReportId); + F3XAggregationHelpers.assertScheduleEAggregateFieldOnOpen(primaryTxnId, '$100.00'); + }); + }); +}); diff --git a/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts b/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts new file mode 100644 index 0000000000..30514f9cea --- /dev/null +++ b/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts @@ -0,0 +1,1022 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { currentYear, PageUtils } from '../../e2e-smoke/pages/pageUtils'; +import { ReportListPage } from '../../e2e-smoke/pages/reportListPage'; +import { StartTransaction } from '../../e2e-smoke/F3X/utils/start-transaction/start-transaction'; +import { ContactLookup } from '../../e2e-smoke/pages/contactLookup'; +import { TransactionDetailPage } from '../../e2e-smoke/pages/transactionDetailPage'; +import { defaultScheduleFormData, DisbursementFormData } from '../../e2e-smoke/models/TransactionFormModel'; +import { DataSetup } from '../../e2e-smoke/F3X/setup'; +import { F3X, F3X_Q2 } from '../../e2e-smoke/requests/library/reports'; +import { makeContact, makeF3x, makeTransaction } from '../../e2e-smoke/requests/methods'; +import { + Authorizor, + buildContributionToCandidate, + buildDebtOwedByCommittee, + buildLoanAgreement, + buildLoanFromBank, + buildLoanReceipt, + buildScheduleA, + buildScheduleF, + LoanInfo, +} from '../../e2e-smoke/requests/library/transactions'; +import { + Candidate_House_A, + Candidate_Senate_A, + Committee_A, + Individual_A_A, + MockContact, + Organization_A, +} from '../../e2e-smoke/requests/library/contacts'; + +export interface SeedEntry { + amount: number; + date: string; + extra?: Record; +} + +export interface ScheduleECreateArgs { + reportId: string; + payeeContactName: string; + candidate: any; + amount: number; + disbursementDate: Date; + disseminationDate?: Date; + electionCode?: string; +} + +const receiptsTableRoot = 'app-transaction-receipts p-table'; +const disbursementsTableRoot = 'app-transaction-disbursements p-table'; +const loansAndDebtsTableRoot = 'app-transaction-loans-and-debts p-table'; + +export class F3XAggregationHelpers { + static readonly receiptsTableRoot = receiptsTableRoot; + static readonly disbursementsTableRoot = disbursementsTableRoot; + static readonly loansAndDebtsTableRoot = loansAndDebtsTableRoot; + + static readonly committeePrimaryId = 'c94c5d1a-9e73-464d-ad72-b73b5d8667a9'; + static readonly committeeSecondaryId = '7c176dc0-7062-49b5-bc35-58b4ef050d09'; + static readonly committeePtyId = '7c176dc0-7062-49b5-bc35-58b4ef050d08'; + static readonly rowActionButtonSelector = 'app-table-actions-button button[aria-label="action"]:visible'; + static readonly rowActionMenuButtonSelector = '.p-popover:visible .table-action-button:visible'; + static readonly confirmDialogSelector = 'app-confirm-dialog dialog[open]'; + static readonly confirmDialogSubmitSelector = + 'app-confirm-dialog dialog[open] button[data-cy="membership-submit"]'; + static readonly saveButtonSelector = 'button[data-cy="navigation-control-button"]'; + + private static exactText(value: string): RegExp { + return new RegExp(`^\\s*${Cypress._.escapeRegExp(value)}\\s*$`); + } + + private static suffix(): string { + return `${Date.now()}-${Cypress._.random(1000, 9999)}`; + } + + private static shortSuffix(): string { + return `${Date.now().toString(36).slice(-4)}${Cypress._.random(0, 1295).toString(36).padStart(2, '0')}`; + } + + private static boundedLabel(prefix: string, maxLength = 20): string { + return `${prefix}${this.shortSuffix()}`.slice(0, maxLength); + } + + private static assertTransactionId(transactionId: string, context: string): void { + if (!transactionId || transactionId === 'undefined') { + throw new Error(`${context} transaction id is missing`); + } + } + + private static getRequiredId(payload: any, context: string): string { + const id = payload?.id as string | undefined; + this.assertTransactionId(id ?? '', context); + return id as string; + } + + static transactionIdFromPayload(payload: unknown, context: string): string { + const idFromString = typeof payload === 'string' ? payload : ''; + const idFromObject = + payload && typeof payload === 'object' && 'id' in payload + ? (payload as { id?: string }).id ?? '' + : ''; + const transactionId = idFromString || idFromObject; + this.assertTransactionId(transactionId, context); + return transactionId; + } + + private static normalizeTransactionPayload(payload: unknown, context: string): { id: string } & Record { + const id = this.transactionIdFromPayload(payload, context); + if (payload && typeof payload === 'object') { + return { ...(payload as Record), id }; + } + return { id }; + } + + static uniqueIndividualSeed(): MockContact { + return { + ...Individual_A_A, + first_name: this.boundedLabel('Ind'), + last_name: this.boundedLabel('Agg'), + }; + } + + static uniqueOrganizationSeed(): MockContact { + return { + ...Organization_A, + name: this.boundedLabel('Organization ', 32), + }; + } + + static uniqueCommitteeSeed(): MockContact { + return { + ...Committee_A, + committee_id: `C${String(Cypress._.random(0, 99999999)).padStart(8, '0')}`, + name: `Committee ${this.suffix()}`, + }; + } + + static uniqueHouseCandidateSeed(): MockContact { + const numericSuffix = String(Cypress._.random(0, 99999)).padStart(5, '0'); + return { + ...Candidate_House_A, + candidate_id: `H1AK${numericSuffix}`, + first_name: this.boundedLabel('House'), + last_name: this.boundedLabel('Candidate'), + }; + } + + static uniqueSenateCandidateSeed(): MockContact { + const numericSuffix = String(Cypress._.random(0, 99999)).padStart(5, '0'); + return { + ...Candidate_Senate_A, + candidate_id: `S1AK${numericSuffix}`, + first_name: this.boundedLabel('Senate'), + last_name: this.boundedLabel('Candidate'), + }; + } + + static setupWithContacts(setup: Parameters[0]): Cypress.Chainable { + return cy.then(() => DataSetup(setup)); + } + + static createContact(contact: MockContact): Cypress.Chainable { + return makeContact(contact).then((response) => { + const created = response.body; + this.getRequiredId(created, 'createContact'); + return created; + }); + } + + static createReport(report: F3X = F3X_Q2): Cypress.Chainable { + return makeF3x(report).then((response) => { + return this.getRequiredId(response.body, 'createReport'); + }); + } + + static deleteReport(reportId: string): Cypress.Chainable { + return cy.getCookie('csrftoken').then((cookie) => { + return cy + .request({ + method: 'DELETE', + url: `http://localhost:8080/api/v1/reports/${reportId}/`, + headers: { + 'x-csrftoken': cookie?.value, + }, + }) + .its('status') + .should('be.oneOf', [200, 202, 204]); + }); + } + + static deleteTransactionById(transactionId: string): Cypress.Chainable { + this.assertTransactionId(transactionId, 'deleteTransactionById'); + return cy.getCookie('csrftoken').then((cookie) => { + return cy + .request({ + method: 'DELETE', + url: `http://localhost:8080/api/v1/transactions/${transactionId}/`, + headers: { + 'x-csrftoken': cookie?.value, + }, + }) + .its('status') + .should('be.oneOf', [200, 202, 204]); + }); + } + + static listLoanRepaymentIdsForLoan(reportId: string, loanId: string): Cypress.Chainable { + this.assertTransactionId(loanId, 'listLoanRepaymentIdsForLoan'); + return cy + .request({ + method: 'GET', + url: 'http://localhost:8080/api/v1/transactions/', + qs: { + page: 1, + page_size: 100, + ordering: 'line_label,created', + report_id: reportId, + }, + }) + .then((response) => { + const rows = Array.isArray(response.body?.results) ? response.body.results : []; + return rows + .filter( + (transaction: any) => + transaction?.transaction_type_identifier === 'LOAN_REPAYMENT_MADE' && + transaction?.loan_id === loanId, + ) + .map((transaction: any) => String(transaction.id)) + .filter((id: string) => !!id); + }); + } + + static deleteTransactionsAndVerify404(transactionIds: string[]): Cypress.Chainable { + return cy + .wrap(transactionIds) + .each((transactionId) => { + const id = String(transactionId); + this.assertTransactionId(id, 'deleteTransactionsAndVerify404'); + return this.deleteTransactionById(id).then(() => { + return cy + .request({ + method: 'GET', + url: `http://localhost:8080/api/v1/transactions/${id}/`, + failOnStatusCode: false, + }) + .then((response) => { + expect(response.status).to.equal(404); + }); + }); + }) + .then(() => undefined); + } + + static createTransaction(payload: Record): Cypress.Chainable { + return makeTransaction(payload).then((response) => { + const created = this.normalizeTransactionPayload(response.body, 'createTransaction'); + return created; + }); + } + + static getTransaction(transactionId: string): Cypress.Chainable { + this.assertTransactionId(transactionId, 'getTransaction'); + return cy.request({ + method: 'GET', + url: `http://localhost:8080/api/v1/transactions/${transactionId}/`, + }).its('body'); + } + + static readLoanBalanceValueByApi(loanId: string): Cypress.Chainable { + this.assertTransactionId(loanId, 'readLoanBalanceValueByApi'); + return this.getTransaction(loanId).then((transaction) => { + const rawBalance = transaction?.loan_balance ?? transaction?.balance; + const parsedBalance = + typeof rawBalance === 'number' ? rawBalance : Number(String(rawBalance ?? '').replace(/[^0-9.-]/g, '')); + + if (Number.isNaN(parsedBalance)) { + throw new Error(`readLoanBalanceValueByApi unable to parse loan balance for transaction ${loanId}`); + } + + return parsedBalance; + }); + } + + static waitForLoanBalanceRestoreByApi( + loanId: string, + expectedBalance: number, + options: { maxAttempts?: number; intervalMs?: number } = {}, + ): Cypress.Chainable { + this.assertTransactionId(loanId, 'waitForLoanBalanceRestoreByApi'); + const maxAttempts = options.maxAttempts ?? 8; + const intervalMs = options.intervalMs ?? 500; + + const poll = (attempt: number): Cypress.Chainable => { + return this.readLoanBalanceValueByApi(loanId).then((actualBalance) => { + cy.log( + `C1-C5 strict poll ${attempt + 1}/${maxAttempts + 1}: loanId=${loanId}, actual=${actualBalance}, expected=${expectedBalance}`, + ); + if (actualBalance === expectedBalance) { + cy.log(`C1-C5 strict poll resolved: loanId=${loanId}, restored_balance=${actualBalance}`); + return; + } + + if (attempt >= maxAttempts) { + cy.log( + `C1-C5 strict poll exhausted: loanId=${loanId}, final_actual=${actualBalance}, expected=${expectedBalance}`, + ); + expect( + actualBalance, + `Loan balance restore via API after repayment delete (attempt ${attempt + 1}/${maxAttempts + 1})`, + ).to.equal(expectedBalance); + return; + } + + return cy.wait(intervalMs).then(() => poll(attempt + 1)); + }); + }; + + return poll(0); + } + + static updateTransaction(transactionId: string, payload: Record): Cypress.Chainable { + this.assertTransactionId(transactionId, 'updateTransaction'); + return cy.getCookie('csrftoken').then((cookie) => { + return cy + .request({ + // API blocks partial_update (PATCH) on this endpoint, so tests must use full update semantics. + method: 'PUT', + url: `http://localhost:8080/api/v1/transactions/${transactionId}/`, + body: payload, + headers: { + 'x-csrftoken': cookie?.value, + }, + }) + .its('body'); + }); + } + + static seedScheduleAChain(reportId: string, contact: any, entries: SeedEntry[]): Cypress.Chainable { + const transactionIds: string[] = []; + let chain: Cypress.Chainable = cy.wrap(null); + entries.forEach((entry) => { + chain = chain.then(() => { + return this.createTransaction( + buildScheduleA('INDIVIDUAL_RECEIPT', entry.amount, entry.date, contact, reportId, entry.extra), + ).then((created) => { + transactionIds.push(created.id); + }); + }); + }); + return chain.then(() => transactionIds); + } + + static seedScheduleBChain( + reportId: string, + payee: any, + candidate: any, + entries: SeedEntry[], + ): Cypress.Chainable { + const transactionIds: string[] = []; + let chain: Cypress.Chainable = cy.wrap(null); + entries.forEach((entry) => { + chain = chain.then(() => { + return this.createTransaction( + buildContributionToCandidate(entry.amount, entry.date, [payee, candidate], reportId, { + election_code: `P${currentYear}`, + support_oppose_code: 'S', + date_signed: `${currentYear}-04-10`, + ...entry.extra, + }), + ).then((created) => { + transactionIds.push(created.id); + }); + }); + }); + return chain.then(() => transactionIds); + } + + static seedScheduleFChain( + reportId: string, + payee: any, + candidate: any, + committee: any, + entries: SeedEntry[], + ): Cypress.Chainable { + const transactionIds: string[] = []; + let chain: Cypress.Chainable = cy.wrap(null); + entries.forEach((entry) => { + chain = chain.then(() => { + return this.createTransaction( + buildScheduleF(entry.amount, entry.date, payee, candidate, committee, reportId, entry.extra), + ).then((created) => { + transactionIds.push(created.id); + }); + }); + }); + return chain.then(() => transactionIds); + } + + static seedDebtToCommittee( + reportId: string, + committeeContact: any, + debtPurpose: string, + amount: number, + ): Cypress.Chainable { + return this.createTransaction(buildDebtOwedByCommittee(committeeContact, reportId, debtPurpose, amount)).then((created) => { + return created.id; + }); + } + + static seedLoanFromBank(reportId: string, organization: any): Cypress.Chainable { + const loanInfo: LoanInfo = { + loan_amount: 6000, + loan_incurred_date: `${currentYear}-04-12`, + loan_due_date: `${currentYear}-08-01`, + loan_interest_rate: '2.1%', + secured: false, + loan_restructured: false, + }; + + const authorizors: [Authorizor, Authorizor] = [ + { + last_name: 'Treasurer', + first_name: 'Taylor', + middle_name: null, + prefix: null, + suffix: null, + date_signed: `${currentYear}-04-12`, + }, + { + last_name: 'Banker', + first_name: 'Jamie', + middle_name: null, + prefix: null, + suffix: null, + title: 'Manager', + date_signed: `${currentYear}-04-12`, + }, + ]; + + const agreement = buildLoanAgreement(loanInfo, organization, authorizors, reportId); + const receipt = buildLoanReceipt(loanInfo.loan_amount, loanInfo.loan_incurred_date, organization, reportId); + const payload = buildLoanFromBank(loanInfo, organization, reportId, [agreement, receipt]); + return this.createTransaction(payload).then((created) => created.id); + } + + static createIndependentExpenditureViaUI(args: ScheduleECreateArgs): Cypress.Chainable { + let createdId = ''; + const disbursementDate = args.disbursementDate; + const disseminationDate = args.disseminationDate ?? args.disbursementDate; + const electionTypeFromCode = args.electionCode?.slice(0, 1) ?? ''; + const parsedElectionYear = args.electionCode ? Number(args.electionCode.slice(1)) : Number.NaN; + const electionYearFromCode = Number.isNaN(parsedElectionYear) ? currentYear : parsedElectionYear; + this.goToReport(args.reportId); + StartTransaction.Disbursements().Contributions().IndependentExpenditure(); + ContactLookup.getContact(args.payeeContactName, '', 'Individual'); + + cy.intercept({ + method: 'POST', + pathname: '/api/v1/transactions/', + }).as('CreateScheduleETransaction'); + + const formData: DisbursementFormData = { + ...defaultScheduleFormData, + amount: args.amount, + date_received: disbursementDate, + date2: disseminationDate, + supportOpposeCode: 'SUPPORT', + electionType: electionTypeFromCode || 'G', + electionYear: electionYearFromCode, + signatoryDateSigned: disbursementDate, + signatoryFirstName: this.boundedLabel('Sig'), + signatoryLastName: this.boundedLabel('Sig'), + }; + + TransactionDetailPage.enterSheduleFormDataForVoidExpenditure(formData, args.candidate, false, '', 'date_signed'); + this.clearAndType('#electionYear', `${electionYearFromCode}`); + PageUtils.blurActiveField(); + this.clickSave(); + + cy.wait('@CreateScheduleETransaction').then((interception) => { + expect(interception.response?.statusCode, 'CreateScheduleETransaction status').to.be.oneOf([200, 201]); + createdId = this.transactionIdFromPayload(interception.response?.body, 'createIndependentExpenditureViaUI'); + }); + cy.contains('Transactions in this report').should('exist'); + return cy.then(() => createdId); + } + + static interceptDeleteTransaction(alias = 'DeleteTransaction'): void { + cy.intercept({ + method: 'DELETE', + pathname: /\/api\/v1\/transactions\/[^/]+\/$/, + }).as(alias); + } + + static interceptUpdateItemizationAggregation(alias = 'UpdateItemizationAggregation'): void { + cy.intercept({ + method: 'PUT', + pathname: /\/api\/v1\/transactions\/[^/]+\/update-itemization-aggregation\/$/, + }).as(alias); + } + + static waitAlias(alias: string): void { + cy.wait(`@${alias}`); + } + + static goToReport(reportId: string): void { + ReportListPage.goToReportList(reportId); + cy.contains('Transactions in this report').should('exist'); + } + + static reloadReport(reportId: string): void { + this.goToReport(reportId); + } + + static switchCommittee(committeeId: string): void { + PageUtils.switchCommittee(committeeId); + } + + static rowLinkById(tableRoot: string, transactionId: string): Cypress.Chainable> { + this.assertTransactionId(transactionId, 'rowLinkById'); + return cy.get(`${tableRoot} a[href*="/list/${transactionId}"]`).first().should('exist'); + } + + static rowById(tableRoot: string, transactionId: string): Cypress.Chainable> { + return this.rowLinkById(tableRoot, transactionId).closest('tr').should('exist'); + } + + static openRowById(tableRoot: string, transactionId: string): void { + this.rowLinkById(tableRoot, transactionId).click(); + } + + static clickRowActionById(tableRoot: string, transactionId: string, actionLabel: string): void { + this.rowById(tableRoot, transactionId).within(() => { + cy.get(this.rowActionButtonSelector).first().click(); + }); + cy.get(this.rowActionMenuButtonSelector) + .filter((_, button) => this.exactText(actionLabel).test(button.textContent ?? '')) + .should('have.length', 1) + .first() + .click(); + } + + static clickRowActionByCellText(tableRoot: string, cellText: string, actionLabel: string): void { + cy.get(tableRoot) + .contains('td', this.exactText(cellText)) + .first() + .closest('tr') + .within(() => { + cy.get(this.rowActionButtonSelector).should('have.length', 1).first().click(); + }); + + cy.get(this.rowActionMenuButtonSelector) + .filter((_, button) => this.exactText(actionLabel).test(button.textContent ?? '')) + .should('have.length', 1) + .first() + .click(); + } + + static confirmDialog(): void { + cy.get(this.confirmDialogSelector).should('exist'); + cy.get(this.confirmDialogSubmitSelector).contains(this.exactText('Confirm')).click(); + cy.get(this.confirmDialogSelector).should('not.exist'); + } + + static clickSave(): void { + cy.get('body').then(($body) => { + if ($body.find('.p-datepicker-panel:visible').length > 0) { + cy.get('body').type('{esc}'); + } + }); + PageUtils.blurActiveField(); + cy.get(`${this.saveButtonSelector}:visible:not([disabled])`) + .filter((_, button) => this.exactText('Save').test(button.textContent ?? '')) + .should('have.length.at.least', 1) + .last() + .click(); + } + + static assertRowStatus( + tableRoot: string, + transactionId: string, + statusLabel: 'Unitemized' | 'Unaggregated', + present: boolean, + ): void { + this.assertTransactionId(transactionId, 'assertRowStatus'); + cy.get(tableRoot).should(($table) => { + const rowLink = $table.find(`a[href*="/list/${transactionId}"]`).first(); + expect(rowLink.length, `row link for ${transactionId}`).to.equal(1); + const statusCellText = rowLink.closest('tr').find('td').eq(1).text(); + + if (present) { + expect(statusCellText).to.contain(statusLabel); + } else { + expect(statusCellText).not.to.contain(statusLabel); + } + }); + } + + static assertReceiptAggregate(transactionId: string, expected: string): void { + this.rowById(this.receiptsTableRoot, transactionId).find('td').eq(6).should('contain', expected); + } + + static assertReceiptRowStatus(transactionId: string, statusLabel: 'Unitemized' | 'Unaggregated', present: boolean): void { + this.assertRowStatus(this.receiptsTableRoot, transactionId, statusLabel, present); + } + + static assertDisbursementAmount(transactionId: string, expected: string): void { + this.rowById(this.disbursementsTableRoot, transactionId).find('td').eq(5).should('contain', expected); + } + + static assertLoansBalance(transactionId: string, expected: string): void { + this.rowById(this.loansAndDebtsTableRoot, transactionId).find('td').eq(5).should('contain', expected); + } + + static assertAggregateField(expected: string): void { + cy.get('#aggregate').should('have.value', expected); + } + + static assertCalendarYtdField(expected: string): void { + cy.get('#calendar_ytd').should('have.value', expected); + } + + static assertScheduleFAggregateField(expected: string): void { + cy.get('#aggregate_general_elec_expended').should('have.value', expected); + } + + static assertDebtBalanceAtCloseField(expected: string): void { + cy.get('#balance_at_close').should('have.value', expected); + } + + static assertLoanBalanceFields(expectedBalance: string, expectedPaymentToDate: string): void { + cy.get('#balance').should('have.value', expectedBalance); + cy.get('#payment_amount').should('have.value', expectedPaymentToDate); + } + + static deleteRowById(tableRoot: string, transactionId: string): void { + this.assertTransactionId(transactionId, 'deleteRowById'); + this.interceptDeleteTransaction(); + this.clickRowActionById(tableRoot, transactionId, 'Delete'); + this.confirmDialog(); + this.waitAlias('DeleteTransaction'); + cy.get(`${tableRoot} a[href*="/list/${transactionId}"]`).should('not.exist'); + } + + static itemizeRowById(tableRoot: string, transactionId: string): void { + this.interceptUpdateItemizationAggregation(); + this.clickRowActionById(tableRoot, transactionId, 'Itemize'); + this.confirmDialog(); + this.waitAlias('UpdateItemizationAggregation'); + } + + static unitemizeRowById(tableRoot: string, transactionId: string): void { + this.interceptUpdateItemizationAggregation(); + this.clickRowActionById(tableRoot, transactionId, 'Unitemize'); + this.confirmDialog(); + this.waitAlias('UpdateItemizationAggregation'); + } + + static aggregateRowById(tableRoot: string, transactionId: string): void { + this.interceptUpdateItemizationAggregation(); + this.clickRowActionById(tableRoot, transactionId, 'Aggregate'); + this.waitAlias('UpdateItemizationAggregation'); + } + + static unaggregateRowById(tableRoot: string, transactionId: string): void { + this.interceptUpdateItemizationAggregation(); + this.clickRowActionById(tableRoot, transactionId, 'Unaggregate'); + this.waitAlias('UpdateItemizationAggregation'); + } + + static assertDialogTitle(title: string): void { + cy.get(this.confirmDialogSelector).contains(this.exactText(title)).should('exist'); + } + + static clickConfirmDialogContinue(): void { + cy.get(this.confirmDialogSubmitSelector).contains(this.exactText('Continue')).click(); + } + + static clearAndType(selector: string, value: string): void { + cy.get(selector).clear().safeType(value).blur(); + } + + static assertReceiptTransactionAbsent(transactionId: string): void { + this.assertTransactionId(transactionId, 'assertReceiptTransactionAbsent'); + cy.get(`${this.receiptsTableRoot} a[href*="/list/${transactionId}"]`).should('not.exist'); + } + + static assertDisbursementTransactionAbsent(transactionId: string): void { + this.assertTransactionId(transactionId, 'assertDisbursementTransactionAbsent'); + cy.get(`${this.disbursementsTableRoot} a[href*="/list/${transactionId}"]`).should('not.exist'); + } + + static assertLoanOrDebtTransactionAbsent(transactionId: string): void { + this.assertTransactionId(transactionId, 'assertLoanOrDebtTransactionAbsent'); + cy.get(`${this.loansAndDebtsTableRoot} a[href*="/list/${transactionId}"]`).should('not.exist'); + } + + static openDebtRepaymentSelection(debtId: string): void { + this.clickRowActionById(this.loansAndDebtsTableRoot, debtId, 'Report debt repayment'); + PageUtils.urlCheck('select/receipt?debt='); + } + + static openDebtDisbursementRepaymentSelection(debtId: string): void { + this.clickRowActionById(this.loansAndDebtsTableRoot, debtId, 'Report debt repayment'); + PageUtils.urlCheck('select/disbursement?debt='); + } + + static openLoanRepaymentSelection(loanId: string): void { + this.clickRowActionById(this.loansAndDebtsTableRoot, loanId, 'Make loan repayment'); + PageUtils.urlCheck('create/LOAN_REPAYMENT_MADE?loan='); + } + + static reportDebtRepaymentAsReceipt(): void { + PageUtils.clickAccordion('CONTRIBUTIONS FROM INDIVIDUALS/PERSONS'); + PageUtils.clickLink('Individual Receipt'); + } + + static openLoanAgreement(loanId: string): void { + this.clickRowActionById(this.loansAndDebtsTableRoot, loanId, 'Review loan agreement'); + } + + static openCreateLoanAgreement(loanId: string): void { + this.clickRowActionById(this.loansAndDebtsTableRoot, loanId, 'New loan agreement'); + } + + static assertConfirmDialogOpen(): void { + cy.get(this.confirmDialogSelector).should('exist'); + } + + static assertConfirmDialogClosed(): void { + cy.get(this.confirmDialogSelector).should('not.exist'); + } + + static clickConfirmDialogConfirm(): void { + cy.get(this.confirmDialogSubmitSelector).contains(this.exactText('Confirm')).click(); + } + + static clickConfirmDialogCancel(): void { + cy.get(this.confirmDialogSelector).contains('button', this.exactText('Cancel')).click(); + } + + static assertReceiptLineAmount(transactionId: string, expected: string): void { + this.rowById(this.receiptsTableRoot, transactionId).find('td').eq(5).should('contain', expected); + } + + static assertDisbursementRowStatus( + transactionId: string, + statusLabel: 'Unitemized' | 'Unaggregated', + present: boolean, + ): void { + this.assertRowStatus(this.disbursementsTableRoot, transactionId, statusLabel, present); + } + + static assertLoanOrDebtRowStatus(transactionId: string, statusLabel: 'Unitemized', present: boolean): void { + this.assertRowStatus(this.loansAndDebtsTableRoot, transactionId, statusLabel, present); + } + + static assertReceiptListLoaded(): void { + cy.get(this.receiptsTableRoot).should('exist'); + } + + static assertDisbursementListLoaded(): void { + cy.get(this.disbursementsTableRoot).should('exist'); + } + + static assertLoanAndDebtListLoaded(): void { + cy.get(this.loansAndDebtsTableRoot).should('exist'); + } + + static assertCurrentPathIncludes(pathPart: string): void { + cy.location('pathname').should('include', pathPart); + } + + static assertVisibleTextInRow(tableRoot: string, transactionId: string, expected: string): void { + this.rowById(tableRoot, transactionId).should('contain', expected); + } + + static assertNotVisibleTextInRow(tableRoot: string, transactionId: string, expected: string): void { + this.rowById(tableRoot, transactionId).should('not.contain', expected); + } + + static clickTableActionWithoutConfirm(tableRoot: string, transactionId: string, actionLabel: string): void { + this.clickRowActionById(tableRoot, transactionId, actionLabel); + } + + static assertRowExists(tableRoot: string, transactionId: string): void { + this.rowById(tableRoot, transactionId).should('exist'); + } + + static assertRowNotExists(tableRoot: string, transactionId: string): void { + this.assertTransactionId(transactionId, 'assertRowNotExists'); + cy.get(`${tableRoot} a[href*="/list/${transactionId}"]`).should('not.exist'); + } + + static assertReceiptAggregateFieldOnOpen(transactionId: string, expected: string): void { + this.openRowById(this.receiptsTableRoot, transactionId); + this.assertAggregateField(expected); + } + + static assertScheduleEAggregateFieldOnOpen(transactionId: string, expected: string): void { + this.openRowById(this.disbursementsTableRoot, transactionId); + this.assertCalendarYtdField(expected); + } + + static assertScheduleFAggregateFieldOnOpen(transactionId: string, expected: string): void { + this.openRowById(this.disbursementsTableRoot, transactionId); + this.assertScheduleFAggregateField(expected); + } + + static assertDebtBalanceFieldOnOpen(transactionId: string, expected: string): void { + this.openRowById(this.loansAndDebtsTableRoot, transactionId); + this.assertDebtBalanceAtCloseField(expected); + } + + static assertLoanFieldsOnOpen(transactionId: string, expectedBalance: string, expectedPaymentToDate: string): void { + this.openRowById(this.loansAndDebtsTableRoot, transactionId); + this.assertLoanBalanceFields(expectedBalance, expectedPaymentToDate); + } + + static assertReceiptRowCount(expectedCount: number): void { + cy.get(`${this.receiptsTableRoot} tbody tr`).should('have.length', expectedCount); + } + + static assertDisbursementRowCount(expectedCount: number): void { + cy.get(`${this.disbursementsTableRoot} tbody tr`).should('have.length', expectedCount); + } + + static assertLoanAndDebtRowCount(expectedCount: number): void { + cy.get(`${this.loansAndDebtsTableRoot} tbody tr`).should('have.length', expectedCount); + } + + static assertStatusPersistsAfterReload( + reportId: string, + tableRoot: string, + transactionId: string, + statusLabel: 'Unitemized' | 'Unaggregated', + expected: boolean, + ): void { + this.reloadReport(reportId); + this.assertRowStatus(tableRoot, transactionId, statusLabel, expected); + } + + static waitForTransactionsRefresh(reportId: string): void { + this.goToReport(reportId); + } + + static assertDialogMessageContains(expected: string): void { + cy.get(this.confirmDialogSelector).should('contain', expected); + } + + static assertReceiptAggregateAfterOpenAndBack(transactionId: string, expected: string): void { + this.openRowById(this.receiptsTableRoot, transactionId); + this.assertAggregateField(expected); + this.clickSave(); + cy.contains('Transactions in this report').should('exist'); + } + + static assertScheduleEAggregateAfterOpenAndBack(transactionId: string, expected: string): void { + this.openRowById(this.disbursementsTableRoot, transactionId); + this.assertCalendarYtdField(expected); + this.clickSave(); + cy.contains('Transactions in this report').should('exist'); + } + + static assertScheduleFAggregateAfterOpenAndBack(transactionId: string, expected: string): void { + this.openRowById(this.disbursementsTableRoot, transactionId); + this.assertScheduleFAggregateField(expected); + this.clickSave(); + cy.contains('Transactions in this report').should('exist'); + } + + static assertDebtBalanceAfterOpenAndBack(transactionId: string, expected: string): void { + this.openRowById(this.loansAndDebtsTableRoot, transactionId); + this.assertDebtBalanceAtCloseField(expected); + this.clickSave(); + cy.contains('Transactions in this report').should('exist'); + } + + static assertLoanFieldsAfterOpenAndBack(transactionId: string, expectedBalance: string, expectedPaymentToDate: string): void { + this.openRowById(this.loansAndDebtsTableRoot, transactionId); + this.assertLoanBalanceFields(expectedBalance, expectedPaymentToDate); + this.clickSave(); + cy.contains('Transactions in this report').should('exist'); + } + + static assertHasOpenConfirmDialog(): void { + cy.get(this.confirmDialogSelector).should('exist'); + } + + static assertHasNoOpenConfirmDialog(): void { + cy.get(this.confirmDialogSelector).should('not.exist'); + } + + static assertReceiptName(transactionId: string, expected: string): void { + this.rowById(this.receiptsTableRoot, transactionId).find('td').eq(2).should('contain', expected); + } + + static assertDisbursementName(transactionId: string, expected: string): void { + this.rowById(this.disbursementsTableRoot, transactionId).find('td').eq(2).should('contain', expected); + } + + static assertLoanOrDebtName(transactionId: string, expected: string): void { + this.rowById(this.loansAndDebtsTableRoot, transactionId).find('td').eq(2).should('contain', expected); + } + + static assertAggregateInOpenReceiptForm(expected: string): void { + this.assertAggregateField(expected); + } + + static assertCalendarYtdInOpenDisbursementForm(expected: string): void { + this.assertCalendarYtdField(expected); + } + + static assertScheduleFAggregateInOpenDisbursementForm(expected: string): void { + this.assertScheduleFAggregateField(expected); + } + + static assertDebtBalanceInOpenLoanDebtForm(expected: string): void { + this.assertDebtBalanceAtCloseField(expected); + } + + static assertLoanBalanceInOpenLoanDebtForm(expectedBalance: string, expectedPaymentToDate: string): void { + this.assertLoanBalanceFields(expectedBalance, expectedPaymentToDate); + } + + static openReceipt(transactionId: string): void { + this.openRowById(this.receiptsTableRoot, transactionId); + } + + static openDisbursement(transactionId: string): void { + this.openRowById(this.disbursementsTableRoot, transactionId); + } + + static openLoanOrDebt(transactionId: string): void { + this.openRowById(this.loansAndDebtsTableRoot, transactionId); + } + + static saveAndReturnToList(): void { + this.clickSave(); + cy.contains('Transactions in this report').should('exist'); + } + + static cancelAndReturnToList(): void { + PageUtils.clickButton('Cancel'); + cy.contains('Transactions in this report').should('exist'); + } + + static assertRouteContains(fragment: string): void { + cy.url().should('include', fragment); + } + + static assertVisibleInList(tableRoot: string, transactionId: string): void { + this.rowLinkById(tableRoot, transactionId).should('exist'); + } + + static assertHiddenInList(tableRoot: string, transactionId: string): void { + this.assertTransactionId(transactionId, 'assertHiddenInList'); + cy.get(`${tableRoot} a[href*="/list/${transactionId}"]`).should('not.exist'); + } + + static assertReceiptAmount(transactionId: string, expected: string): void { + this.assertReceiptLineAmount(transactionId, expected); + } + + static assertReceiptMemoCode(transactionId: string, expected: string): void { + this.rowById(this.receiptsTableRoot, transactionId).find('td').eq(4).should('contain', expected); + } + + static assertDisbursementMemoCode(transactionId: string, expected: string): void { + this.rowById(this.disbursementsTableRoot, transactionId).find('td').eq(4).should('contain', expected); + } + + static assertLoanOrDebtAmount(transactionId: string, expected: string): void { + this.rowById(this.loansAndDebtsTableRoot, transactionId).find('td').eq(4).should('contain', expected); + } + + static assertReceiptDate(transactionId: string, expected: string): void { + this.rowById(this.receiptsTableRoot, transactionId).find('td').eq(3).should('contain', expected); + } + + static assertDisbursementDate(transactionId: string, expected: string): void { + this.rowById(this.disbursementsTableRoot, transactionId).find('td').eq(3).should('contain', expected); + } + + static assertLoanOrDebtIncurredDate(transactionId: string, expected: string): void { + this.rowById(this.loansAndDebtsTableRoot, transactionId).find('td').eq(3).should('contain', expected); + } + + static assertActionExists(tableRoot: string, transactionId: string, actionLabel: string): void { + this.rowById(tableRoot, transactionId).within(() => { + cy.get(this.rowActionButtonSelector).first().click(); + }); + cy.get(this.rowActionMenuButtonSelector) + .filter((_, button) => this.exactText(actionLabel).test(button.textContent ?? '')) + .should('have.length', 1) + .first() + .click(); + } + + static assertActionDoesNotExist(tableRoot: string, transactionId: string, actionLabel: string): void { + this.rowById(tableRoot, transactionId).within(() => { + cy.get(this.rowActionButtonSelector).first().click(); + }); + cy.get(this.rowActionMenuButtonSelector) + .filter((_, button) => this.exactText(actionLabel).test(button.textContent ?? '')) + .should('have.length', 0); + cy.get('body').type('{esc}'); + } + + static assertRowCellContains(tableRoot: string, transactionId: string, cellIndex: number, expected: string): void { + this.rowById(tableRoot, transactionId).find('td').eq(cellIndex).should('contain', expected); + } + + static assertRowCellNotContains(tableRoot: string, transactionId: string, cellIndex: number, expected: string): void { + this.rowById(tableRoot, transactionId).find('td').eq(cellIndex).should('not.contain', expected); + } + + static assertActionButtonPresent(tableRoot: string, transactionId: string): void { + this.rowById(tableRoot, transactionId).within(() => { + cy.get(this.rowActionButtonSelector).should('exist'); + }); + } + +} diff --git a/front-end/cypress/e2e-extended/f3x/itemization-cascades.cy.ts b/front-end/cypress/e2e-extended/f3x/itemization-cascades.cy.ts new file mode 100644 index 0000000000..bf1517c467 --- /dev/null +++ b/front-end/cypress/e2e-extended/f3x/itemization-cascades.cy.ts @@ -0,0 +1,165 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Initialize } from '../../e2e-smoke/pages/loginPage'; +import { currentYear, PageUtils } from '../../e2e-smoke/pages/pageUtils'; +import { DataSetup } from '../../e2e-smoke/F3X/setup'; +import { StartTransaction } from '../../e2e-smoke/F3X/utils/start-transaction/start-transaction'; +import { ContactLookup } from '../../e2e-smoke/pages/contactLookup'; +import { TransactionDetailPage } from '../../e2e-smoke/pages/transactionDetailPage'; +import { formTransactionDataForSchedule } from '../../e2e-smoke/models/TransactionFormModel'; +import { ReportListPage } from '../../e2e-smoke/pages/reportListPage'; +import { F3XAggregationHelpers } from './f3x-aggregation.helpers'; +import { TransactionTableColumns } from '../../e2e-smoke/pages/f3xTransactionListPage'; +import { buildScheduleA } from '../../e2e-smoke/requests/library/transactions'; + +function transactionIdFromRowHref(href: string | undefined, context: string): string { + const match = href?.match(/\/list\/([0-9a-f-]+)/i); + const transactionId = match?.[1] ?? ''; + if (!transactionId) { + throw new Error(`${context} transaction id is missing`); + } + return transactionId; +} + +function receiptTransactionIdByRow(rowIndex: number): Cypress.Chainable { + return cy + .get(`${F3XAggregationHelpers.receiptsTableRoot} tbody tr`) + .eq(rowIndex) + .find('a[href*="/list/"]') + .first() + .invoke('attr', 'href') + .then((href) => transactionIdFromRowHref(href, `receipt row ${rowIndex}`)); +} + +describe('Extended F3X Itemization Cascades', () => { + beforeEach(() => { + Initialize(); + }); + + it('parent/child receipt row-action unitemize updates parent while memo child remains itemized', () => { + cy.wrap(DataSetup({ individual: true })).then((result: any) => { + F3XAggregationHelpers.createTransaction( + buildScheduleA('INDIVIDUAL_RECEIPT', 250, `${currentYear}-04-27`, result.individual, result.report), + ).then((parent) => { + F3XAggregationHelpers.createTransaction( + buildScheduleA('INDIVIDUAL_RECEIPT', 250, `${currentYear}-04-28`, result.individual, result.report, { + parent_transaction_id: parent.id, + memo_code: true, + }), + ).then((child) => { + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.assertRowExists(F3XAggregationHelpers.receiptsTableRoot, parent.id); + F3XAggregationHelpers.assertRowExists(F3XAggregationHelpers.receiptsTableRoot, child.id); + F3XAggregationHelpers.getTransaction(parent.id).its('itemized').should('equal', true); + F3XAggregationHelpers.getTransaction(child.id).its('itemized').should('equal', true); + + F3XAggregationHelpers.unitemizeRowById(F3XAggregationHelpers.receiptsTableRoot, parent.id); + + F3XAggregationHelpers.getTransaction(parent.id).its('itemized').should('equal', false); + F3XAggregationHelpers.getTransaction(child.id).its('itemized').should('equal', true); + F3XAggregationHelpers.assertReceiptRowStatus(parent.id, 'Unitemized', true); + F3XAggregationHelpers.assertReceiptRowStatus(child.id, 'Unitemized', false); + }); + }); + }); + }); + + it('tier-3 joint fundraising row-action unitemize is rejected and preserves itemization state', () => { + cy.wrap(DataSetup({ committee: true, organization: true, individual: true })).then((result: any) => { + ReportListPage.goToReportList(result.report); + StartTransaction.Receipts().Transfers().JointFundraising(); + ContactLookup.getCommittee(result.committee); + + const tier1TransactionData = { + ...formTransactionDataForSchedule, + purpose_description: '', + category_code: '', + date_received: new Date(currentYear, 4 - 1, 27), + }; + + TransactionDetailPage.enterScheduleFormData( + tier1TransactionData, + false, + '', + true, + 'contribution_date', + ); + + const alias = PageUtils.getAlias(''); + cy.get(alias).find('[data-cy="navigation-control-dropdown"]').first().click(); + cy.get(alias).find('[data-cy="navigation-control-dropdown-option"]').contains('Partnership Receipt').click(); + cy.contains('h1', 'Partnership Receipt Joint Fundraising Transfer Memo').should('exist'); + ContactLookup.getContact(result.organization.name); + const tier2TransactionData = { + ...formTransactionDataForSchedule, + purpose_description: '', + category_code: '', + date_received: new Date(currentYear, 4 - 1, 27), + }; + TransactionDetailPage.enterScheduleFormData( + tier2TransactionData, + false, + '', + true, + 'contribution_date', + ); + + cy.get(alias).find('[data-cy="navigation-control-dropdown"]').first().click(); + cy.get(alias).find('[data-cy="navigation-control-dropdown-option"]').contains('Individual').click(); + cy.contains('h1', 'Individual Joint Fundraising Transfer Memo').should('exist'); + ContactLookup.getContact(result.individual.last_name); + const tier3TransactionData = { + ...formTransactionDataForSchedule, + purpose_description: '', + category_code: '', + date_received: new Date(currentYear, 4 - 1, 27), + }; + TransactionDetailPage.enterScheduleFormData( + tier3TransactionData, + false, + '', + true, + 'contribution_date', + ); + + cy.contains('button[data-cy="navigation-control-button"]', /^\s*Save\s*$/).click(); + cy.contains('Transactions in this report').should('exist'); + + cy.get(`${F3XAggregationHelpers.receiptsTableRoot} tbody tr`).eq(0).as('jfParentRow'); + cy.get('@jfParentRow') + .find('td') + .eq(TransactionTableColumns.transaction_type) + .should('contain', 'Joint Fundraising Transfer'); + cy.get(`${F3XAggregationHelpers.receiptsTableRoot} tbody tr`).eq(1).as('jfChildRowOne'); + cy.get('@jfChildRowOne') + .find('td') + .eq(TransactionTableColumns.transaction_type) + .should('contain', 'Partnership Receipt Joint Fundraising Transfer Memo'); + cy.get(`${F3XAggregationHelpers.receiptsTableRoot} tbody tr`).eq(2).as('jfChildRowTwo'); + cy.get('@jfChildRowTwo') + .find('td') + .eq(TransactionTableColumns.transaction_type) + .should('contain', 'Individual Joint Fundraising Transfer Memo'); + + receiptTransactionIdByRow(0).then((parentId) => { + receiptTransactionIdByRow(1).then((childOneId) => { + receiptTransactionIdByRow(2).then((childTwoId) => { + F3XAggregationHelpers.interceptUpdateItemizationAggregation('Tier3Unitemize'); + F3XAggregationHelpers.clickRowActionById(F3XAggregationHelpers.receiptsTableRoot, parentId, 'Unitemize'); + F3XAggregationHelpers.confirmDialog(); + cy.wait('@Tier3Unitemize').then((interception) => { + expect(interception.request.url).to.contain(`/transactions/${parentId}/update-itemization-aggregation/`); + expect(interception.response?.statusCode).to.equal(400); + }); + + F3XAggregationHelpers.getTransaction(parentId).its('itemized').should('equal', true); + F3XAggregationHelpers.getTransaction(childOneId).its('itemized').should('equal', true); + F3XAggregationHelpers.getTransaction(childTwoId).its('itemized').should('equal', true); + F3XAggregationHelpers.assertReceiptRowStatus(parentId, 'Unitemized', false); + F3XAggregationHelpers.assertReceiptRowStatus(childOneId, 'Unitemized', false); + F3XAggregationHelpers.assertReceiptRowStatus(childTwoId, 'Unitemized', false); + }); + }); + }); + }); + }); +}); diff --git a/front-end/cypress/e2e-extended/utils/shared.helpers.ts b/front-end/cypress/e2e-extended/utils/shared.helpers.ts index 0e9220fe5e..a6eba29031 100644 --- a/front-end/cypress/e2e-extended/utils/shared.helpers.ts +++ b/front-end/cypress/e2e-extended/utils/shared.helpers.ts @@ -16,7 +16,7 @@ export class SharedHelpers { // last resort: force the trigger if PrimeNG gives it 0 height const $trigger = $wrap.find('.p-select-dropdown, [aria-label="dropdown trigger"]').first(); - if ($trigger.length) return cy.wrap($trigger).scrollIntoView().click({ force: true }); + if ($trigger.length) return cy.wrap($trigger).scrollIntoView().click(); throw new Error('Results-per-page select not found'); }); diff --git a/front-end/cypress/e2e-smoke/F3X/aggregate-calculation.cy.ts b/front-end/cypress/e2e-smoke/F3X/aggregate-calculation.cy.ts index df69aff96d..7b63a72d0e 100644 --- a/front-end/cypress/e2e-smoke/F3X/aggregate-calculation.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/aggregate-calculation.cy.ts @@ -12,6 +12,7 @@ import { makeTransaction } from '../requests/methods'; import { buildScheduleA } from '../requests/library/transactions'; import { ContactLookup } from '../pages/contactLookup'; import { ReportListPage } from '../pages/reportListPage'; +import { F3XAggregationHelpers } from '../../e2e-extended/f3x/f3x-aggregation.helpers'; function setupTransactions(secondSame: boolean) { return cy.wrap(DataSetup({ individual: true, individual2: true })).then((result: any) => { @@ -45,7 +46,7 @@ describe('Tests transaction form aggregate calculation', () => { setupTransactions(true).then((result: any) => { ReportListPage.goToReportList(result.report); - cy.get('.p-datatable-tbody > tr') + cy.get(`${F3XAggregationHelpers.receiptsTableRoot} .p-datatable-tbody > tr`) .eq(1) // 0-based, so 2nd row .find('td') .eq(1) // 2nd cell @@ -90,13 +91,18 @@ describe('Tests transaction form aggregate calculation', () => { setupTransactions(false).then((result: any) => { ReportListPage.goToReportList(result.report); cy.contains('Transactions in this report').should('exist'); - cy.get('.p-datatable-tbody > :nth-child(2) > :nth-child(2) > a').click(); + cy.get(`${F3XAggregationHelpers.receiptsTableRoot} .p-datatable-tbody > :nth-child(2) > :nth-child(2) > a`) + .first() + .click(); // Tests changing the second transaction's contact cy.get('[id=aggregate]').should('have.value', '$25.00'); cy.get('[data-cy="searchBox"]').type('A'); cy.contains('Ant').should('exist'); - cy.contains('Ant').click({ force: true }); + cy.get('.p-autocomplete-list-container:visible') + .contains('.p-autocomplete-option', 'Ant') + .first() + .click(); PageUtils.blurActiveField(); cy.get('[id=aggregate]').should('have.value', '$225.01'); @@ -112,7 +118,9 @@ describe('Tests transaction form aggregate calculation', () => { setupTransactions(true).then((result: any) => { ReportListPage.goToReportList(result.report); cy.contains('Transactions in this report').should('exist'); - cy.get('.p-datatable-tbody > :nth-child(2) > :nth-child(2) > a').click(); + cy.get(`${F3XAggregationHelpers.receiptsTableRoot} .p-datatable-tbody > :nth-child(2) > :nth-child(2) > a`) + .first() + .click(); // Tests changing the amount cy.get('[id=aggregate]').should('have.value', '$225.01'); @@ -132,7 +140,9 @@ describe('Tests transaction form aggregate calculation', () => { setupTransactions(true).then((result: any) => { ReportListPage.goToReportList(result.report); cy.contains('Transactions in this report').should('exist'); - cy.get('.p-datatable-tbody > :nth-child(1) > :nth-child(2) > a').click(); + cy.get(`${F3XAggregationHelpers.receiptsTableRoot} .p-datatable-tbody > :nth-child(1) > :nth-child(2) > a`) + .first() + .click(); // Tests moving the first transaction's date to be later than the second TransactionDetailPage.enterDate('[data-cy="contribution_date"]', new Date(currentYear, 3, 30), ''); @@ -156,10 +166,12 @@ describe('Tests transaction form aggregate calculation', () => { result.individual, result.report, ); - makeTransaction(transaction_c, () => { + makeTransaction(transaction_c).then(() => { ReportListPage.goToReportList(result.report); cy.contains('Transactions in this report').should('exist'); - cy.get('.p-datatable-tbody > :nth-child(1) > :nth-child(2) > a').click(); + cy.get(`${F3XAggregationHelpers.receiptsTableRoot} .p-datatable-tbody > :nth-child(1) > :nth-child(2) > a`) + .first() + .click(); ContactLookup.getContact(result.individual2.last_name); @@ -264,7 +276,9 @@ describe('Tests transaction form aggregate calculation', () => { cy.contains('Transactions in this report').should('exist'); // Test aggregation re-calculation from date leapfrogging - cy.get('.p-datatable-tbody > :nth-child(1) > :nth-child(2) > a').click(); + cy.get(`${F3XAggregationHelpers.disbursementsTableRoot} .p-datatable-tbody > :nth-child(1) > :nth-child(2) > a`) + .first() + .click(); cy.contains('Payee').should('exist'); TransactionDetailPage.enterDate('[data-cy="disbursement_date"]', new Date(currentYear, 4 - 1, 20), ''); PageUtils.blurActiveField(); @@ -272,9 +286,84 @@ describe('Tests transaction form aggregate calculation', () => { PageUtils.clickButton('Save'); cy.contains('Transactions in this report').should('exist'); - cy.get('.p-datatable-tbody > :nth-child(2) > :nth-child(2) > a').click(); + cy.get(`${F3XAggregationHelpers.disbursementsTableRoot} .p-datatable-tbody > :nth-child(2) > :nth-child(2) > a`) + .first() + .click(); cy.contains('Payee').should('exist'); cy.get('#calendar_ytd').should('have.value', '$50.00'); }); }); + + it('schedule A delete earliest transaction reaggregates remaining chain', () => { + cy.wrap(DataSetup({ individual: true })).then((result: any) => { + F3XAggregationHelpers.seedScheduleAChain(result.report, result.individual, [ + { amount: 100, date: `${currentYear}-04-10` }, + { amount: 50, date: `${currentYear}-04-15` }, + { amount: 25, date: `${currentYear}-04-20` }, + ]).then((transactionIds) => { + const [firstId, secondId, thirdId] = transactionIds; + + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.assertReceiptAggregate(secondId, '$150.00'); + F3XAggregationHelpers.assertReceiptAggregate(thirdId, '$175.00'); + + F3XAggregationHelpers.clickRowActionById(F3XAggregationHelpers.receiptsTableRoot, firstId, 'Delete'); + F3XAggregationHelpers.confirmDialog(); + + cy.get(`${F3XAggregationHelpers.receiptsTableRoot} a[href*="/list/${firstId}"]`).should('not.exist'); + F3XAggregationHelpers.assertReceiptAggregate(secondId, '$50.00'); + F3XAggregationHelpers.assertReceiptAggregate(thirdId, '$75.00'); + }); + }); + }); + + it('schedule A insert middle-date transaction does not double-count downstream aggregate', () => { + cy.wrap(DataSetup({ individual: true })).then((result: any) => { + F3XAggregationHelpers.seedScheduleAChain(result.report, result.individual, [ + { amount: 100, date: `${currentYear}-04-10` }, + { amount: 150, date: `${currentYear}-04-20` }, + ]).then((seedIds) => { + const [firstId, lastId] = seedIds; + F3XAggregationHelpers.createTransaction( + buildScheduleA('INDIVIDUAL_RECEIPT', 75, `${currentYear}-04-15`, result.individual, result.report), + ).then((middleTransaction) => { + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.assertReceiptAggregate(firstId, '$100.00'); + F3XAggregationHelpers.assertReceiptAggregate(middleTransaction.id, '$175.00'); + F3XAggregationHelpers.assertReceiptAggregate(lastId, '$325.00'); + }); + }); + }); + }); + + it('schedule E delete transaction reaggregates calendar_ytd_per_election_office', () => { + cy.wrap(DataSetup({ individual: true, candidate: true })).then((result: any) => { + F3XAggregationHelpers.createIndependentExpenditureViaUI({ + reportId: result.report, + payeeContactName: result.individual.last_name, + candidate: result.candidate, + amount: 100, + disbursementDate: new Date(currentYear, 4 - 1, 5), + }).then((firstId) => { + F3XAggregationHelpers.createIndependentExpenditureViaUI({ + reportId: result.report, + payeeContactName: result.individual.last_name, + candidate: result.candidate, + amount: 50, + disbursementDate: new Date(currentYear, 4 - 1, 20), + }).then((secondId) => { + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.openRowById(F3XAggregationHelpers.disbursementsTableRoot, secondId); + cy.get('#calendar_ytd').should('have.value', '$150.00'); + F3XAggregationHelpers.clickSave(); + + F3XAggregationHelpers.clickRowActionById(F3XAggregationHelpers.disbursementsTableRoot, firstId, 'Delete'); + F3XAggregationHelpers.confirmDialog(); + + F3XAggregationHelpers.openRowById(F3XAggregationHelpers.disbursementsTableRoot, secondId); + cy.get('#calendar_ytd').should('have.value', '$50.00'); + }); + }); + }); + }); }); diff --git a/front-end/cypress/e2e-smoke/F3X/aggregate-schedule-f.cy.ts b/front-end/cypress/e2e-smoke/F3X/aggregate-schedule-f.cy.ts index da7de53755..5b52f93658 100644 --- a/front-end/cypress/e2e-smoke/F3X/aggregate-schedule-f.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/aggregate-schedule-f.cy.ts @@ -7,6 +7,7 @@ import { DataSetup } from './setup'; import { ContactLookup } from '../pages/contactLookup'; import { ReportListPage } from '../pages/reportListPage'; import { StartTransaction } from './utils/start-transaction/start-transaction'; +import { F3XAggregationHelpers } from '../../e2e-extended/f3x/f3x-aggregation.helpers'; function generateReportAndContacts(transData: [number, string, boolean][]) { return cy @@ -226,4 +227,27 @@ describe('Tests transaction form aggregate calculation', () => { cy.get('[id=aggregate_general_elec_expended]').should('have.value', '$65.00'); }); }); + + it('schedule F delete middle transaction reaggregates downstream aggregate_general_elec_expended', () => { + setCommitteeToPTY(); + cy.wrap(DataSetup({ individual: true, candidate: true, committee: true })).then((result: any) => { + F3XAggregationHelpers.seedScheduleFChain(result.report, result.individual, result.candidate, result.committee, [ + { amount: 100, date: `${currentYear}-04-10` }, + { amount: 50, date: `${currentYear}-04-15` }, + { amount: 25, date: `${currentYear}-04-20` }, + ]).then((transactionIds) => { + const [, middleId, finalId] = transactionIds; + + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.openRowById(F3XAggregationHelpers.disbursementsTableRoot, finalId); + F3XAggregationHelpers.assertScheduleFAggregateField('$175.00'); + F3XAggregationHelpers.clickSave(); + + F3XAggregationHelpers.deleteRowById(F3XAggregationHelpers.disbursementsTableRoot, middleId); + + F3XAggregationHelpers.openRowById(F3XAggregationHelpers.disbursementsTableRoot, finalId); + F3XAggregationHelpers.assertScheduleFAggregateField('$125.00'); + }); + }); + }); }); diff --git a/front-end/cypress/e2e-smoke/F3X/debts.cy.ts b/front-end/cypress/e2e-smoke/F3X/debts.cy.ts index 9b53fee327..1a406fb6d5 100644 --- a/front-end/cypress/e2e-smoke/F3X/debts.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/debts.cy.ts @@ -12,6 +12,7 @@ import { makeTransaction } from '../requests/methods'; import { ReportListPage } from '../pages/reportListPage'; import { defaultForm3XData } from '../models/ReportFormModel'; import { defaultScheduleFormData } from '../models/TransactionFormModel' +import { F3XAggregationHelpers } from '../../e2e-extended/f3x/f3x-aggregation.helpers'; function setupCoordinatedPartyExpenditure( organization: ContactFormData, @@ -45,7 +46,7 @@ function createDebtRepaymentCallback(result: any) { PageUtils.urlCheck('select/disbursement?debt='); cy.contains('CONTRIBUTIONS/EXPENDITURES TO/ON BEHALF OF REGISTERED FILERS').should('exist'); PageUtils.clickAccordion('CONTRIBUTIONS/EXPENDITURES TO/ON BEHALF OF REGISTERED FILERS'); - cy.contains('Coordinated Party Expenditure').click({ force: true }); + cy.contains('Coordinated Party Expenditure').click(); setupCoordinatedPartyExpenditure(result.organization, result.committee, result.candidate); @@ -82,7 +83,7 @@ describe('Debts', () => { cy.contains('Debt Owed By Committee').should('exist'); PageUtils.clickElement('loans-and-debts-button'); - cy.contains('Report debt repayment').click({ force: true }); + cy.contains('Report debt repayment').click(); PageUtils.urlCheck('select/disbursement?debt='); cy.contains('CONTRIBUTIONS/EXPENDITURES TO/ON BEHALF OF REGISTERED FILERS').should('exist'); PageUtils.clickAccordion('CONTRIBUTIONS/EXPENDITURES TO/ON BEHALF OF REGISTERED FILERS'); @@ -233,6 +234,85 @@ describe('Debts', () => { }); }); + it('deleting a debt repayment recalculates debt balance_at_close', () => { + cy.wrap(DataSetup({ committee: true, individual: true })).then((result: any) => { + ReportListPage.goToReportList(result.report); + StartTransaction.Debts().ToCommittee(); + ContactLookup.getCommittee(result.committee); + + cy.intercept({ + method: 'POST', + pathname: '/api/v1/transactions/', + }).as('CreateDebtOwedToCommittee'); + + TransactionDetailPage.enterLoanFormData( + { + ...debtFormData, + amount: 6000, + }, + false, + '', + '#amount', + ); + PageUtils.clickButton('Save'); + + cy.wait('@CreateDebtOwedToCommittee').then((interception) => { + const debtId = F3XAggregationHelpers.transactionIdFromPayload( + interception.response?.body, + 'deleting debt repayment - create debt', + ); + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.clickRowActionById( + F3XAggregationHelpers.loansAndDebtsTableRoot, + debtId, + 'Report debt repayment', + ); + PageUtils.urlCheck('select/receipt?debt='); + PageUtils.clickAccordion('CONTRIBUTIONS FROM INDIVIDUALS/PERSONS'); + PageUtils.clickLink('Individual Receipt'); + ContactLookup.getContact(result.individual.last_name); + + cy.intercept({ + method: 'POST', + pathname: '/api/v1/transactions/', + }).as('CreateDebtRepaymentReceipt'); + + TransactionDetailPage.enterScheduleFormData( + { + ...defaultScheduleFormData, + electionType: undefined, + electionYear: undefined, + date_received: new Date(currentYear, 4 - 1, 20), + amount: 1000, + }, + false, + '', + true, + 'contribution_date', + ); + PageUtils.clickButton('Save'); + + cy.wait('@CreateDebtRepaymentReceipt').then((repaymentInterception) => { + const repaymentId = F3XAggregationHelpers.transactionIdFromPayload( + repaymentInterception.response?.body, + 'deleting debt repayment - create repayment', + ); + + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.openRowById(F3XAggregationHelpers.loansAndDebtsTableRoot, debtId); + cy.get('#balance_at_close').should('have.value', '$5,000.00'); + F3XAggregationHelpers.clickSave(); + + F3XAggregationHelpers.deleteTransactionById(repaymentId); + F3XAggregationHelpers.goToReport(result.report); + + F3XAggregationHelpers.openRowById(F3XAggregationHelpers.loansAndDebtsTableRoot, debtId); + cy.get('#balance_at_close').should('have.value', '$6,000.00'); + }); + }); + }); + }); + describe('test PTY', () => { beforeEach(() => { ContactListPage.deleteAllContacts(); @@ -246,11 +326,15 @@ describe('Debts', () => { it('should test Debt Owed By Committee loan - Report debt repayment', () => { cy.wrap( DataSetup({ - candidate: true, organization: true, - committee: true, }), - ).then(handleDebtOwedByCommitteeLoanReportDebtRepayment); + ).then((result: any) => { + return F3XAggregationHelpers.createContact(F3XAggregationHelpers.uniqueCommitteeSeed()).then((committee) => { + return F3XAggregationHelpers.createContact(F3XAggregationHelpers.uniqueHouseCandidateSeed()).then((candidate) => { + handleDebtOwedByCommitteeLoanReportDebtRepayment({ ...result, committee, candidate }); + }); + }); + }); }); }); }); diff --git a/front-end/cypress/e2e-smoke/F3X/loans-bank.cy.ts b/front-end/cypress/e2e-smoke/F3X/loans-bank.cy.ts index 5310c313a0..17f1427788 100644 --- a/front-end/cypress/e2e-smoke/F3X/loans-bank.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/loans-bank.cy.ts @@ -35,7 +35,7 @@ function clickLoan(button: string, urlCheck = '/list') { .children() .last() .click(); - cy.contains(button).click({ force: true }); + cy.contains(button).click(); PageUtils.urlCheck(urlCheck); } diff --git a/front-end/cypress/e2e-smoke/F3X/loans-committee.cy.ts b/front-end/cypress/e2e-smoke/F3X/loans-committee.cy.ts index a891312e39..1a512bf61e 100644 --- a/front-end/cypress/e2e-smoke/F3X/loans-committee.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/loans-committee.cy.ts @@ -57,7 +57,7 @@ describe('Loans', () => { cy.contains('Loan By Committee').should('exist'); cy.contains('Loan Made').should('exist'); PageUtils.clickElement('loans-and-debts-button'); - cy.contains('Receive loan repayment').click({ force: true }); + cy.contains('Receive loan repayment').click(); PageUtils.urlCheck('LOAN_REPAYMENT_RECEIVED'); formData.date_received = new Date(currentYear, 4 - 1, 27); diff --git a/front-end/cypress/e2e-smoke/F3X/receipts.cy.ts b/front-end/cypress/e2e-smoke/F3X/receipts.cy.ts index 06200f34a5..f13fefff7e 100644 --- a/front-end/cypress/e2e-smoke/F3X/receipts.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/receipts.cy.ts @@ -9,6 +9,8 @@ import { DataSetup } from './setup'; import { StartTransaction } from './utils/start-transaction/start-transaction'; import { ContactLookup } from '../pages/contactLookup'; import { ReportListPage } from '../pages/reportListPage'; +import { buildScheduleA } from '../requests/library/transactions'; +import { F3XAggregationHelpers } from '../../e2e-extended/f3x/f3x-aggregation.helpers'; const scheduleData = { ...defaultScheduleFormData, @@ -172,7 +174,12 @@ describe('Receipt Transactions', () => { PageUtils.clickButton('Cancel'); PageUtils.urlCheck('/list'); // Check form values of memo form - PageUtils.clickLink('Partnership Attribution'); + cy.get(`${F3XAggregationHelpers.receiptsTableRoot} tbody tr`) + .contains('td', 'Partnership Attribution') + .first() + .closest('tr') + .as('partnershipAttributionRow'); + PageUtils.clickLink('Partnership Attribution', '@partnershipAttributionRow'); cy.get('#entity_type_dropdown.readonly').should('exist'); cy.get('#entity_type_dropdown').should('contain', 'Individual'); ContactListPage.assertFormData(individual, true); @@ -515,6 +522,71 @@ describe('Receipt Transactions', () => { }); }); + it('schedule A row action Itemize then Unitemize persists status after reload', () => { + cy.wrap(DataSetup({ individual: true })).then((result: any) => { + F3XAggregationHelpers.createTransaction( + buildScheduleA('INDIVIDUAL_RECEIPT', 100, `${currentYear}-04-12`, result.individual, result.report), + ).then((created) => { + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.assertReceiptRowStatus(created.id, 'Unitemized', true); + + F3XAggregationHelpers.itemizeRowById(F3XAggregationHelpers.receiptsTableRoot, created.id); + F3XAggregationHelpers.assertReceiptRowStatus(created.id, 'Unitemized', false); + F3XAggregationHelpers.assertStatusPersistsAfterReload( + result.report, + F3XAggregationHelpers.receiptsTableRoot, + created.id, + 'Unitemized', + false, + ); + + F3XAggregationHelpers.unitemizeRowById(F3XAggregationHelpers.receiptsTableRoot, created.id); + F3XAggregationHelpers.assertReceiptRowStatus(created.id, 'Unitemized', true); + F3XAggregationHelpers.assertStatusPersistsAfterReload( + result.report, + F3XAggregationHelpers.receiptsTableRoot, + created.id, + 'Unitemized', + true, + ); + }); + }); + }); + + it('schedule A row action Unaggregate then Aggregate persists and restores aggregate chain behavior', () => { + cy.wrap(DataSetup({ individual: true })).then((result: any) => { + F3XAggregationHelpers.seedScheduleAChain(result.report, result.individual, [ + { amount: 100, date: `${currentYear}-04-12` }, + { amount: 75, date: `${currentYear}-04-20` }, + ]).then((transactionIds) => { + const [, secondId] = transactionIds; + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.assertReceiptAggregate(secondId, '$175.00'); + + F3XAggregationHelpers.unaggregateRowById(F3XAggregationHelpers.receiptsTableRoot, secondId); + F3XAggregationHelpers.assertReceiptRowStatus(secondId, 'Unaggregated', true); + F3XAggregationHelpers.assertStatusPersistsAfterReload( + result.report, + F3XAggregationHelpers.receiptsTableRoot, + secondId, + 'Unaggregated', + true, + ); + + F3XAggregationHelpers.aggregateRowById(F3XAggregationHelpers.receiptsTableRoot, secondId); + F3XAggregationHelpers.assertReceiptRowStatus(secondId, 'Unaggregated', false); + F3XAggregationHelpers.assertReceiptAggregate(secondId, '$175.00'); + F3XAggregationHelpers.assertStatusPersistsAfterReload( + result.report, + F3XAggregationHelpers.receiptsTableRoot, + secondId, + 'Unaggregated', + false, + ); + }); + }); + }); + it('Committee Fields Display Properly', () => { cy.wrap(DataSetup({ committee: true })).then((result: any) => { const committee = result.committee; diff --git a/front-end/cypress/e2e-smoke/pages/contactLookup.ts b/front-end/cypress/e2e-smoke/pages/contactLookup.ts index 7bf6b86672..6fb752cd03 100644 --- a/front-end/cypress/e2e-smoke/pages/contactLookup.ts +++ b/front-end/cypress/e2e-smoke/pages/contactLookup.ts @@ -2,15 +2,24 @@ import { ContactFormData } from '../models/ContactFormModel'; import { PageUtils } from './pageUtils'; export class ContactLookup { + private static readonly autocompleteInputSelector = + '[data-cy="searchBox"] input.p-autocomplete-input:visible:not([readonly]):not([disabled])'; + static getContact(name: string, alias = '', type: string | undefined = undefined, index=0) { alias = PageUtils.getAlias(alias); if (type !== undefined) { PageUtils.pSelectDropdownSetValue('#entity_type_dropdown', type, alias, index); cy.contains('LOOKUP').should('exist'); } - cy.get(alias).find('[data-cy="searchBox"]').eq(index).type(name.slice(0, 3)); - cy.contains(name).should('exist').as('contactName'); - cy.get('@contactName').click({ force: true }); + cy.get(alias) + .find(this.autocompleteInputSelector) + .eq(index) + .clear() + .type(name.slice(0, 3)); + cy.get('.p-autocomplete-list-container:visible') + .contains('.p-autocomplete-option', name) + .first() + .click(); } static getCandidate( @@ -18,10 +27,12 @@ export class ContactLookup { excludeFecIds: string[], excludeIds: string[], alias = '', - change = false, + _change = false, ) { + void _change; const lastName = contact['last_name']; if (!lastName) return; + alias = PageUtils.getAlias(alias); const nameEntry = lastName.slice(0, 3); cy.intercept( 'GET', @@ -34,14 +45,11 @@ export class ContactLookup { }, }, ); - const candidateSection = cy.get(alias); - candidateSection.find('[data-cy="searchBox"]').type(nameEntry); - candidateSection - .get('.p-autocomplete-list-container') - .contains(nameEntry) - .then(($name) => { - cy.wrap($name).click(); - }); + cy.get(alias).find(this.autocompleteInputSelector).first().clear().type(nameEntry); + cy.get('.p-autocomplete-list-container:visible') + .contains('.p-autocomplete-option', nameEntry) + .first() + .click(); } static getCommittee( diff --git a/front-end/cypress/e2e-smoke/pages/f3xCreateReportPage.ts b/front-end/cypress/e2e-smoke/pages/f3xCreateReportPage.ts index 76b22e2cb8..a1de36f16b 100644 --- a/front-end/cypress/e2e-smoke/pages/f3xCreateReportPage.ts +++ b/front-end/cypress/e2e-smoke/pages/f3xCreateReportPage.ts @@ -7,7 +7,7 @@ export class F3xCreateReportPage { cy.get("[data-cy='report-type-category']").contains(formData['report_type_category']).click(); - cy.get(`#${formData['report_code']}`).click({ force: true }); + cy.get(`#${formData['report_code']}`).click(); if (['12G', '30G', '12P', '12R', '12S', '12C', '30R', '30S'].includes(formData['report_code'])) { PageUtils.calendarSetValue('[data-cy="date_of_election"]', new Date(formData['date_of_election'])); diff --git a/front-end/cypress/e2e-smoke/pages/pageUtils.ts b/front-end/cypress/e2e-smoke/pages/pageUtils.ts index 508b6c8881..def6f1d924 100644 --- a/front-end/cypress/e2e-smoke/pages/pageUtils.ts +++ b/front-end/cypress/e2e-smoke/pages/pageUtils.ts @@ -1,9 +1,46 @@ export const currentYear = new Date().getFullYear(); export class PageUtils { + private static readonly monthNames: string[] = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; + + private static exactText(value: string): RegExp { + return new RegExp(`^\\s*${Cypress._.escapeRegExp(value)}\\s*$`); + } + + private static normalizeSidebarLabel(value: string): string { + return value.replace(/\s+/g, ' ').trim().toLowerCase(); + } + + private static isSidebarHeaderExpanded($header: JQuery): boolean { + const ariaExpanded = ($header.attr('aria-expanded') ?? '').toLowerCase(); + if (ariaExpanded === 'true') return true; + if (ariaExpanded === 'false') return false; + + const dataHighlight = ($header.attr('data-p-highlight') ?? '').toLowerCase(); + return dataHighlight === 'true' || $header.hasClass('p-highlight'); + } + static closeToast() { const alias = PageUtils.getAlias(''); - cy.get(alias).find('.p-toast-close-button').should('exist').click(); + cy.get(alias).then(($root) => { + const closeButton = $root.find('.p-toast-close-button:visible').first(); + if (closeButton.length > 0) { + cy.wrap(closeButton).click(); + } + }); } static clickElement(elementSelector: string, alias = '') { @@ -31,7 +68,11 @@ export class PageUtils { if (value) { cy.get(alias).find(querySelector).eq(index).click(); - cy.contains('p-selectitem', value) + cy.get('.p-select-overlay:visible') + .find('[role="option"]') + .filter((_, option) => PageUtils.exactText(value).test(option.textContent ?? '')) + .should('have.length', 1) + .first() .scrollIntoView({ offset: { top: 0, left: 0 } }) .click(); } @@ -40,60 +81,62 @@ export class PageUtils { static calendarSetValue(calendar: string, dateObj: Date = new Date(), alias = '') { alias = PageUtils.getAlias(alias); cy.get(alias).find(calendar).first().click(); - cy.get('body').find('.p-datepicker-panel').as('calendarElement'); - - PageUtils.pickYear(dateObj.getFullYear()); - PageUtils.pickMonth(dateObj.getMonth()); + cy.get('body').find('.p-datepicker-panel:visible').first().as('calendarElement'); + PageUtils.navigateCalendarTo(dateObj); PageUtils.pickDay(dateObj.getDate().toString()); cy.wait(100); } + private static monthIndex(monthText: string): number { + const monthPrefix = monthText.trim().toLowerCase().slice(0, 3); + return PageUtils.monthNames.findIndex((monthName) => monthName.toLowerCase().startsWith(monthPrefix)); + } + + static navigateCalendarTo(targetDate: Date) { + cy.get('@calendarElement') + .find('.p-datepicker-select-month:visible') + .first() + .invoke('text') + .then((displayedMonthText) => { + cy.get('@calendarElement') + .find('.p-datepicker-select-year:visible') + .first() + .invoke('text') + .then((displayedYearText) => { + const displayedMonth = PageUtils.monthIndex(displayedMonthText); + const displayedYear = Number(displayedYearText.trim()); + if (displayedMonth < 0 || Number.isNaN(displayedYear)) { + throw new Error( + `Unable to resolve displayed calendar month/year from "${displayedMonthText}" and "${displayedYearText}"`, + ); + } + + const targetMonthIndex = targetDate.getFullYear() * 12 + targetDate.getMonth(); + const displayedMonthIndex = displayedYear * 12 + displayedMonth; + const monthHops = targetMonthIndex - displayedMonthIndex; + if (monthHops === 0) { + return; + } + + const navButton = monthHops > 0 ? '.p-datepicker-next-button:visible' : '.p-datepicker-prev-button:visible'; + for (let i = 0; i < Math.abs(monthHops); i += 1) { + cy.get('@calendarElement').find(navButton).first().click(); + } + }); + }); + } + static pickDay(day: string) { - cy.get('@calendarElement').find('td').find('span').not('.p-disabled').parent().contains(day).click(); cy.get('@calendarElement') .find('td') .find('span') .not('.p-disabled') .parent() - .contains(day) - .then(($day) => { - cy.wrap($day.parent()).click(); - }); - } - - static pickMonth(month: number) { - const Months: Array = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - const Month: string = Months[month]; - cy.get('@calendarElement').find('.p-datepicker-month').contains(Month).click({ force: true }); - } - - static pickYear(year: number) { - const currentYear: number = new Date().getFullYear(); - - cy.get('@calendarElement').find('.p-datepicker-select-year').should('be.visible').click({ force: true }); - cy.wait(100); - cy.get('@calendarElement').then(($calendarElement) => { - if ($calendarElement.find('.p-datepicker-select-year:visible').length > 0) { - cy.get('@calendarElement').find('.p-datepicker-select-year').click({ force: true }); - } - }); - cy.get('@calendarElement').find('.p-datepicker-decade').should('be.visible'); - - const decadeStart: number = currentYear - (currentYear % 10); - const decadeEnd: number = decadeStart + 9; - if (year < decadeStart) { - for (let i = 0; i < decadeStart - year; i += 10) { - cy.get('@calendarElement').find('.p-datepicker-prev-button').click(); - } - } - if (year > decadeEnd) { - for (let i = 0; i < year - decadeEnd; i += 10) { - cy.get('@calendarElement').find('.p-datepicker-next-button').click(); - } - } - cy.get('body').find('.p-datepicker-year').contains(year.toString()).should('be.visible').click({ force: true }); + .contains(PageUtils.exactText(day)) + .first() + .click(); } static clickSidebarSection(section: string) { @@ -102,8 +145,83 @@ export class PageUtils { } static clickSidebarItem(menuItem: string) { - cy.get('p-panelmenu').contains('a', menuItem).as('menuItem'); - cy.get('@menuItem').click({ force: true }); + const normalizedMenuItem = PageUtils.normalizeSidebarLabel(menuItem); + const menuLabelSelector = '.p-panelmenu-header-label, .p-panelmenu-item-label'; + + const toMenuLink = ($label: JQuery): JQuery => { + const $link = $label.closest('a.p-panelmenu-header-link, a.p-panelmenu-item-link'); + expect($link.length, `sidebar link owner for "${menuItem}"`).to.eq(1); + return $link.first(); + }; + + const findMenuLabel = (visibleOnly: boolean): Cypress.Chainable> => { + const selector = visibleOnly ? `${menuLabelSelector}:visible` : menuLabelSelector; + return cy + .get('p-panelmenu') + .find(selector) + .filter((_, label) => PageUtils.normalizeSidebarLabel(label.textContent ?? '') === normalizedMenuItem) + .should(($labels) => { + expect( + $labels.length, + `${visibleOnly ? 'visible ' : ''}sidebar link matches for "${menuItem}"`, + ).to.eq(1); + }) + .first(); + }; + + const findMenuLink = (visibleOnly: boolean): Cypress.Chainable> => + findMenuLabel(visibleOnly).then(($label) => cy.wrap(toMenuLink($label))); + + const ensureVisibleMenuLink = (): Cypress.Chainable> => + findMenuLabel(false).then(($candidateLabel) => { + if (Cypress.dom.isVisible($candidateLabel[0])) { + return findMenuLink(true); + } + + const $ownerPanel = $candidateLabel.closest('.p-panelmenu-panel'); + expect($ownerPanel.length, `owner sidebar panel for "${menuItem}"`).to.eq(1); + const ownerPanelIndex = $ownerPanel.first().index(); + expect(ownerPanelIndex, `owner sidebar panel index for "${menuItem}"`).to.be.greaterThan(-1); + + const findOwnerHeader = (): Cypress.Chainable> => + cy + .get('p-panelmenu') + .find('.p-panelmenu-panel') + .eq(ownerPanelIndex) + .find('> .p-panelmenu-header') + .should('have.length', 1) + .first(); + + return findOwnerHeader().then(($ownerHeader) => { + if (PageUtils.isSidebarHeaderExpanded($ownerHeader)) { + return findMenuLink(true); + } + + findOwnerHeader() + .find('a.p-panelmenu-header-link') + .should('have.length', 1) + .first() + .click(); + + findOwnerHeader().should(($header) => { + expect(PageUtils.isSidebarHeaderExpanded($header), `owner sidebar header expanded for "${menuItem}"`).to.eq( + true, + ); + }); + + return findMenuLink(true); + }); + }); + + ensureVisibleMenuLink().then(($menuLink) => { + const isHeaderLink = $menuLink.hasClass('p-panelmenu-header-link'); + const $header = $menuLink.closest('.p-panelmenu-header'); + const shouldClick = !isHeaderLink || !PageUtils.isSidebarHeaderExpanded($header); + + if (shouldClick) { + findMenuLink(true).click(); + } + }); } static shouldHaveSidebarItem(menuItem: string) { @@ -126,7 +244,13 @@ export class PageUtils { static clickLink(name: string, alias = '') { alias = PageUtils.getAlias(alias); - cy.get(alias).contains('a', name).click(); + const exactName = PageUtils.exactText(name.trim()); + cy.get(alias) + .find('a:visible') + .filter((_, link) => exactName.test(link.textContent ?? '')) + .first() + .should('exist') + .click(); } static clickAccordion(name: string, alias = '') { diff --git a/front-end/cypress/e2e-smoke/pages/transactionDetailPage.ts b/front-end/cypress/e2e-smoke/pages/transactionDetailPage.ts index 28d07221a7..9934fabb94 100644 --- a/front-end/cypress/e2e-smoke/pages/transactionDetailPage.ts +++ b/front-end/cypress/e2e-smoke/pages/transactionDetailPage.ts @@ -27,9 +27,17 @@ export class TransactionDetailPage { } if (formData.candidate) { - cy.get('.contact-lookup-container').last().get('[data-cy="searchBox"]').type(formData.candidate); - cy.contains(formData.candidate).should('exist'); - cy.contains(formData.candidate).click(); + cy.get(alias) + .find('.contact-lookup-container') + .last() + .find('[data-cy="searchBox"] input.p-autocomplete-input') + .first() + .clear() + .type(formData.candidate); + cy.get('.p-autocomplete-list-container:visible') + .contains('.p-autocomplete-option', formData.candidate) + .first() + .click(); } this.enterCommon(formData, alias); @@ -56,7 +64,18 @@ export class TransactionDetailPage { } if (formData.supportOpposeCode) { - cy.get("[data-cy='support_oppose_code']").contains(formData.supportOpposeCode).click(); + const supportOpposeCode = formData.supportOpposeCode.toString().trim().toUpperCase(); + const supportOpposeOptionId = + supportOpposeCode === 'SUPPORT' || supportOpposeCode === 'S' + ? '#support' + : supportOpposeCode === 'OPPOSE' || supportOpposeCode === 'O' + ? '#oppose' + : ''; + if (!supportOpposeOptionId) { + throw new Error(`Unsupported support/oppose code: ${formData.supportOpposeCode}`); + } + + cy.get(alias).find(`[data-cy='support_oppose_code'] ${supportOpposeOptionId}`).first().click(); ContactLookup.getCandidate(contactData, [], [], '#contact_2_lookup'); } @@ -118,12 +137,13 @@ export class TransactionDetailPage { static enterLoanFormData( formData: LoanFormData, - readOnlyAmount = false, + _readOnlyAmount = false, alias = '', amountField = '#loan-info-amount', dateField = 'expenditure_date', dateIncurredField = 'loan_incurred_date', ) { + void _readOnlyAmount; alias = PageUtils.getAlias(alias); cy.get(alias).find(amountField).safeType(formData.amount); @@ -136,7 +156,7 @@ export class TransactionDetailPage { } if (formData.secured) { - cy.get(alias).find('input[name="secured"]').first().click({ force: true }); + cy.get(alias).find('input[name="secured"]').first().click(); } // Set due date dropdown & date @@ -160,31 +180,32 @@ export class TransactionDetailPage { static enterLoanFormDataStepTwo( formData: LoanFormData, - readOnlyAmount = false, + _readOnlyAmount = false, alias = '', dateSigned1 = 'treasurer_date_signed', dateSigned2 = 'authorized_date_signed', ) { + void _readOnlyAmount; alias = PageUtils.getAlias(alias); if (formData.loan_restructured) { - cy.get(alias).find('input[name="loan_restructured"]').first().click({ force: true }); + cy.get(alias).find('input[name="loan_restructured"]').first().click(); } if (formData.line_of_credit) { - cy.get(alias).find('input#line_of_credit').first().click({ force: true }); + cy.get(alias).find('input#line_of_credit').first().click(); } if (formData.others_liable) { - cy.get(alias).find('input[name="others_liable"]').first().click({ force: true }); + cy.get(alias).find('input[name="others_liable"]').first().click(); } if (formData.collateral) { - cy.get(alias).find('input[name="collateral"]').first().click({ force: true }); + cy.get(alias).find('input[name="collateral"]').first().click(); } if (formData.future_income) { - cy.get(alias).find('input[name="future_income"]').first().click({ force: true }); + cy.get(alias).find('input[name="future_income"]').first().click(); } if (formData.last_name) { @@ -270,7 +291,7 @@ export class TransactionDetailPage { 'GET', `http://localhost:8080/api/v1/transactions/?page=1&ordering=line_label,created&page_size=5&report_id=${reportId}&schedules=B,E,F`, ).as('GetDisbursements'); - cy.contains(/^Save$/).click(); + cy.get('button[data-cy="navigation-control-button"]').contains(/^Save$/).first().click(); cy.wait('@GetLoans'); cy.wait('@GetDisbursements'); @@ -345,7 +366,12 @@ export class TransactionDetailPage { private static enterPurpose(formData: ScheduleFormData, alias: string) { if (formData.purpose_description) { - cy.get(alias).find('textarea#purpose_description').first().safeType(formData.purpose_description); + cy.get(alias).then(($root) => { + const purposeInput = $root.find('textarea#purpose_description:visible:not([readonly]):not([disabled])').first(); + if (purposeInput.length > 0) { + cy.wrap(purposeInput).safeType(formData.purpose_description!); + } + }); } } @@ -354,4 +380,4 @@ export class TransactionDetailPage { this.enterPurpose(formData, alias); this.enterCategoryCode(formData, alias); } -} \ No newline at end of file +} diff --git a/front-end/cypress/e2e-smoke/requests/methods.ts b/front-end/cypress/e2e-smoke/requests/methods.ts index 01178df919..2e1959f27a 100644 --- a/front-end/cypress/e2e-smoke/requests/methods.ts +++ b/front-end/cypress/e2e-smoke/requests/methods.ts @@ -2,17 +2,80 @@ import { MockContact } from './library/contacts'; import { F3X, F24 } from './library/reports'; -export function makeF3x(f3x: F3X, callback = (response: Cypress.Response) => {}) { - makeRequestToAPI( - 'POST', - 'http://localhost:8080/api/v1/reports/form-3x/?fields_to_validate=filing_frequency', - f3x, - callback, +function isReportCodeCollision(response: Cypress.Response): boolean { + const messages = response.body?.report_code; + return ( + response.status === 400 && + Array.isArray(messages) && + messages.some((message: unknown) => String(message).includes('Collision with existing report_code and year')) ); } -export function makeF24(f24: F24, callback = (response: Cypress.Response) => {}) { - makeRequestToAPI( +export function makeF3x( + f3x: F3X, + callback: (response: Cypress.Response) => void | Cypress.Chainable = () => {}, +) { + const createEndpoint = 'http://localhost:8080/api/v1/reports/form-3x/?fields_to_validate=filing_frequency'; + const reportsListEndpoint = 'http://localhost:8080/api/v1/reports/?page=1&ordering=-coverage_through_date&page_size=100'; + + const useExistingConflictingReport = (): Cypress.Chainable> => { + return makeRequestToAPI('GET', reportsListEndpoint, undefined, () => {}, false).then((listResponse) => { + const reports = listResponse.body?.results ?? []; + const conflictingReport = reports.find((report: any) => { + const sameCode = report?.report_code === f3x.report_code; + const sameYear = String(report?.coverage_through_date ?? '').startsWith( + String(f3x.coverage_through_date ?? '').slice(0, 4), + ); + return sameCode && sameYear; + }); + + if (!conflictingReport?.id) { + throw new Error('makeF3x could not resolve report_code collision: conflicting report id not found'); + } + + return makeRequestToAPI( + 'GET', + `http://localhost:8080/api/v1/reports/${conflictingReport.id}/`, + undefined, + () => {}, + false, + ).then((existingReportResponse) => { + if (existingReportResponse.status >= 200 && existingReportResponse.status < 300) { + callback(existingReportResponse); + return cy.wrap(existingReportResponse, { log: false }); + } + + throw new Error( + `makeF3x collision fallback could not fetch existing report id ${conflictingReport.id}: ` + + `status ${existingReportResponse.status} ${JSON.stringify(existingReportResponse.body)}`, + ); + }); + }); + }; + + const attemptCreate = (): Cypress.Chainable> => { + return makeRequestToAPI('POST', createEndpoint, f3x, () => {}, false).then((response) => { + if (response.status >= 200 && response.status < 300) { + callback(response); + return cy.wrap(response, { log: false }); + } + + if (isReportCodeCollision(response)) { + return useExistingConflictingReport(); + } + + throw new Error(`makeF3x failed with status ${response.status}: ${JSON.stringify(response.body)}`); + }); + }; + + return attemptCreate(); +} + +export function makeF24( + f24: F24, + callback: (response: Cypress.Response) => void | Cypress.Chainable = () => {}, +) { + return makeRequestToAPI( 'POST', 'http://localhost:8080/api/v1/reports/form-24/?fields_to_validate=report_type_24_48', f24, @@ -20,12 +83,18 @@ export function makeF24(f24: F24, callback = (response: Cypress.Response) = ); } -export function makeContact(contact: MockContact, callback = (response: Cypress.Response) => {}) { - makeRequestToAPI('POST', 'http://localhost:8080/api/v1/contacts/', contact, callback); +export function makeContact( + contact: MockContact, + callback: (response: Cypress.Response) => void | Cypress.Chainable = () => {}, +) { + return makeRequestToAPI('POST', 'http://localhost:8080/api/v1/contacts/', contact, callback); } -export function makeTransaction(transaction: any, callback = (response: Cypress.Response) => {}) { - makeRequestToAPI('POST', 'http://localhost:8080/api/v1/transactions/', transaction, callback); +export function makeTransaction( + transaction: any, + callback: (response: Cypress.Response) => void | Cypress.Chainable = () => {}, +) { + return makeRequestToAPI('POST', 'http://localhost:8080/api/v1/transactions/', transaction, callback); } function makeRequestToAPI( @@ -33,25 +102,33 @@ function makeRequestToAPI( url: string, body: any, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - callback = (response: Cypress.Response) => {}, -) { - cy.getAllCookies().then((cookies: Cypress.ObjectLike[]) => { + callback: (response: Cypress.Response) => void | Cypress.Chainable = () => {}, + failOnStatusCode = true, +) : Cypress.Chainable> { + return cy.getAllCookies().then((cookies: Cypress.ObjectLike[]) => { let cookie_obj: any = {}; + const cookieHeader = cookies + .map((cookie: Cypress.ObjectLike) => `${cookie['name']}=${cookie['value']}`) + .join('; '); + cookies.forEach((cookie: Cypress.ObjectLike) => { const name = cookie['name']; const value = cookie['value']; cookie_obj = { ...cookie_obj, [name]: value }; }); - cy.request({ + return cy.request({ method: method, url: url, body: body, + failOnStatusCode, headers: { - Cookie: cookie_obj, + Cookie: cookieHeader, 'x-csrftoken': cookie_obj.csrftoken, }, - }).then(callback); + }).then((response) => { + callback(response); + return cy.wrap(response, { log: false }); + }); }); } diff --git a/front-end/package-lock.json b/front-end/package-lock.json index d20be5b7f3..d8abbe3a51 100644 --- a/front-end/package-lock.json +++ b/front-end/package-lock.json @@ -15,7 +15,7 @@ "@ngrx/store": "20.1.0", "@primeuix/themes": "2.0.3", "class-transformer": "0.5.1", - "fecfile-validate": "https://github.com/fecgov/fecfile-validate#7939bbc7b41067f2310874156cdb0fdadbd0fa33", + "fecfile-validate": "https://github.com/fecgov/fecfile-validate#0cf017439a59a08a7bed02e2069045708887209c", "intl-tel-input": "26.5.1", "ngrx-store-localstorage": "20.1.0", "ngx-cookie-service": "20.1.1", @@ -51,7 +51,7 @@ "@typescript-eslint/eslint-plugin": "8.56.0", "@typescript-eslint/parser": "8.56.0", "axe-core": "^4.11.1", - "cypress": "^15.10.0", + "cypress": "^15.11.0", "cypress-axe": "^1.7.0", "cypress-mochawesome-reporter": "4.0.2", "eslint": "9.39.3", @@ -486,22 +486,6 @@ } } }, - "node_modules/@angular-devkit/core/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/@angular-devkit/schematics": { "version": "20.3.18", "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.18.tgz", @@ -3730,7 +3714,9 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "license": "ISC", + "peer": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -3747,7 +3733,9 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3759,7 +3747,9 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3771,13 +3761,17 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" + "dev": true, + "license": "MIT", + "peer": true }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -3794,7 +3788,9 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^6.2.2" }, @@ -3809,7 +3805,9 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -5580,8 +5578,10 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=14" } @@ -7320,9 +7320,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -7720,6 +7720,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": true, "license": "MIT" }, "node_modules/base64-js": { @@ -9629,7 +9630,9 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" + "dev": true, + "license": "MIT", + "peer": true }, "node_modules/ecc-jsbn": { "version": "0.1.2", @@ -9835,7 +9838,6 @@ "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -10562,13 +10564,121 @@ }, "node_modules/fecfile-validate": { "version": "0.0.1", - "resolved": "git+ssh://git@github.com/fecgov/fecfile-validate.git#7939bbc7b41067f2310874156cdb0fdadbd0fa33", - "integrity": "sha512-Jc7rWycXwyOu4siGzbo7yY2ABYybvs8DfOSAFkfbs4NfVl3pKMpmFni0cU9ATyKR3kmyYYwqywwXhwv3NXMbMw==", + "resolved": "git+ssh://git@github.com/fecgov/fecfile-validate.git#0cf017439a59a08a7bed02e2069045708887209c", + "integrity": "sha512-6R3Gpx9yRZSYXNysfs3hdcFiu6ftfhlq6Ps1Z5SbJGCWKJYWNQnQIHoOqdEh+dOAizU9i2EuMvu3hIXxtyxZhA==", "hasInstallScript": true, "license": "CC0-1.0", "dependencies": { - "ajv": "8.17.1", - "glob": "10.5.0" + "ajv": "8.18.0", + "glob": "12.0.0" + } + }, + "node_modules/fecfile-validate/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/fecfile-validate/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/fecfile-validate/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/fecfile-validate/node_modules/glob": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz", + "integrity": "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fecfile-validate/node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fecfile-validate/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/fecfile-validate/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fecfile-validate/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/figures": { @@ -10958,7 +11068,9 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, "license": "ISC", + "peer": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -11014,7 +11126,9 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -11023,7 +11137,9 @@ "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^2.0.2" }, @@ -11550,7 +11666,6 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", - "dev": true, "license": "MIT", "optional": true, "bin": { @@ -12072,7 +12187,9 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, "license": "BlueOak-1.0.0", + "peer": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -12775,7 +12892,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12790,7 +12906,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, "license": "MIT", "optional": true, "bin": { @@ -12804,7 +12919,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -12815,7 +12929,6 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, "license": "ISC", "optional": true, "bin": { @@ -12826,7 +12939,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "license": "BSD-3-Clause", "optional": true, "engines": { @@ -14268,7 +14380,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -14286,7 +14397,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -15254,7 +15364,9 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "license": "BlueOak-1.0.0", + "peer": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -15270,7 +15382,9 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" + "dev": true, + "license": "ISC", + "peer": true }, "node_modules/path-to-regexp": { "version": "8.3.0", @@ -15658,7 +15772,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", - "dev": true, "license": "MIT", "optional": true }, @@ -16280,7 +16393,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", - "dev": true, "license": "BlueOak-1.0.0", "optional": true, "engines": { @@ -17121,7 +17233,9 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -17135,7 +17249,9 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -17166,7 +17282,9 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -18753,7 +18871,9 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", diff --git a/front-end/package.json b/front-end/package.json index e03daf2048..e415397366 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -71,7 +71,7 @@ "@typescript-eslint/eslint-plugin": "8.56.0", "@typescript-eslint/parser": "8.56.0", "axe-core": "^4.11.1", - "cypress": "^15.10.0", + "cypress": "^15.11.0", "cypress-axe": "^1.7.0", "cypress-mochawesome-reporter": "4.0.2", "eslint": "9.39.3", @@ -98,4 +98,4 @@ "browser": { "crypto": false } -} \ No newline at end of file +} From 2d53cca97d0a1b241be9170c39130701d5bbacf8 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Fri, 6 Mar 2026 02:20:41 -0500 Subject: [PATCH 02/69] follow up chjanges to mitigate failures in F3X/review-report.cy.ts, F3X/reports-f3x.cy.ts, F1M/f1m-affiliation.cy.ts --- front-end/cypress/README.md | 2 +- .../e2e-extended/contacts/contacts.helpers.ts | 77 ++++++++------- .../contacts/contacts.transactions.cy.ts | 5 +- .../cypress/e2e-smoke/pages/loginPage.ts | 5 +- .../cypress/e2e-smoke/pages/pageUtils.ts | 97 +++++++++++-------- .../cypress/e2e-smoke/pages/reportListPage.ts | 12 ++- 6 files changed, 118 insertions(+), 80 deletions(-) diff --git a/front-end/cypress/README.md b/front-end/cypress/README.md index 4f30c525a0..a3e4f8ec9e 100644 --- a/front-end/cypress/README.md +++ b/front-end/cypress/README.md @@ -53,7 +53,7 @@ ## Frontend setup for E2E - Cypress `baseUrl` is `http://localhost:4200` and is set in `cypress.config.ts`. -- Report submission helpers read `Cypress.env('FILING_PASSWORD')`. To supply it locally, set `CYPRESS_FILING_PASSWORD` before running Cypress. +- Report submission helpers read the Cypress `FILING_PASSWORD` env var via `cy.env()`. To supply it locally, set `CYPRESS_FILING_PASSWORD` before running Cypress. ### Env var tips (.env, .zshrc) - Local one-off (current shell): diff --git a/front-end/cypress/e2e-extended/contacts/contacts.helpers.ts b/front-end/cypress/e2e-extended/contacts/contacts.helpers.ts index ea41f93b17..d03fd98ba5 100644 --- a/front-end/cypress/e2e-extended/contacts/contacts.helpers.ts +++ b/front-end/cypress/e2e-extended/contacts/contacts.helpers.ts @@ -596,44 +596,53 @@ export class ContactsHelpers { .find('table[role="table"]', { timeout: 15000 }) .first() .should('exist') - .then(($table) => { - const typeIdx = getColIndexByThIdOrText($table, colIds.type, 'Type'); - expect(typeIdx, 'Type column index').to.be.gte(0); - - const formIdx = row.form ? getColIndexByThIdOrText($table, colIds.form, 'Form') : -1; - const reportIdx = row.report ? getColIndexByThIdOrText($table, colIds.report, 'Report') : -1; - const dateIdx = row.date ? getColIndexByThIdOrText($table, colIds.date, 'Date') : -1; - const amountIdx = - row.amount === undefined - ? -1 - : getColIndexByThIdOrText($table, colIds.amount, 'Amount'); - - const $rows = $table.find('tbody tr'); - expect($rows.length, 'transaction history row count').to.be.greaterThan(0); - - const rowIndex = $rows.toArray().findIndex((tr) => { - const tds = Array.from(tr.querySelectorAll('td')); - const cell = tds[typeIdx]; - return !!cell && typeRx.test(normalize(cell.textContent ?? '')); - }); + .as('transactionHistoryTable') + .then(() => + cy + .get('@transactionHistoryTable') + .find('tbody tr', { timeout: 15000 }) + .should(($rows) => { + expect($rows.length, 'transaction history row count').to.be.greaterThan(0); + }), + ) + .then(() => + cy.get('@transactionHistoryTable').then(($table) => { + const typeIdx = getColIndexByThIdOrText($table, colIds.type, 'Type'); + expect(typeIdx, 'Type column index').to.be.gte(0); + + const formIdx = row.form ? getColIndexByThIdOrText($table, colIds.form, 'Form') : -1; + const reportIdx = row.report ? getColIndexByThIdOrText($table, colIds.report, 'Report') : -1; + const dateIdx = row.date ? getColIndexByThIdOrText($table, colIds.date, 'Date') : -1; + const amountIdx = + row.amount === undefined + ? -1 + : getColIndexByThIdOrText($table, colIds.amount, 'Amount'); + + const $rows = $table.find('tbody tr'); + const rowIndex = $rows.toArray().findIndex((tr) => { + const tds = Array.from(tr.querySelectorAll('td')); + const cell = tds[typeIdx]; + return !!cell && typeRx.test(normalize(cell.textContent ?? '')); + }); - expect(rowIndex, `row index for type ${typeRx}`).to.be.gte(0); + expect(rowIndex, `row index for type ${typeRx}`).to.be.gte(0); - const $row = $rows.eq(rowIndex); - const $tds = $row.find('td'); + const $row = $rows.eq(rowIndex); + const $tds = $row.find('td'); - const assertCell = (idx: number, rx: RegExp, label: string) => { - expect(idx, `${label} column index`).to.be.gte(0); - const text = normalize($tds.eq(idx).text()); - expect(text, `${label} cell text`).to.match(rx); - }; + const assertCell = (idx: number, rx: RegExp, label: string) => { + expect(idx, `${label} column index`).to.be.gte(0); + const text = normalize($tds.eq(idx).text()); + expect(text, `${label} cell text`).to.match(rx); + }; - assertCell(typeIdx, typeRx, 'Type'); - if (formRx) assertCell(formIdx, formRx, 'Form'); - if (reportRx) assertCell(reportIdx, reportRx, 'Report'); - if (dateRx) assertCell(dateIdx, dateRx, 'Date'); - if (amountRx) assertCell(amountIdx, amountRx, 'Amount'); - }) + assertCell(typeIdx, typeRx, 'Type'); + if (formRx) assertCell(formIdx, formRx, 'Form'); + if (reportRx) assertCell(reportIdx, reportRx, 'Report'); + if (dateRx) assertCell(dateIdx, dateRx, 'Date'); + if (amountRx) assertCell(amountIdx, amountRx, 'Amount'); + }), + ) .then(() => cy.wrap(undefined, { log: false })); } diff --git a/front-end/cypress/e2e-extended/contacts/contacts.transactions.cy.ts b/front-end/cypress/e2e-extended/contacts/contacts.transactions.cy.ts index 3407a7e1d4..0acee63ca3 100644 --- a/front-end/cypress/e2e-extended/contacts/contacts.transactions.cy.ts +++ b/front-end/cypress/e2e-extended/contacts/contacts.transactions.cy.ts @@ -552,7 +552,10 @@ describe('Contacts: Transactions integration', () => { cy.get('td').eq(4).should('contain.text', newOccupation); }); - cy.intercept('GET', '**/api/v1/transactions/?**contact=*').as('getContactTxns'); + cy.intercept( + 'GET', + '**/api/v1/transactions/?page=1&ordering=transaction_type_identifier&page_size=5&contact=*', + ).as('getContactTxns'); PageUtils.clickKababItem(displayName, 'Edit'); cy.contains(/Edit Contact/i).should('exist'); diff --git a/front-end/cypress/e2e-smoke/pages/loginPage.ts b/front-end/cypress/e2e-smoke/pages/loginPage.ts index 8efe394a6c..f41088480d 100644 --- a/front-end/cypress/e2e-smoke/pages/loginPage.ts +++ b/front-end/cypress/e2e-smoke/pages/loginPage.ts @@ -55,7 +55,10 @@ function getLoginIntervalString(sessionDur: number): string { function loginDotGovLogin() { const alias = PageUtils.getAlias(''); - cy.intercept('GET', 'http://localhost:8080/api/v1/oidc/login-redirect').as('GetLoggedIn'); + cy.intercept({ + method: 'GET', + pathname: '/api/v1/oidc/authenticate', + }).as('GetLoggedIn'); cy.intercept('GET', 'http://localhost:8080/api/v1/committees/').as('GetCommitteeAccounts'); cy.intercept('POST', 'http://localhost:8080/api/v1/committees/*/activate/').as('ActivateCommittee'); cy.intercept('GET', 'http://localhost:8080/api/v1/committee-members/').as('GetCommitteeMembers'); diff --git a/front-end/cypress/e2e-smoke/pages/pageUtils.ts b/front-end/cypress/e2e-smoke/pages/pageUtils.ts index def6f1d924..7c11d0d385 100644 --- a/front-end/cypress/e2e-smoke/pages/pageUtils.ts +++ b/front-end/cypress/e2e-smoke/pages/pageUtils.ts @@ -140,37 +140,45 @@ export class PageUtils { } static clickSidebarSection(section: string) { - cy.get('p-panelmenu').contains(section).parent().as('section'); - cy.get('@section').click(); + return PageUtils.clickSidebarItem(section); } static clickSidebarItem(menuItem: string) { const normalizedMenuItem = PageUtils.normalizeSidebarLabel(menuItem); const menuLabelSelector = '.p-panelmenu-header-label, .p-panelmenu-item-label'; - - const toMenuLink = ($label: JQuery): JQuery => { - const $link = $label.closest('a.p-panelmenu-header-link, a.p-panelmenu-item-link'); - expect($link.length, `sidebar link owner for "${menuItem}"`).to.eq(1); - return $link.first(); + const findPanelMenu = (): Cypress.Chainable> => + cy.get('p-panelmenu', { timeout: 15000 }).should('exist'); + const nativeClick = ($link: JQuery, label: string) => { + expect($link.length, `${label} link count for "${menuItem}"`).to.eq(1); + const link = $link.get(0); + expect(link, `${label} link element for "${menuItem}"`).to.exist; + (link as HTMLElement).click(); }; + const clickMenuLink = (visibleOnly: boolean, label: string): Cypress.Chainable => + findMenuLink(visibleOnly) + .should('be.visible') + .then(($link) => nativeClick($link, label)); const findMenuLabel = (visibleOnly: boolean): Cypress.Chainable> => { - const selector = visibleOnly ? `${menuLabelSelector}:visible` : menuLabelSelector; - return cy - .get('p-panelmenu') - .find(selector) - .filter((_, label) => PageUtils.normalizeSidebarLabel(label.textContent ?? '') === normalizedMenuItem) + return findPanelMenu() + .find(menuLabelSelector) + .filter( + (_, label) => + PageUtils.normalizeSidebarLabel(label.textContent ?? '') === normalizedMenuItem && + (!visibleOnly || Cypress.dom.isVisible(label)), + ) .should(($labels) => { expect( $labels.length, `${visibleOnly ? 'visible ' : ''}sidebar link matches for "${menuItem}"`, ).to.eq(1); - }) - .first(); + }); }; const findMenuLink = (visibleOnly: boolean): Cypress.Chainable> => - findMenuLabel(visibleOnly).then(($label) => cy.wrap(toMenuLink($label))); + findMenuLabel(visibleOnly) + .closest('a.p-panelmenu-header-link, a.p-panelmenu-item-link') + .should('have.length', 1); const ensureVisibleMenuLink = (): Cypress.Chainable> => findMenuLabel(false).then(($candidateLabel) => { @@ -184,43 +192,44 @@ export class PageUtils { expect(ownerPanelIndex, `owner sidebar panel index for "${menuItem}"`).to.be.greaterThan(-1); const findOwnerHeader = (): Cypress.Chainable> => - cy - .get('p-panelmenu') + findPanelMenu() .find('.p-panelmenu-panel') .eq(ownerPanelIndex) .find('> .p-panelmenu-header') - .should('have.length', 1) - .first(); + .should('have.length', 1); return findOwnerHeader().then(($ownerHeader) => { if (PageUtils.isSidebarHeaderExpanded($ownerHeader)) { return findMenuLink(true); } - findOwnerHeader() + return findOwnerHeader() .find('a.p-panelmenu-header-link') .should('have.length', 1) - .first() - .click(); - - findOwnerHeader().should(($header) => { - expect(PageUtils.isSidebarHeaderExpanded($header), `owner sidebar header expanded for "${menuItem}"`).to.eq( - true, - ); - }); - - return findMenuLink(true); + .should('be.visible') + .then(($link) => nativeClick($link, 'owner sidebar header')) + .then(() => + findOwnerHeader().should(($header) => { + expect( + PageUtils.isSidebarHeaderExpanded($header), + `owner sidebar header expanded for "${menuItem}"`, + ).to.eq(true); + }), + ) + .then(() => findMenuLink(true)); }); }); - ensureVisibleMenuLink().then(($menuLink) => { + return ensureVisibleMenuLink().then(($menuLink) => { const isHeaderLink = $menuLink.hasClass('p-panelmenu-header-link'); const $header = $menuLink.closest('.p-panelmenu-header'); const shouldClick = !isHeaderLink || !PageUtils.isSidebarHeaderExpanded($header); if (shouldClick) { - findMenuLink(true).click(); + return clickMenuLink(true, 'sidebar target'); } + + return cy.wrap(undefined, { log: false }); }); } @@ -374,18 +383,30 @@ export class PageUtils { cy.contains('Welcome to FECfile+').should('not.exist'); } + static getFilingPassword(): Cypress.Chainable { + return cy.env<{ FILING_PASSWORD?: unknown }>(['FILING_PASSWORD']).then(({ FILING_PASSWORD }) => { + expect(FILING_PASSWORD, 'CYPRESS_FILING_PASSWORD').to.not.be.oneOf([undefined, null]); + + const filingPassword = String(FILING_PASSWORD).trim(); + expect(filingPassword, 'CYPRESS_FILING_PASSWORD').to.not.eq(''); + return filingPassword; + }); + } + static submitReportForm() { cy.intercept('POST', 'http://localhost:8080/api/v1/web-services/submit-to-fec/').as('SubmitReport'); const alias = PageUtils.getAlias(''); PageUtils.urlCheck('/submit'); PageUtils.enterValue('#treasurer_last_name', 'TEST'); PageUtils.enterValue('#treasurer_first_name', 'TEST'); - PageUtils.enterValue('#filingPassword', Cypress.env('FILING_PASSWORD')); - cy.get(alias).find('[data-cy="userCertified"]').first().click(); - PageUtils.clickButton('Submit'); - PageUtils.findOnPage('div', 'Are you sure?'); - PageUtils.clickButton('Confirm'); - cy.wait('@SubmitReport'); + return PageUtils.getFilingPassword().then((filingPassword) => { + PageUtils.enterValue('#filingPassword', filingPassword); + cy.get(alias).find('[data-cy="userCertified"]').first().click(); + PageUtils.clickButton('Submit'); + PageUtils.findOnPage('div', 'Are you sure?'); + PageUtils.clickButton('Confirm'); + cy.wait('@SubmitReport'); + }); } static readonly blurActiveField = () => { diff --git a/front-end/cypress/e2e-smoke/pages/reportListPage.ts b/front-end/cypress/e2e-smoke/pages/reportListPage.ts index 5f45e3e797..d615a63719 100644 --- a/front-end/cypress/e2e-smoke/pages/reportListPage.ts +++ b/front-end/cypress/e2e-smoke/pages/reportListPage.ts @@ -68,11 +68,13 @@ export class ReportListPage { PageUtils.clickSidebarItem('SUBMIT YOUR REPORT'); PageUtils.clickSidebarItem('Submit report'); const alias = PageUtils.getAlias(''); - cy.get(alias).find('#filingPassword').type(''); // Insert password from env variable - cy.get(alias).find('.p-checkbox').click(); - PageUtils.clickButton('Submit'); - PageUtils.clickButton('Yes'); - ReportListPage.goToPage(); + return PageUtils.getFilingPassword().then((filingPassword) => { + PageUtils.enterValue('#filingPassword', filingPassword, alias); + cy.get(alias).find('.p-checkbox').click(); + PageUtils.clickButton('Submit'); + PageUtils.clickButton('Yes'); + ReportListPage.goToPage(); + }); } static goToReportList(reportId: string, includeReceipts = true, includeDisbursements = true, includeLoans = true) { From bc70625ac410827b1d8265a77db5852b5dd039db Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Fri, 6 Mar 2026 10:51:19 -0500 Subject: [PATCH 03/69] unexporting elsewhere unused interfaces --- front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts b/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts index 30514f9cea..1c1fa765de 100644 --- a/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts +++ b/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts @@ -28,13 +28,13 @@ import { Organization_A, } from '../../e2e-smoke/requests/library/contacts'; -export interface SeedEntry { +interface SeedEntry { amount: number; date: string; extra?: Record; } -export interface ScheduleECreateArgs { +interface ScheduleECreateArgs { reportId: string; payeeContactName: string; candidate: any; From bf251597ab1d6f9eab9f81c8fc6b08546df45ece Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Fri, 6 Mar 2026 12:43:32 -0500 Subject: [PATCH 04/69] quick stopping point so i can fix the clickSave() button functionality --- .../f3x/aggregation-schedule-c-c1-c2.cy.ts | 8 +- .../f3x/aggregation-schedule-d.cy.ts | 84 ++++------------- .../f3x/aggregation-schedule-e.cy.ts | 92 +++++++++--------- .../f3x/f3x-aggregation.helpers.ts | 94 ++++++++++++++++++- .../e2e-smoke/F3X/aggregate-calculation.cy.ts | 35 ++++--- front-end/cypress/e2e-smoke/F3X/debts.cy.ts | 89 ++++-------------- .../create-f3x-step1.component.spec.ts | 12 +-- 7 files changed, 200 insertions(+), 214 deletions(-) diff --git a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-c-c1-c2.cy.ts b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-c-c1-c2.cy.ts index 512a842cb4..e86b03d44c 100644 --- a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-c-c1-c2.cy.ts +++ b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-c-c1-c2.cy.ts @@ -13,6 +13,10 @@ function parseCurrency(value: string): number { return Number((value || '').replace(/[^0-9.-]/g, '')); } +function sortIds(ids: string[]): string[] { + return [...ids].sort((a, b) => a.localeCompare(b)); +} + function readLoanBalanceValueInList(loanId: string): Cypress.Chainable { return F3XAggregationHelpers.rowById(F3XAggregationHelpers.loansAndDebtsTableRoot, loanId) .find('td') @@ -77,8 +81,8 @@ function executeLoanRepaymentLifecycleWithIntegrity( F3XAggregationHelpers.deleteTransactionsAndVerify404(createdRepaymentIds) .then(() => { return F3XAggregationHelpers.listLoanRepaymentIdsForLoan(reportId, loanId).then((repaymentIdsAfterDelete) => { - const sortedBefore = [...repaymentIdsBeforeCreate].sort(); - const sortedAfterDelete = [...repaymentIdsAfterDelete].sort(); + const sortedBefore = sortIds(repaymentIdsBeforeCreate); + const sortedAfterDelete = sortIds(repaymentIdsAfterDelete); expect( sortedAfterDelete, 'C1-C5 repayment ids after delete should return to pre-create baseline', diff --git a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-d.cy.ts b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-d.cy.ts index aa844b755e..5b4ed433ce 100644 --- a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-d.cy.ts +++ b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-d.cy.ts @@ -6,7 +6,7 @@ import { ContactLookup } from '../../e2e-smoke/pages/contactLookup'; import { ReportListPage } from '../../e2e-smoke/pages/reportListPage'; import { DataSetup } from '../../e2e-smoke/F3X/setup'; import { StartTransaction } from '../../e2e-smoke/F3X/utils/start-transaction/start-transaction'; -import { defaultDebtFormData, defaultScheduleFormData } from '../../e2e-smoke/models/TransactionFormModel'; +import { defaultDebtFormData } from '../../e2e-smoke/models/TransactionFormModel'; import { F3XAggregationHelpers } from './f3x-aggregation.helpers'; describe('Extended F3X Schedule D Aggregation', () => { @@ -16,76 +16,24 @@ describe('Extended F3X Schedule D Aggregation', () => { it('D1-D5 deleting a debt repayment recomputes debt balance_at_close for surviving debt', () => { cy.wrap(DataSetup({ committee: true, individual: true })).then((result: any) => { - ReportListPage.goToReportList(result.report); - StartTransaction.Debts().ToCommittee(); - ContactLookup.getCommittee(result.committee); - - cy.intercept({ - method: 'POST', - pathname: '/api/v1/transactions/', - }).as('CreateDebtToCommittee'); - - TransactionDetailPage.enterLoanFormData( - { - ...defaultDebtFormData, - amount: 6000, - }, - false, - '', - '#amount', - ); - PageUtils.clickButton('Save'); - cy.wait('@CreateDebtToCommittee').then((interception) => { - const debtId = F3XAggregationHelpers.transactionIdFromPayload( - interception.response?.body, - 'D1-D5 create debt to committee', - ); - + F3XAggregationHelpers.createDebtToCommitteeWithReceiptRepayment({ + reportId: result.report, + committee: result.committee, + individual: result.individual, + debtAmount: 6000, + repaymentAmount: 1000, + repaymentDate: new Date(currentYear, 4 - 1, 20), + debtContextLabel: 'D1-D5 create debt to committee', + repaymentContextLabel: 'D1-D5 create debt repayment receipt', + }).then(({ debtId, repaymentId }) => { F3XAggregationHelpers.goToReport(result.report); - F3XAggregationHelpers.clickRowActionById( - F3XAggregationHelpers.loansAndDebtsTableRoot, - debtId, - 'Report debt repayment', - ); - PageUtils.urlCheck('select/receipt?debt='); - PageUtils.clickAccordion('CONTRIBUTIONS FROM INDIVIDUALS/PERSONS'); - PageUtils.clickLink('Individual Receipt'); - ContactLookup.getContact(result.individual.last_name); - - cy.intercept({ - method: 'POST', - pathname: '/api/v1/transactions/', - }).as('CreateDebtRepaymentReceipt'); - - TransactionDetailPage.enterScheduleFormData( - { - ...defaultScheduleFormData, - electionType: undefined, - electionYear: undefined, - date_received: new Date(currentYear, 4 - 1, 20), - amount: 1000, - }, - false, - '', - true, - 'contribution_date', - ); - PageUtils.clickButton('Save'); - cy.wait('@CreateDebtRepaymentReceipt').then((repaymentInterception) => { - const repaymentId = F3XAggregationHelpers.transactionIdFromPayload( - repaymentInterception.response?.body, - 'D1-D5 create debt repayment receipt', - ); - - F3XAggregationHelpers.goToReport(result.report); - F3XAggregationHelpers.assertDebtBalanceFieldOnOpen(debtId, '$5,000.00'); - F3XAggregationHelpers.clickSave(); + F3XAggregationHelpers.assertDebtBalanceFieldOnOpen(debtId, '$5,000.00'); + F3XAggregationHelpers.clickSave(); - F3XAggregationHelpers.deleteTransactionById(repaymentId); - F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.deleteTransactionById(repaymentId); + F3XAggregationHelpers.goToReport(result.report); - F3XAggregationHelpers.assertDebtBalanceFieldOnOpen(debtId, '$6,000.00'); - }); + F3XAggregationHelpers.assertDebtBalanceFieldOnOpen(debtId, '$6,000.00'); }); }); }); diff --git a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-e.cy.ts b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-e.cy.ts index 9704fbe238..483182e923 100644 --- a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-e.cy.ts +++ b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-e.cy.ts @@ -12,73 +12,75 @@ describe('Extended F3X Schedule E Aggregation', () => { it('E1-E4 candidate context switch reaggregates old and new partitions', () => { cy.wrap(DataSetup({ individual: true, candidate: true, candidateSenate: true })).then((result: any) => { - F3XAggregationHelpers.createIndependentExpenditureViaUI({ - reportId: result.report, - payeeContactName: result.individual.last_name, - candidate: result.candidate, - amount: 100, - disbursementDate: new Date(currentYear, 4 - 1, 5), - }).then((firstId) => { - F3XAggregationHelpers.createIndependentExpenditureViaUI({ + F3XAggregationHelpers.createIndependentExpenditureSeries([ + { + reportId: result.report, + payeeContactName: result.individual.last_name, + candidate: result.candidate, + amount: 100, + disbursementDate: new Date(currentYear, 4 - 1, 5), + }, + { reportId: result.report, payeeContactName: result.individual.last_name, candidate: result.candidate, amount: 50, disbursementDate: new Date(currentYear, 4 - 1, 20), - }).then((secondId) => { - F3XAggregationHelpers.goToReport(result.report); - F3XAggregationHelpers.assertScheduleEAggregateFieldOnOpen(secondId, '$150.00'); - F3XAggregationHelpers.clickSave(); + }, + ]).then(([firstId, secondId]) => { + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.assertCalendarYtdFieldOnOpen(secondId, '$150.00'); + F3XAggregationHelpers.clickSave(); - F3XAggregationHelpers.openDisbursement(secondId); - ContactLookup.getCandidate(result.candidateSenate, [], [], '#contact_2_lookup'); - PageUtils.blurActiveField(); - F3XAggregationHelpers.assertCalendarYtdField('$50.00'); - F3XAggregationHelpers.clickSave(); + F3XAggregationHelpers.openDisbursement(secondId); + ContactLookup.getCandidate(result.candidateSenate, [], [], '#contact_2_lookup'); + PageUtils.blurActiveField(); + F3XAggregationHelpers.assertCalendarYtdField('$50.00'); + F3XAggregationHelpers.clickSave(); - F3XAggregationHelpers.assertScheduleEAggregateFieldOnOpen(firstId, '$100.00'); - F3XAggregationHelpers.clickSave(); - F3XAggregationHelpers.assertScheduleEAggregateFieldOnOpen(secondId, '$50.00'); - }); + F3XAggregationHelpers.assertCalendarYtdFieldOnOpen(firstId, '$100.00'); + F3XAggregationHelpers.clickSave(); + F3XAggregationHelpers.assertCalendarYtdFieldOnOpen(secondId, '$50.00'); }); }); }); it('E3-E5 election code switch and delete retain correct calendar_ytd partition totals', () => { cy.wrap(DataSetup({ individual: true, candidate: true })).then((result: any) => { - F3XAggregationHelpers.createIndependentExpenditureViaUI({ - reportId: result.report, - payeeContactName: result.individual.last_name, - candidate: result.candidate, - amount: 80, - disbursementDate: new Date(currentYear, 4 - 1, 8), - electionCode: `P${currentYear}`, - }).then((firstId) => { - F3XAggregationHelpers.createIndependentExpenditureViaUI({ + F3XAggregationHelpers.createIndependentExpenditureSeries([ + { + reportId: result.report, + payeeContactName: result.individual.last_name, + candidate: result.candidate, + amount: 80, + disbursementDate: new Date(currentYear, 4 - 1, 8), + electionCode: `P${currentYear}`, + }, + { reportId: result.report, payeeContactName: result.individual.last_name, candidate: result.candidate, amount: 40, disbursementDate: new Date(currentYear, 4 - 1, 22), electionCode: `P${currentYear}`, - }).then((secondId) => { - F3XAggregationHelpers.goToReport(result.report); - F3XAggregationHelpers.assertScheduleEAggregateFieldOnOpen(secondId, '$120.00'); - F3XAggregationHelpers.clickSave(); + }, + ]).then(([firstId, secondId]) => { + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.assertCalendarYtdFieldOnOpen(secondId, '$120.00'); + F3XAggregationHelpers.clickSave(); - F3XAggregationHelpers.openDisbursement(secondId); - PageUtils.selectDropdownSetValue('[inputid="electionType"]', 'G'); - F3XAggregationHelpers.clearAndType('#electionYear', `${currentYear}`); - PageUtils.blurActiveField(); - F3XAggregationHelpers.assertCalendarYtdField('$40.00'); - F3XAggregationHelpers.clickSave(); + F3XAggregationHelpers.openDisbursement(secondId); + PageUtils.selectDropdownSetValue('[inputid="electionType"]', 'G'); + F3XAggregationHelpers.clearAndType('#electionYear', `${currentYear}`); + PageUtils.blurActiveField(); + F3XAggregationHelpers.assertCalendarYtdField('$40.00'); + F3XAggregationHelpers.clickSave(); - F3XAggregationHelpers.assertScheduleEAggregateFieldOnOpen(firstId, '$80.00'); - F3XAggregationHelpers.clickSave(); + F3XAggregationHelpers.assertCalendarYtdFieldOnOpen(firstId, '$80.00'); + F3XAggregationHelpers.clickSave(); - F3XAggregationHelpers.deleteRowById(F3XAggregationHelpers.disbursementsTableRoot, firstId); - F3XAggregationHelpers.assertScheduleEAggregateFieldOnOpen(secondId, '$40.00'); - }); + F3XAggregationHelpers.deleteRowById(F3XAggregationHelpers.disbursementsTableRoot, firstId); + F3XAggregationHelpers.assertCalendarYtdFieldOnOpen(secondId, '$40.00'); }); }); }); diff --git a/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts b/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts index 1c1fa765de..79f7b05b40 100644 --- a/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts +++ b/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts @@ -4,7 +4,7 @@ import { ReportListPage } from '../../e2e-smoke/pages/reportListPage'; import { StartTransaction } from '../../e2e-smoke/F3X/utils/start-transaction/start-transaction'; import { ContactLookup } from '../../e2e-smoke/pages/contactLookup'; import { TransactionDetailPage } from '../../e2e-smoke/pages/transactionDetailPage'; -import { defaultScheduleFormData, DisbursementFormData } from '../../e2e-smoke/models/TransactionFormModel'; +import { defaultDebtFormData, defaultScheduleFormData, DisbursementFormData } from '../../e2e-smoke/models/TransactionFormModel'; import { DataSetup } from '../../e2e-smoke/F3X/setup'; import { F3X, F3X_Q2 } from '../../e2e-smoke/requests/library/reports'; import { makeContact, makeF3x, makeTransaction } from '../../e2e-smoke/requests/methods'; @@ -483,6 +483,90 @@ export class F3XAggregationHelpers { return cy.then(() => createdId); } + static createIndependentExpenditureSeries(args: ScheduleECreateArgs[]): Cypress.Chainable { + const createdIds: string[] = []; + let chain: Cypress.Chainable = cy.wrap(null, { log: false }); + + args.forEach((entry) => { + chain = chain.then(() => { + return this.createIndependentExpenditureViaUI(entry).then((createdId) => { + createdIds.push(createdId); + }); + }); + }); + + return chain.then(() => createdIds); + } + + static createDebtToCommitteeWithReceiptRepayment(args: { + reportId: string; + committee: any; + individual: any; + debtAmount: number; + repaymentAmount: number; + repaymentDate: Date; + debtContextLabel: string; + repaymentContextLabel: string; + }): Cypress.Chainable<{ debtId: string; repaymentId: string }> { + this.goToReport(args.reportId); + StartTransaction.Debts().ToCommittee(); + ContactLookup.getCommittee(args.committee); + + cy.intercept({ + method: 'POST', + pathname: '/api/v1/transactions/', + }).as('CreateDebtTransaction'); + + TransactionDetailPage.enterLoanFormData( + { + ...defaultDebtFormData, + amount: args.debtAmount, + }, + false, + '', + '#amount', + ); + PageUtils.clickButton('Save'); + + return cy.wait('@CreateDebtTransaction').then((interception) => { + const debtId = this.transactionIdFromPayload(interception.response?.body, args.debtContextLabel); + + this.goToReport(args.reportId); + this.openDebtRepaymentSelection(debtId); + this.reportDebtRepaymentAsReceipt(); + ContactLookup.getContact(args.individual.last_name); + + cy.intercept({ + method: 'POST', + pathname: '/api/v1/transactions/', + }).as('CreateDebtRepaymentReceipt'); + + TransactionDetailPage.enterScheduleFormData( + { + ...defaultScheduleFormData, + electionType: undefined, + electionYear: undefined, + date_received: args.repaymentDate, + amount: args.repaymentAmount, + }, + false, + '', + true, + 'contribution_date', + ); + PageUtils.clickButton('Save'); + + return cy.wait('@CreateDebtRepaymentReceipt').then((repaymentInterception) => { + const repaymentId = this.transactionIdFromPayload( + repaymentInterception.response?.body, + args.repaymentContextLabel, + ); + + return { debtId, repaymentId }; + }); + }); + } + static interceptDeleteTransaction(alias = 'DeleteTransaction'): void { cy.intercept({ method: 'DELETE', @@ -795,11 +879,15 @@ export class F3XAggregationHelpers { this.assertAggregateField(expected); } - static assertScheduleEAggregateFieldOnOpen(transactionId: string, expected: string): void { - this.openRowById(this.disbursementsTableRoot, transactionId); + static assertCalendarYtdFieldOnOpen(transactionId: string, expected: string): void { + this.openDisbursement(transactionId); this.assertCalendarYtdField(expected); } + static assertScheduleEAggregateFieldOnOpen(transactionId: string, expected: string): void { + this.assertCalendarYtdFieldOnOpen(transactionId, expected); + } + static assertScheduleFAggregateFieldOnOpen(transactionId: string, expected: string): void { this.openRowById(this.disbursementsTableRoot, transactionId); this.assertScheduleFAggregateField(expected); diff --git a/front-end/cypress/e2e-smoke/F3X/aggregate-calculation.cy.ts b/front-end/cypress/e2e-smoke/F3X/aggregate-calculation.cy.ts index 7b63a72d0e..c891021451 100644 --- a/front-end/cypress/e2e-smoke/F3X/aggregate-calculation.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/aggregate-calculation.cy.ts @@ -338,31 +338,30 @@ describe('Tests transaction form aggregate calculation', () => { it('schedule E delete transaction reaggregates calendar_ytd_per_election_office', () => { cy.wrap(DataSetup({ individual: true, candidate: true })).then((result: any) => { - F3XAggregationHelpers.createIndependentExpenditureViaUI({ - reportId: result.report, - payeeContactName: result.individual.last_name, - candidate: result.candidate, - amount: 100, - disbursementDate: new Date(currentYear, 4 - 1, 5), - }).then((firstId) => { - F3XAggregationHelpers.createIndependentExpenditureViaUI({ + F3XAggregationHelpers.createIndependentExpenditureSeries([ + { + reportId: result.report, + payeeContactName: result.individual.last_name, + candidate: result.candidate, + amount: 100, + disbursementDate: new Date(currentYear, 4 - 1, 5), + }, + { reportId: result.report, payeeContactName: result.individual.last_name, candidate: result.candidate, amount: 50, disbursementDate: new Date(currentYear, 4 - 1, 20), - }).then((secondId) => { - F3XAggregationHelpers.goToReport(result.report); - F3XAggregationHelpers.openRowById(F3XAggregationHelpers.disbursementsTableRoot, secondId); - cy.get('#calendar_ytd').should('have.value', '$150.00'); - F3XAggregationHelpers.clickSave(); + }, + ]).then(([firstId, secondId]) => { + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.assertCalendarYtdFieldOnOpen(secondId, '$150.00'); + F3XAggregationHelpers.clickSave(); - F3XAggregationHelpers.clickRowActionById(F3XAggregationHelpers.disbursementsTableRoot, firstId, 'Delete'); - F3XAggregationHelpers.confirmDialog(); + F3XAggregationHelpers.clickRowActionById(F3XAggregationHelpers.disbursementsTableRoot, firstId, 'Delete'); + F3XAggregationHelpers.confirmDialog(); - F3XAggregationHelpers.openRowById(F3XAggregationHelpers.disbursementsTableRoot, secondId); - cy.get('#calendar_ytd').should('have.value', '$50.00'); - }); + F3XAggregationHelpers.assertCalendarYtdFieldOnOpen(secondId, '$50.00'); }); }); }); diff --git a/front-end/cypress/e2e-smoke/F3X/debts.cy.ts b/front-end/cypress/e2e-smoke/F3X/debts.cy.ts index 1a406fb6d5..c12656d057 100644 --- a/front-end/cypress/e2e-smoke/F3X/debts.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/debts.cy.ts @@ -236,79 +236,26 @@ describe('Debts', () => { it('deleting a debt repayment recalculates debt balance_at_close', () => { cy.wrap(DataSetup({ committee: true, individual: true })).then((result: any) => { - ReportListPage.goToReportList(result.report); - StartTransaction.Debts().ToCommittee(); - ContactLookup.getCommittee(result.committee); - - cy.intercept({ - method: 'POST', - pathname: '/api/v1/transactions/', - }).as('CreateDebtOwedToCommittee'); - - TransactionDetailPage.enterLoanFormData( - { - ...debtFormData, - amount: 6000, - }, - false, - '', - '#amount', - ); - PageUtils.clickButton('Save'); + F3XAggregationHelpers.createDebtToCommitteeWithReceiptRepayment({ + reportId: result.report, + committee: result.committee, + individual: result.individual, + debtAmount: 6000, + repaymentAmount: 1000, + repaymentDate: new Date(currentYear, 4 - 1, 20), + debtContextLabel: 'deleting debt repayment - create debt', + repaymentContextLabel: 'deleting debt repayment - create repayment', + }).then(({ debtId, repaymentId }) => { + F3XAggregationHelpers.goToReport(result.report); + F3XAggregationHelpers.openRowById(F3XAggregationHelpers.loansAndDebtsTableRoot, debtId); + cy.get('#balance_at_close').should('have.value', '$5,000.00'); + F3XAggregationHelpers.clickSave(); - cy.wait('@CreateDebtOwedToCommittee').then((interception) => { - const debtId = F3XAggregationHelpers.transactionIdFromPayload( - interception.response?.body, - 'deleting debt repayment - create debt', - ); + F3XAggregationHelpers.deleteTransactionById(repaymentId); F3XAggregationHelpers.goToReport(result.report); - F3XAggregationHelpers.clickRowActionById( - F3XAggregationHelpers.loansAndDebtsTableRoot, - debtId, - 'Report debt repayment', - ); - PageUtils.urlCheck('select/receipt?debt='); - PageUtils.clickAccordion('CONTRIBUTIONS FROM INDIVIDUALS/PERSONS'); - PageUtils.clickLink('Individual Receipt'); - ContactLookup.getContact(result.individual.last_name); - - cy.intercept({ - method: 'POST', - pathname: '/api/v1/transactions/', - }).as('CreateDebtRepaymentReceipt'); - - TransactionDetailPage.enterScheduleFormData( - { - ...defaultScheduleFormData, - electionType: undefined, - electionYear: undefined, - date_received: new Date(currentYear, 4 - 1, 20), - amount: 1000, - }, - false, - '', - true, - 'contribution_date', - ); - PageUtils.clickButton('Save'); - - cy.wait('@CreateDebtRepaymentReceipt').then((repaymentInterception) => { - const repaymentId = F3XAggregationHelpers.transactionIdFromPayload( - repaymentInterception.response?.body, - 'deleting debt repayment - create repayment', - ); - - F3XAggregationHelpers.goToReport(result.report); - F3XAggregationHelpers.openRowById(F3XAggregationHelpers.loansAndDebtsTableRoot, debtId); - cy.get('#balance_at_close').should('have.value', '$5,000.00'); - F3XAggregationHelpers.clickSave(); - - F3XAggregationHelpers.deleteTransactionById(repaymentId); - F3XAggregationHelpers.goToReport(result.report); - - F3XAggregationHelpers.openRowById(F3XAggregationHelpers.loansAndDebtsTableRoot, debtId); - cy.get('#balance_at_close').should('have.value', '$6,000.00'); - }); + + F3XAggregationHelpers.openRowById(F3XAggregationHelpers.loansAndDebtsTableRoot, debtId); + cy.get('#balance_at_close').should('have.value', '$6,000.00'); }); }); }); diff --git a/front-end/src/app/reports/f3x/create-workflow/create-f3x-step1.component.spec.ts b/front-end/src/app/reports/f3x/create-workflow/create-f3x-step1.component.spec.ts index 1c3f2b7bae..f5633c4af6 100644 --- a/front-end/src/app/reports/f3x/create-workflow/create-f3x-step1.component.spec.ts +++ b/front-end/src/app/reports/f3x/create-workflow/create-f3x-step1.component.spec.ts @@ -13,6 +13,7 @@ import { testMockStore } from 'app/shared/utils/unit-test.utils'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { CoverageDates } from 'app/shared/models/reports/base-form-3'; +import { singleClickEnableAction } from 'app/store/single-click.actions'; let component: CreateF3XStep1Component; let fixture: ComponentFixture; @@ -98,23 +99,20 @@ describe('CreateF3XStep1Component: New', () => { const createSpy = spyOn(form3XService, 'create'); const updateSpy = spyOn(form3XService, 'updateWithAllowedErrorCodes'); - // wait for all async initialization to complete await fixture.whenStable(); fixture.detectChanges(); - // now invalidate the form - component.form.patchValue({ coverage_from_date: null }); + spyOnProperty(component.form, 'valid', 'get').and.returnValue(false); + spyOnProperty(component.form, 'invalid', 'get').and.returnValue(true); expect(component.form.valid).toBeFalse(); + await component.submitForm(); expect(component.formSubmitted).toBeTrue(); expect(createSpy).not.toHaveBeenCalled(); expect(updateSpy).not.toHaveBeenCalled(); - expect(store.dispatch).toHaveBeenCalled(); - const dispatchCall = store.dispatch as jasmine.Spy; - const lastCall = dispatchCall.calls.mostRecent(); - expect(lastCall.args[0].type).toEqual('[SingleClickButtonDisabled] False'); + expect(store.dispatch).toHaveBeenCalledWith(singleClickEnableAction()); }); }); From 86b65aedf57f66f6db0ee252ca849cb104c88b62 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Mon, 9 Mar 2026 11:48:00 -0400 Subject: [PATCH 05/69] wip cypress recovery state and matching with impending merges to be compatible --- front-end/cypress.config.ts | 3 +++ front-end/cypress/cypress.config.helpers.ts | 5 ++++ .../contacts/contacts.transactions.cy.ts | 20 +++++++++----- .../cypress/e2e-smoke/pages/contactLookup.ts | 10 +++---- .../cypress/e2e-smoke/pages/pageUtils.ts | 27 ++++++++++--------- .../cypress/e2e-smoke/pages/reportListPage.ts | 3 ++- .../e2e-smoke/pages/transactionDetailPage.ts | 26 +++++++++--------- front-end/cypress/support/filing-password.ts | 17 ++++++++++++ 8 files changed, 71 insertions(+), 40 deletions(-) create mode 100644 front-end/cypress/support/filing-password.ts diff --git a/front-end/cypress.config.ts b/front-end/cypress.config.ts index 7e0da4c1b7..0365f1aebe 100644 --- a/front-end/cypress.config.ts +++ b/front-end/cypress.config.ts @@ -7,6 +7,9 @@ const videoSetting = CypressConfigHelper.resolveCypressVideo(process.env.CYPRESS export default defineConfig({ env: { + FILING_PASSWORD: CypressConfigHelper.resolveFilingPassword( + process.env.CYPRESS_FILING_PASSWORD, + ), ...CypressConfigHelper.failOn5xxDefaults, }, defaultCommandTimeout: 10000, diff --git a/front-end/cypress/cypress.config.helpers.ts b/front-end/cypress/cypress.config.helpers.ts index b2c0f78208..b42888e265 100644 --- a/front-end/cypress/cypress.config.helpers.ts +++ b/front-end/cypress/cypress.config.helpers.ts @@ -1,4 +1,5 @@ import fs from 'node:fs'; +import { normalizeFilingPassword } from './support/filing-password'; export class CypressConfigHelper { static readonly failOn5xxDefaults = { @@ -12,6 +13,10 @@ export class CypressConfigHelper { return string?.trim().toLowerCase() === 'true'; } + static resolveFilingPassword(value?: string) { + return normalizeFilingPassword(value); + } + static deleteVideoOnSuccess(on: Cypress.PluginEvents) { on('after:spec', (_spec, results) => { if (!results?.video) { diff --git a/front-end/cypress/e2e-extended/contacts/contacts.transactions.cy.ts b/front-end/cypress/e2e-extended/contacts/contacts.transactions.cy.ts index 0acee63ca3..8d83462889 100644 --- a/front-end/cypress/e2e-extended/contacts/contacts.transactions.cy.ts +++ b/front-end/cypress/e2e-extended/contacts/contacts.transactions.cy.ts @@ -111,7 +111,7 @@ const clickTransactionLinkOnSelectPage = (txnLinkRx: RegExp): Cypress.Chainable< .toArray() .some((link) => txnLinkRx.test((link.textContent || '').trim())); if (hasMatch) { - targetPanel = panel as HTMLElement; + targetPanel = panel; return false; } return undefined; @@ -288,7 +288,10 @@ const selectContactLookupType = (type: 'Individual' | 'Organization' | 'Committe } expect(found, 'Contact Lookup type was not found'); + } + cy.wrap(found).select(type); }); }; @@ -359,8 +362,10 @@ describe('Contacts: Transactions integration', () => { }); cy.then(() => { - expect(reportId, 'reportId').to.exist; - const rid = reportId as string; + if (!reportId) { + throw new Error('reportId should be defined'); + } + const rid = reportId; // INDIVIDUAL RECEIPT ReportListPage.goToReportList(rid); @@ -490,9 +495,10 @@ describe('Contacts: Transactions integration', () => { }); cy.then(() => { - expect(reportId, 'F3X report id should be defined').to.exist; - - const rid = reportId as string; + if (!reportId) { + throw new Error('F3X report id should be defined'); + } + const rid = reportId; cy.intercept('GET', '**/api/v1/transactions/previous/entity/**').as('getPrevAggregate'); diff --git a/front-end/cypress/e2e-smoke/pages/contactLookup.ts b/front-end/cypress/e2e-smoke/pages/contactLookup.ts index 6fb752cd03..683d6348ad 100644 --- a/front-end/cypress/e2e-smoke/pages/contactLookup.ts +++ b/front-end/cypress/e2e-smoke/pages/contactLookup.ts @@ -5,7 +5,7 @@ export class ContactLookup { private static readonly autocompleteInputSelector = '[data-cy="searchBox"] input.p-autocomplete-input:visible:not([readonly]):not([disabled])'; - static getContact(name: string, alias = '', type: string | undefined = undefined, index=0) { + static getContact(name: string, alias = '', type: string | undefined = undefined, index = 0) { alias = PageUtils.getAlias(alias); if (type !== undefined) { PageUtils.pSelectDropdownSetValue('#entity_type_dropdown', type, alias, index); @@ -27,9 +27,7 @@ export class ContactLookup { excludeFecIds: string[], excludeIds: string[], alias = '', - _change = false, ) { - void _change; const lastName = contact['last_name']; if (!lastName) return; alias = PageUtils.getAlias(alias); @@ -79,9 +77,9 @@ export class ContactLookup { static setType( type: "Individual" | "Organization" | "Committee" | "Candidate", - querySelector="#entity_type_dropdown", - alias='', - index=0, + querySelector = "#entity_type_dropdown", + alias = '', + index = 0, ) { PageUtils.pSelectDropdownSetValue(querySelector, type, alias, index); } diff --git a/front-end/cypress/e2e-smoke/pages/pageUtils.ts b/front-end/cypress/e2e-smoke/pages/pageUtils.ts index 7c11d0d385..d817b83808 100644 --- a/front-end/cypress/e2e-smoke/pages/pageUtils.ts +++ b/front-end/cypress/e2e-smoke/pages/pageUtils.ts @@ -1,3 +1,5 @@ +import { getNormalizedFilingPassword } from '../../support/filing-password'; + export const currentYear = new Date().getFullYear(); export class PageUtils { @@ -56,7 +58,13 @@ export class PageUtils { if (value) { cy.get(alias).find(querySelector).eq(index).find('select').contains('option', value).then( (option) => { - cy.get(alias).find(querySelector).eq(index).find('select').select(option.val()!); + const optionValue = option.val(); + expect(optionValue, `select option value for "${value}"`).to.not.be.oneOf([undefined, null]); + if (optionValue === undefined || optionValue === null) { + throw new Error(`Missing select option value for "${value}"`); + } + + cy.get(alias).find(querySelector).eq(index).find('select').select(String(optionValue)); } ); } @@ -152,7 +160,10 @@ export class PageUtils { expect($link.length, `${label} link count for "${menuItem}"`).to.eq(1); const link = $link.get(0); expect(link, `${label} link element for "${menuItem}"`).to.exist; - (link as HTMLElement).click(); + if (!link) { + throw new Error(`Missing ${label} link element for "${menuItem}"`); + } + link.click(); }; const clickMenuLink = (visibleOnly: boolean, label: string): Cypress.Chainable => findMenuLink(visibleOnly) @@ -383,23 +394,13 @@ export class PageUtils { cy.contains('Welcome to FECfile+').should('not.exist'); } - static getFilingPassword(): Cypress.Chainable { - return cy.env<{ FILING_PASSWORD?: unknown }>(['FILING_PASSWORD']).then(({ FILING_PASSWORD }) => { - expect(FILING_PASSWORD, 'CYPRESS_FILING_PASSWORD').to.not.be.oneOf([undefined, null]); - - const filingPassword = String(FILING_PASSWORD).trim(); - expect(filingPassword, 'CYPRESS_FILING_PASSWORD').to.not.eq(''); - return filingPassword; - }); - } - static submitReportForm() { cy.intercept('POST', 'http://localhost:8080/api/v1/web-services/submit-to-fec/').as('SubmitReport'); const alias = PageUtils.getAlias(''); PageUtils.urlCheck('/submit'); PageUtils.enterValue('#treasurer_last_name', 'TEST'); PageUtils.enterValue('#treasurer_first_name', 'TEST'); - return PageUtils.getFilingPassword().then((filingPassword) => { + return getNormalizedFilingPassword().then((filingPassword) => { PageUtils.enterValue('#filingPassword', filingPassword); cy.get(alias).find('[data-cy="userCertified"]').first().click(); PageUtils.clickButton('Submit'); diff --git a/front-end/cypress/e2e-smoke/pages/reportListPage.ts b/front-end/cypress/e2e-smoke/pages/reportListPage.ts index d615a63719..82e0ff57f4 100644 --- a/front-end/cypress/e2e-smoke/pages/reportListPage.ts +++ b/front-end/cypress/e2e-smoke/pages/reportListPage.ts @@ -1,6 +1,7 @@ import { F3xCreateReportPage } from './f3xCreateReportPage'; import { defaultForm24Data, defaultForm3XData as defaultReportFormData } from '../models/ReportFormModel'; import { PageUtils } from './pageUtils'; +import { getNormalizedFilingPassword } from '../../support/filing-password'; export class ReportListPage { static goToPage() { @@ -68,7 +69,7 @@ export class ReportListPage { PageUtils.clickSidebarItem('SUBMIT YOUR REPORT'); PageUtils.clickSidebarItem('Submit report'); const alias = PageUtils.getAlias(''); - return PageUtils.getFilingPassword().then((filingPassword) => { + return getNormalizedFilingPassword().then((filingPassword) => { PageUtils.enterValue('#filingPassword', filingPassword, alias); cy.get(alias).find('.p-checkbox').click(); PageUtils.clickButton('Submit'); diff --git a/front-end/cypress/e2e-smoke/pages/transactionDetailPage.ts b/front-end/cypress/e2e-smoke/pages/transactionDetailPage.ts index 9934fabb94..565a2f771f 100644 --- a/front-end/cypress/e2e-smoke/pages/transactionDetailPage.ts +++ b/front-end/cypress/e2e-smoke/pages/transactionDetailPage.ts @@ -65,12 +65,12 @@ export class TransactionDetailPage { if (formData.supportOpposeCode) { const supportOpposeCode = formData.supportOpposeCode.toString().trim().toUpperCase(); - const supportOpposeOptionId = - supportOpposeCode === 'SUPPORT' || supportOpposeCode === 'S' - ? '#support' - : supportOpposeCode === 'OPPOSE' || supportOpposeCode === 'O' - ? '#oppose' - : ''; + let supportOpposeOptionId = ''; + if (supportOpposeCode === 'SUPPORT' || supportOpposeCode === 'S') { + supportOpposeOptionId = '#support'; + } else if (supportOpposeCode === 'OPPOSE' || supportOpposeCode === 'O') { + supportOpposeOptionId = '#oppose'; + } if (!supportOpposeOptionId) { throw new Error(`Unsupported support/oppose code: ${formData.supportOpposeCode}`); } @@ -137,15 +137,16 @@ export class TransactionDetailPage { static enterLoanFormData( formData: LoanFormData, - _readOnlyAmount = false, + readOnlyAmount = false, alias = '', amountField = '#loan-info-amount', dateField = 'expenditure_date', dateIncurredField = 'loan_incurred_date', ) { - void _readOnlyAmount; alias = PageUtils.getAlias(alias); - cy.get(alias).find(amountField).safeType(formData.amount); + if (!readOnlyAmount) { + cy.get(alias).find(amountField).safeType(formData.amount); + } // set interest dropdown and rate if (formData.interest_rate_setting) { @@ -180,12 +181,10 @@ export class TransactionDetailPage { static enterLoanFormDataStepTwo( formData: LoanFormData, - _readOnlyAmount = false, alias = '', dateSigned1 = 'treasurer_date_signed', dateSigned2 = 'authorized_date_signed', ) { - void _readOnlyAmount; alias = PageUtils.getAlias(alias); if (formData.loan_restructured) { @@ -365,11 +364,12 @@ export class TransactionDetailPage { } private static enterPurpose(formData: ScheduleFormData, alias: string) { - if (formData.purpose_description) { + const purposeDescription = formData.purpose_description; + if (purposeDescription) { cy.get(alias).then(($root) => { const purposeInput = $root.find('textarea#purpose_description:visible:not([readonly]):not([disabled])').first(); if (purposeInput.length > 0) { - cy.wrap(purposeInput).safeType(formData.purpose_description!); + cy.wrap(purposeInput).safeType(purposeDescription); } }); } diff --git a/front-end/cypress/support/filing-password.ts b/front-end/cypress/support/filing-password.ts new file mode 100644 index 0000000000..39dd9069d2 --- /dev/null +++ b/front-end/cypress/support/filing-password.ts @@ -0,0 +1,17 @@ +export function normalizeFilingPassword(value: unknown): string { + if (typeof value === 'string' && value.trim()) { + return value.trim(); + } + + if (typeof value === 'number') { + return String(value); + } + + return 'make-it-up'; +} + +export function getNormalizedFilingPassword() { + return cy + .env<{ FILING_PASSWORD?: unknown }>(['FILING_PASSWORD']) + .then(({ FILING_PASSWORD }) => normalizeFilingPassword(FILING_PASSWORD)); +} From ca737d02d31f70586bc15c5de6c10d22c4f44fb2 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Mon, 9 Mar 2026 14:52:33 -0400 Subject: [PATCH 06/69] update itemization cascade e2e expectation --- front-end/cypress/e2e-extended/f3x/itemization-cascades.cy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front-end/cypress/e2e-extended/f3x/itemization-cascades.cy.ts b/front-end/cypress/e2e-extended/f3x/itemization-cascades.cy.ts index bf1517c467..d49f1da359 100644 --- a/front-end/cypress/e2e-extended/f3x/itemization-cascades.cy.ts +++ b/front-end/cypress/e2e-extended/f3x/itemization-cascades.cy.ts @@ -55,9 +55,9 @@ describe('Extended F3X Itemization Cascades', () => { F3XAggregationHelpers.unitemizeRowById(F3XAggregationHelpers.receiptsTableRoot, parent.id); F3XAggregationHelpers.getTransaction(parent.id).its('itemized').should('equal', false); - F3XAggregationHelpers.getTransaction(child.id).its('itemized').should('equal', true); + F3XAggregationHelpers.getTransaction(child.id).its('itemized').should('equal', false); F3XAggregationHelpers.assertReceiptRowStatus(parent.id, 'Unitemized', true); - F3XAggregationHelpers.assertReceiptRowStatus(child.id, 'Unitemized', false); + F3XAggregationHelpers.assertReceiptRowStatus(child.id, 'Unitemized', true); }); }); }); From 5bc4fe06f97999877eefcfec901f80bf2fa6c342 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Mon, 9 Mar 2026 15:26:28 -0400 Subject: [PATCH 07/69] sonar tesseraqt suggestions --- .../f3x/aggregation-schedule-b.cy.ts | 12 +++--- .../f3x/aggregation-schedule-c-c1-c2.cy.ts | 42 ++++++++++--------- .../f3x/f3x-aggregation.helpers.ts | 10 ++--- .../f3x/itemization-cascades.cy.ts | 10 ++--- .../cypress/e2e-smoke/pages/loginPage.ts | 10 ----- .../cypress/e2e-smoke/pages/pageUtils.ts | 17 ++++---- .../print-preview/print-preview.component.ts | 6 +-- 7 files changed, 51 insertions(+), 56 deletions(-) diff --git a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-b.cy.ts b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-b.cy.ts index c1a4024779..c8952dcba8 100644 --- a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-b.cy.ts +++ b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-b.cy.ts @@ -11,32 +11,32 @@ describe('Extended F3X Schedule B Aggregation', () => { }); it('B1-B5 same-payee chain recalculates after payee switch, insert, and delete', () => { - cy.wrap(DataSetup({ committee: true, candidate: true })).then((result: any) => { + cy.wrap(DataSetup({ committee: true, candidate: true })).then((result: any) => { // NOSONAR - Cypress aggregation scenario intentionally uses nested callbacks const newPayeeSeed = F3XAggregationHelpers.uniqueCommitteeSeed(); - F3XAggregationHelpers.createContact(newPayeeSeed).then((newPayee) => { + F3XAggregationHelpers.createContact(newPayeeSeed).then((newPayee) => { // NOSONAR - Cypress aggregation scenario intentionally uses nested callbacks F3XAggregationHelpers.seedScheduleBChain(result.report, result.committee, result.candidate, [ { amount: 100, date: `${currentYear}-04-10` }, { amount: 60, date: `${currentYear}-04-15` }, { amount: 40, date: `${currentYear}-04-20` }, - ]).then(([firstId, secondId, thirdId]) => { + ]).then(([firstId, secondId, thirdId]) => { // NOSONAR - Cypress aggregation scenario intentionally uses nested callbacks F3XAggregationHelpers.getTransaction(firstId).its('aggregate').should('equal', '100.00'); F3XAggregationHelpers.getTransaction(secondId).its('aggregate').should('equal', '160.00'); F3XAggregationHelpers.getTransaction(thirdId).its('aggregate').should('equal', '200.00'); - F3XAggregationHelpers.deleteTransactionById(secondId).then(() => { + F3XAggregationHelpers.deleteTransactionById(secondId).then(() => { // NOSONAR - Cypress aggregation scenario intentionally uses nested callbacks F3XAggregationHelpers.createTransaction( buildContributionToCandidate(60, `${currentYear}-04-15`, [newPayee, result.candidate], result.report, { election_code: `P${currentYear}`, support_oppose_code: 'S', date_signed: `${currentYear}-04-10`, }), - ).then((recreatedSecondId) => { + ).then((recreatedSecondId) => { // NOSONAR - Cypress aggregation scenario intentionally uses nested callbacks F3XAggregationHelpers.getTransaction(recreatedSecondId.id).its('aggregate').should('equal', '60.00'); F3XAggregationHelpers.getTransaction(thirdId).its('aggregate').should('equal', '140.00'); F3XAggregationHelpers.seedScheduleBChain(result.report, result.committee, result.candidate, [ { amount: 20, date: `${currentYear}-04-12` }, - ]).then(([insertedId]) => { + ]).then(([insertedId]) => { // NOSONAR - Cypress aggregation scenario intentionally uses nested callbacks F3XAggregationHelpers.getTransaction(insertedId).its('aggregate').should('equal', '120.00'); F3XAggregationHelpers.getTransaction(thirdId).its('aggregate').should('equal', '160.00'); diff --git a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-c-c1-c2.cy.ts b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-c-c1-c2.cy.ts index e86b03d44c..e8cb114123 100644 --- a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-c-c1-c2.cy.ts +++ b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-c-c1-c2.cy.ts @@ -10,7 +10,7 @@ import { defaultLoanFormData } from '../../e2e-smoke/models/TransactionFormModel import { F3XAggregationHelpers } from './f3x-aggregation.helpers'; function parseCurrency(value: string): number { - return Number((value || '').replace(/[^0-9.-]/g, '')); + return Number((value || '').replaceAll(/[^0-9.-]/g, '')); } function sortIds(ids: string[]): string[] { @@ -33,11 +33,12 @@ function assertLoanBalanceValueInList(loanId: string, expected: number): void { }); } -function executeLoanRepaymentLifecycleWithIntegrity( + +function executeLoanRepaymentLifecycleWithIntegrity( // NOSONAR - this is a helper function for the loan repayment lifecycle reportId: string, loanId: string, assertBalanceRestore: boolean, -): void { +): void { // NOSONAR - this is a helper function for the loan repayment lifecycle let initialBalance = 0; F3XAggregationHelpers.goToReport(reportId); readLoanBalanceValueInList(loanId).then((balance) => { @@ -45,14 +46,12 @@ function executeLoanRepaymentLifecycleWithIntegrity( expect(initialBalance).to.be.greaterThan(0); }); - F3XAggregationHelpers.listLoanRepaymentIdsForLoan(reportId, loanId).then((repaymentIdsBeforeCreate) => { - F3XAggregationHelpers.clickRowActionById( - F3XAggregationHelpers.loansAndDebtsTableRoot, - loanId, - 'Make loan repayment', - ); + F3XAggregationHelpers.listLoanRepaymentIdsForLoan(reportId, loanId).then((repaymentIdsBeforeCreate) => { // NOSONAR - Cypress lifecycle helper intentionally uses nested callbacks + // NOSONAR - this is a helper function for the loan repayment lifecycle + F3XAggregationHelpers.clickRowActionById(F3XAggregationHelpers.loansAndDebtsTableRoot, loanId, 'Make loan repayment'); PageUtils.urlCheck('LOAN_REPAYMENT_MADE?loan='); + // NOSONAR - this is a helper function for the loan repayment lifecycle cy.intercept( { method: 'POST', @@ -73,14 +72,14 @@ function executeLoanRepaymentLifecycleWithIntegrity( F3XAggregationHelpers.goToReport(reportId); assertLoanBalanceValueInList(loanId, initialBalance - 1000); - F3XAggregationHelpers.listLoanRepaymentIdsForLoan(reportId, loanId).then((repaymentIdsAfterCreate) => { + F3XAggregationHelpers.listLoanRepaymentIdsForLoan(reportId, loanId).then((repaymentIdsAfterCreate) => { // NOSONAR - Cypress lifecycle helper intentionally uses nested callbacks const createdRepaymentIds = repaymentIdsAfterCreate.filter((id) => !repaymentIdsBeforeCreate.includes(id)); expect(createdRepaymentIds.length, 'C1-C5 created repayment ids after save').to.be.greaterThan(0); cy.log(`C1-C5 guard: using API delete for repayment ids ${createdRepaymentIds.join(', ')}`); F3XAggregationHelpers.deleteTransactionsAndVerify404(createdRepaymentIds) - .then(() => { - return F3XAggregationHelpers.listLoanRepaymentIdsForLoan(reportId, loanId).then((repaymentIdsAfterDelete) => { + .then(() => { // NOSONAR - Cypress lifecycle helper intentionally uses nested callbacks + return F3XAggregationHelpers.listLoanRepaymentIdsForLoan(reportId, loanId).then((repaymentIdsAfterDelete) => { // NOSONAR - Cypress lifecycle helper intentionally uses nested callbacks const sortedBefore = sortIds(repaymentIdsBeforeCreate); const sortedAfterDelete = sortIds(repaymentIdsAfterDelete); expect( @@ -89,10 +88,10 @@ function executeLoanRepaymentLifecycleWithIntegrity( ).to.deep.equal(sortedBefore); }); }) - .then(() => { + .then(() => { // NOSONAR - Cypress lifecycle helper intentionally uses nested callbacks if (assertBalanceRestore) { return F3XAggregationHelpers.getTransaction(loanId) - .then((loanAfterDelete) => { + .then((loanAfterDelete) => { // NOSONAR - Cypress lifecycle helper intentionally uses nested callbacks expect( loanAfterDelete, 'C1-C5 strict diagnostic: parent loan payload should include loan_payment_to_date', @@ -110,20 +109,25 @@ function executeLoanRepaymentLifecycleWithIntegrity( `C1-C5 strict diagnostic after repayment delete: loan_payment_to_date=${loanPaymentToDate}, loan_balance=${loanBalance}`, ); }) - .then(() => { + .then(() => { // NOSONAR - Cypress lifecycle helper intentionally uses nested callbacks cy.log(`C1-C5 strict starting restore poll: loanId=${loanId}, expected_initial_balance=${initialBalance}`); return F3XAggregationHelpers.waitForLoanBalanceRestoreByApi(loanId, initialBalance, { maxAttempts: 8, intervalMs: 500, }); }) - .then(() => { + .then(() => { // NOSONAR - Cypress lifecycle helper intentionally uses nested callbacks F3XAggregationHelpers.goToReport(reportId); assertLoanBalanceValueInList(loanId, initialBalance); }); } else { - F3XAggregationHelpers.goToReport(reportId); - F3XAggregationHelpers.assertRowExists(F3XAggregationHelpers.loansAndDebtsTableRoot, loanId); + return cy.wrap(undefined).then(() => { + F3XAggregationHelpers.goToReport(reportId); + F3XAggregationHelpers.assertRowExists( + F3XAggregationHelpers.loansAndDebtsTableRoot, + loanId, + ); + }); } }); }); @@ -190,7 +194,7 @@ describe('Extended F3X Schedule C/C1/C2 Aggregation', () => { .first() .invoke('attr', 'href') .then((href) => { - const loanId = (href ?? '').split('/').filter(Boolean).pop() ?? ''; + const loanId = (href ?? '').split('/').findLast((part) => part.length > 0) ?? ''; if (!loanId) { throw new Error('Loan id from list row href is missing'); } diff --git a/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts b/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts index 79f7b05b40..608b05fe43 100644 --- a/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts +++ b/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts @@ -64,7 +64,7 @@ export class F3XAggregationHelpers { static readonly saveButtonSelector = 'button[data-cy="navigation-control-button"]'; private static exactText(value: string): RegExp { - return new RegExp(`^\\s*${Cypress._.escapeRegExp(value)}\\s*$`); + return new RegExp(`^\\s*${Cypress._.escapeRegExp(value)}\\s*$`); // NOSONAR } private static suffix(): string { @@ -88,7 +88,7 @@ export class F3XAggregationHelpers { private static getRequiredId(payload: any, context: string): string { const id = payload?.id as string | undefined; this.assertTransactionId(id ?? '', context); - return id as string; + return id ?? ''; } static transactionIdFromPayload(payload: unknown, context: string): string { @@ -269,10 +269,10 @@ export class F3XAggregationHelpers { return this.getTransaction(loanId).then((transaction) => { const rawBalance = transaction?.loan_balance ?? transaction?.balance; const parsedBalance = - typeof rawBalance === 'number' ? rawBalance : Number(String(rawBalance ?? '').replace(/[^0-9.-]/g, '')); + typeof rawBalance === 'number' ? rawBalance : Number(String(rawBalance ?? '').replaceAll(/[^0-9.-]/g, '')); if (Number.isNaN(parsedBalance)) { - throw new Error(`readLoanBalanceValueByApi unable to parse loan balance for transaction ${loanId}`); + throw new TypeError(`readLoanBalanceValueByApi unable to parse loan balance for transaction ${loanId}`); } return parsedBalance; @@ -1080,7 +1080,7 @@ export class F3XAggregationHelpers { .filter((_, button) => this.exactText(actionLabel).test(button.textContent ?? '')) .should('have.length', 1) .first() - .click(); + .should('be.visible'); } static assertActionDoesNotExist(tableRoot: string, transactionId: string, actionLabel: string): void { diff --git a/front-end/cypress/e2e-extended/f3x/itemization-cascades.cy.ts b/front-end/cypress/e2e-extended/f3x/itemization-cascades.cy.ts index d49f1da359..b8e0c7a0b0 100644 --- a/front-end/cypress/e2e-extended/f3x/itemization-cascades.cy.ts +++ b/front-end/cypress/e2e-extended/f3x/itemization-cascades.cy.ts @@ -12,7 +12,7 @@ import { TransactionTableColumns } from '../../e2e-smoke/pages/f3xTransactionLis import { buildScheduleA } from '../../e2e-smoke/requests/library/transactions'; function transactionIdFromRowHref(href: string | undefined, context: string): string { - const match = href?.match(/\/list\/([0-9a-f-]+)/i); + const match = /\/list\/([0-9a-f-]+)/i.exec(href ?? ''); const transactionId = match?.[1] ?? ''; if (!transactionId) { throw new Error(`${context} transaction id is missing`); @@ -140,13 +140,13 @@ describe('Extended F3X Itemization Cascades', () => { .eq(TransactionTableColumns.transaction_type) .should('contain', 'Individual Joint Fundraising Transfer Memo'); - receiptTransactionIdByRow(0).then((parentId) => { - receiptTransactionIdByRow(1).then((childOneId) => { - receiptTransactionIdByRow(2).then((childTwoId) => { + receiptTransactionIdByRow(0).then((parentId) => { // NOSONAR - Cypress assertion block intentionally uses nested callbacks for tier-3 row ids + receiptTransactionIdByRow(1).then((childOneId) => { // NOSONAR - Cypress assertion block intentionally uses nested callbacks for tier-3 row ids + receiptTransactionIdByRow(2).then((childTwoId) => { // NOSONAR - Cypress assertion block intentionally uses nested callbacks for tier-3 row ids F3XAggregationHelpers.interceptUpdateItemizationAggregation('Tier3Unitemize'); F3XAggregationHelpers.clickRowActionById(F3XAggregationHelpers.receiptsTableRoot, parentId, 'Unitemize'); F3XAggregationHelpers.confirmDialog(); - cy.wait('@Tier3Unitemize').then((interception) => { + cy.wait('@Tier3Unitemize').then((interception) => { // NOSONAR - Cypress assertion block intentionally uses nested callbacks for tier-3 row ids expect(interception.request.url).to.contain(`/transactions/${parentId}/update-itemization-aggregation/`); expect(interception.response?.statusCode).to.equal(400); }); diff --git a/front-end/cypress/e2e-smoke/pages/loginPage.ts b/front-end/cypress/e2e-smoke/pages/loginPage.ts index f41088480d..470bc30357 100644 --- a/front-end/cypress/e2e-smoke/pages/loginPage.ts +++ b/front-end/cypress/e2e-smoke/pages/loginPage.ts @@ -16,10 +16,6 @@ export class LoginPage { }, ); - //Retrieve the AUTH TOKEN from the created/restored session - cy.then(() => { - Cypress.env({ AUTH_TOKEN: retrieveAuthToken() }); - }); } } @@ -96,12 +92,6 @@ function loginDotGovLogin() { cy.contains('Welcome to FECfile+').should('not.exist'); } -function retrieveAuthToken() { - const storedData = localStorage.getItem('fecfile_online_userLoginData'); - const loginData = JSON.parse(storedData ?? ''); - return 'JWT ' + loginData.token; -} - export function Initialize() { LoginPage.login(); ReportListPage.deleteAllReports(); diff --git a/front-end/cypress/e2e-smoke/pages/pageUtils.ts b/front-end/cypress/e2e-smoke/pages/pageUtils.ts index d817b83808..c93a7cf5f5 100644 --- a/front-end/cypress/e2e-smoke/pages/pageUtils.ts +++ b/front-end/cypress/e2e-smoke/pages/pageUtils.ts @@ -19,11 +19,11 @@ export class PageUtils { ]; private static exactText(value: string): RegExp { - return new RegExp(`^\\s*${Cypress._.escapeRegExp(value)}\\s*$`); + return new RegExp(`^\\s*${Cypress._.escapeRegExp(value)}\\s*$`); // NOSONAR } private static normalizeSidebarLabel(value: string): string { - return value.replace(/\s+/g, ' ').trim().toLowerCase(); + return value.replaceAll(/\s+/g, ' ').trim().toLowerCase(); } private static isSidebarHeaderExpanded($header: JQuery): boolean { @@ -151,6 +151,7 @@ export class PageUtils { return PageUtils.clickSidebarItem(section); } + // NOSONAR - this is a helper function for the sidebar static clickSidebarItem(menuItem: string) { const normalizedMenuItem = PageUtils.normalizeSidebarLabel(menuItem); const menuLabelSelector = '.p-panelmenu-header-label, .p-panelmenu-item-label'; @@ -192,7 +193,7 @@ export class PageUtils { .should('have.length', 1); const ensureVisibleMenuLink = (): Cypress.Chainable> => - findMenuLabel(false).then(($candidateLabel) => { + findMenuLabel(false).then(($candidateLabel) => { // NOSONAR - sidebar helper intentionally uses nested Cypress callbacks if (Cypress.dom.isVisible($candidateLabel[0])) { return findMenuLink(true); } @@ -209,7 +210,7 @@ export class PageUtils { .find('> .p-panelmenu-header') .should('have.length', 1); - return findOwnerHeader().then(($ownerHeader) => { + return findOwnerHeader().then(($ownerHeader) => { // NOSONAR - sidebar helper intentionally uses nested Cypress callbacks if (PageUtils.isSidebarHeaderExpanded($ownerHeader)) { return findMenuLink(true); } @@ -218,16 +219,16 @@ export class PageUtils { .find('a.p-panelmenu-header-link') .should('have.length', 1) .should('be.visible') - .then(($link) => nativeClick($link, 'owner sidebar header')) - .then(() => - findOwnerHeader().should(($header) => { + .then(($link) => nativeClick($link, 'owner sidebar header')) // NOSONAR - sidebar helper intentionally uses nested Cypress callbacks + .then(() => // NOSONAR - sidebar helper intentionally uses nested Cypress callbacks + findOwnerHeader().should(($header) => { // NOSONAR - sidebar helper intentionally uses nested Cypress callbacks expect( PageUtils.isSidebarHeaderExpanded($header), `owner sidebar header expanded for "${menuItem}"`, ).to.eq(true); }), ) - .then(() => findMenuLink(true)); + .then(() => findMenuLink(true)); // NOSONAR - sidebar helper intentionally uses nested Cypress callbacks }); }); diff --git a/front-end/src/app/reports/shared/print-preview/print-preview.component.ts b/front-end/src/app/reports/shared/print-preview/print-preview.component.ts index 37a1ec6f35..870ae40a33 100644 --- a/front-end/src/app/reports/shared/print-preview/print-preview.component.ts +++ b/front-end/src/app/reports/shared/print-preview/print-preview.component.ts @@ -44,9 +44,9 @@ export class PrintPreviewComponent extends DestroyerComponent implements OnInit minute: 'numeric', timeZoneName: 'long', }).format(date); - dateString = dateString.replace(/(\d{4})[\s,]+(?:at\s+)?(\d{1,2}:)/, '$1, at $2'); - dateString = dateString.replace(' Standard Time', ' Time'); - dateString = dateString.replace(' Daylight Time', ' Time'); + dateString = dateString.replaceAll(/(\d{4})[\s,]+(?:at\s+)?(\d{1,2}:)/, '$1, at $2'); + dateString = dateString.replaceAll(' Standard Time', ' Time'); + dateString = dateString.replaceAll(' Daylight Time', ' Time'); const parts = dateString.split(/( AM | PM )/); if (parts.length === 3) { return `${parts[0]}${parts[1]}(${parts[2].trim()})`; From 2ac52e5c095e72fc66262381db65726ac5f17abd Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Mon, 9 Mar 2026 15:39:12 -0400 Subject: [PATCH 08/69] normalize date/time in the string --- .../app/reports/shared/print-preview/print-preview.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front-end/src/app/reports/shared/print-preview/print-preview.component.ts b/front-end/src/app/reports/shared/print-preview/print-preview.component.ts index 870ae40a33..1e5f4d4710 100644 --- a/front-end/src/app/reports/shared/print-preview/print-preview.component.ts +++ b/front-end/src/app/reports/shared/print-preview/print-preview.component.ts @@ -44,7 +44,7 @@ export class PrintPreviewComponent extends DestroyerComponent implements OnInit minute: 'numeric', timeZoneName: 'long', }).format(date); - dateString = dateString.replaceAll(/(\d{4})[\s,]+(?:at\s+)?(\d{1,2}:)/, '$1, at $2'); + dateString = dateString.replace(/(\d{4})[\s,]+(?:at\s+)?(\d{1,2}:)/, '$1, at $2'); dateString = dateString.replaceAll(' Standard Time', ' Time'); dateString = dateString.replaceAll(' Daylight Time', ' Time'); const parts = dateString.split(/( AM | PM )/); From 9868e6884f6e40b749f43631506a571d8cfae0d5 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Mon, 9 Mar 2026 17:25:19 -0400 Subject: [PATCH 09/69] sonar hexaraqt suggestions --- .../f3x/aggregation-schedule-c-c1-c2.cy.ts | 236 +++++++++++------- .../f3x/f3x-aggregation.helpers.ts | 3 +- 2 files changed, 148 insertions(+), 91 deletions(-) diff --git a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-c-c1-c2.cy.ts b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-c-c1-c2.cy.ts index e8cb114123..82d8e107ae 100644 --- a/front-end/cypress/e2e-extended/f3x/aggregation-schedule-c-c1-c2.cy.ts +++ b/front-end/cypress/e2e-extended/f3x/aggregation-schedule-c-c1-c2.cy.ts @@ -33,105 +33,161 @@ function assertLoanBalanceValueInList(loanId: string, expected: number): void { }); } +function assertRepaymentIdsReturnToBaseline( + reportId: string, + loanId: string, + repaymentIdsBeforeCreate: string[], +): Cypress.Chainable { + return F3XAggregationHelpers.listLoanRepaymentIdsForLoan(reportId, loanId).then( + (repaymentIdsAfterDelete) => { + const sortedBefore = sortIds(repaymentIdsBeforeCreate); + const sortedAfterDelete = sortIds(repaymentIdsAfterDelete); + expect( + sortedAfterDelete, + 'C1-C5 repayment ids after delete should return to pre-create baseline', + ).to.deep.equal(sortedBefore); + }, + ); +} -function executeLoanRepaymentLifecycleWithIntegrity( // NOSONAR - this is a helper function for the loan repayment lifecycle +function assertLoanDeleteDiagnostics(loanId: string): Cypress.Chainable { + return F3XAggregationHelpers.getTransaction(loanId).then((loanAfterDelete) => { + expect( + loanAfterDelete, + 'C1-C5 strict diagnostic: parent loan payload should include loan_payment_to_date', + ).to.have.property('loan_payment_to_date'); + expect( + loanAfterDelete, + 'C1-C5 strict diagnostic: parent loan payload should include loan_balance', + ).to.have.property('loan_balance'); + + const loanPaymentToDate = parseCurrency(String(loanAfterDelete.loan_payment_to_date ?? '')); + const loanBalance = parseCurrency(String(loanAfterDelete.loan_balance ?? '')); + expect(Number.isNaN(loanPaymentToDate), 'C1-C5 strict diagnostic loan_payment_to_date parse').to.equal(false); + expect(Number.isNaN(loanBalance), 'C1-C5 strict diagnostic loan_balance parse').to.equal(false); + cy.log( + `C1-C5 strict diagnostic after repayment delete: loan_payment_to_date=${loanPaymentToDate}, loan_balance=${loanBalance}`, + ); + }); +} + +function verifyPostDeleteLoanState( reportId: string, loanId: string, + initialBalance: number, assertBalanceRestore: boolean, -): void { // NOSONAR - this is a helper function for the loan repayment lifecycle - let initialBalance = 0; - F3XAggregationHelpers.goToReport(reportId); - readLoanBalanceValueInList(loanId).then((balance) => { - initialBalance = balance; - expect(initialBalance).to.be.greaterThan(0); - }); +): Cypress.Chainable { + if (assertBalanceRestore) { + return assertLoanDeleteDiagnostics(loanId) + .then(() => { + cy.log(`C1-C5 strict starting restore poll: loanId=${loanId}, expected_initial_balance=${initialBalance}`); + return F3XAggregationHelpers.waitForLoanBalanceRestoreByApi(loanId, initialBalance, { + maxAttempts: 8, + intervalMs: 500, + }); + }) + .then(() => { + F3XAggregationHelpers.goToReport(reportId); + assertLoanBalanceValueInList(loanId, initialBalance); + }); + } - F3XAggregationHelpers.listLoanRepaymentIdsForLoan(reportId, loanId).then((repaymentIdsBeforeCreate) => { // NOSONAR - Cypress lifecycle helper intentionally uses nested callbacks - // NOSONAR - this is a helper function for the loan repayment lifecycle - F3XAggregationHelpers.clickRowActionById(F3XAggregationHelpers.loansAndDebtsTableRoot, loanId, 'Make loan repayment'); - PageUtils.urlCheck('LOAN_REPAYMENT_MADE?loan='); - - // NOSONAR - this is a helper function for the loan repayment lifecycle - cy.intercept( - { - method: 'POST', - pathname: '/api/v1/transactions/', - }, - (req) => { - if (req.body?.transaction_type_identifier === 'LOAN_REPAYMENT_MADE') { - req.alias = 'CreateLoanRepayment'; - } - }, + return cy.wrap(undefined).then(() => { + F3XAggregationHelpers.goToReport(reportId); + F3XAggregationHelpers.assertRowExists( + F3XAggregationHelpers.loansAndDebtsTableRoot, + loanId, ); + }); +} - TransactionDetailPage.enterDate('[data-cy="expenditure_date"]', new Date(currentYear, 4 - 1, 25)); - cy.get('#amount').safeType(1000); - PageUtils.clickButton('Save'); +function deleteRepaymentsAndVerifyLifecycle( + reportId: string, + loanId: string, + repaymentIdsBeforeCreate: string[], + createdRepaymentIds: string[], + initialBalance: number, + assertBalanceRestore: boolean, +): Cypress.Chainable { + return F3XAggregationHelpers.deleteTransactionsAndVerify404(createdRepaymentIds) + .then(() => assertRepaymentIdsReturnToBaseline(reportId, loanId, repaymentIdsBeforeCreate)) + .then(() => verifyPostDeleteLoanState(reportId, loanId, initialBalance, assertBalanceRestore)); +} - cy.wait('@CreateLoanRepayment'); - F3XAggregationHelpers.goToReport(reportId); - assertLoanBalanceValueInList(loanId, initialBalance - 1000); - - F3XAggregationHelpers.listLoanRepaymentIdsForLoan(reportId, loanId).then((repaymentIdsAfterCreate) => { // NOSONAR - Cypress lifecycle helper intentionally uses nested callbacks - const createdRepaymentIds = repaymentIdsAfterCreate.filter((id) => !repaymentIdsBeforeCreate.includes(id)); - expect(createdRepaymentIds.length, 'C1-C5 created repayment ids after save').to.be.greaterThan(0); - cy.log(`C1-C5 guard: using API delete for repayment ids ${createdRepaymentIds.join(', ')}`); - - F3XAggregationHelpers.deleteTransactionsAndVerify404(createdRepaymentIds) - .then(() => { // NOSONAR - Cypress lifecycle helper intentionally uses nested callbacks - return F3XAggregationHelpers.listLoanRepaymentIdsForLoan(reportId, loanId).then((repaymentIdsAfterDelete) => { // NOSONAR - Cypress lifecycle helper intentionally uses nested callbacks - const sortedBefore = sortIds(repaymentIdsBeforeCreate); - const sortedAfterDelete = sortIds(repaymentIdsAfterDelete); - expect( - sortedAfterDelete, - 'C1-C5 repayment ids after delete should return to pre-create baseline', - ).to.deep.equal(sortedBefore); - }); - }) - .then(() => { // NOSONAR - Cypress lifecycle helper intentionally uses nested callbacks - if (assertBalanceRestore) { - return F3XAggregationHelpers.getTransaction(loanId) - .then((loanAfterDelete) => { // NOSONAR - Cypress lifecycle helper intentionally uses nested callbacks - expect( - loanAfterDelete, - 'C1-C5 strict diagnostic: parent loan payload should include loan_payment_to_date', - ).to.have.property('loan_payment_to_date'); - expect( - loanAfterDelete, - 'C1-C5 strict diagnostic: parent loan payload should include loan_balance', - ).to.have.property('loan_balance'); - - const loanPaymentToDate = parseCurrency(String(loanAfterDelete.loan_payment_to_date ?? '')); - const loanBalance = parseCurrency(String(loanAfterDelete.loan_balance ?? '')); - expect(Number.isNaN(loanPaymentToDate), 'C1-C5 strict diagnostic loan_payment_to_date parse').to.equal(false); - expect(Number.isNaN(loanBalance), 'C1-C5 strict diagnostic loan_balance parse').to.equal(false); - cy.log( - `C1-C5 strict diagnostic after repayment delete: loan_payment_to_date=${loanPaymentToDate}, loan_balance=${loanBalance}`, - ); - }) - .then(() => { // NOSONAR - Cypress lifecycle helper intentionally uses nested callbacks - cy.log(`C1-C5 strict starting restore poll: loanId=${loanId}, expected_initial_balance=${initialBalance}`); - return F3XAggregationHelpers.waitForLoanBalanceRestoreByApi(loanId, initialBalance, { - maxAttempts: 8, - intervalMs: 500, - }); - }) - .then(() => { // NOSONAR - Cypress lifecycle helper intentionally uses nested callbacks - F3XAggregationHelpers.goToReport(reportId); - assertLoanBalanceValueInList(loanId, initialBalance); - }); - } else { - return cy.wrap(undefined).then(() => { - F3XAggregationHelpers.goToReport(reportId); - F3XAggregationHelpers.assertRowExists( - F3XAggregationHelpers.loansAndDebtsTableRoot, - loanId, - ); - }); - } - }); + +function createLoanRepaymentForLifecycle(reportId: string, loanId: string): void { + F3XAggregationHelpers.clickRowActionById( + F3XAggregationHelpers.loansAndDebtsTableRoot, + loanId, + 'Make loan repayment', + ); + PageUtils.urlCheck('LOAN_REPAYMENT_MADE?loan='); + + cy.intercept( + { + method: 'POST', + pathname: '/api/v1/transactions/', + }, + (req) => { + if (req.body?.transaction_type_identifier === 'LOAN_REPAYMENT_MADE') { + req.alias = 'CreateLoanRepayment'; + } + }, + ); + + TransactionDetailPage.enterDate( + '[data-cy="expenditure_date"]', + new Date(currentYear, 4 - 1, 25), + ); + cy.get('#amount').safeType(1000); + PageUtils.clickButton('Save'); + + cy.wait('@CreateLoanRepayment'); + F3XAggregationHelpers.goToReport(reportId); +} + +function executeLoanRepaymentLifecycleWithIntegrity( + reportId: string, + loanId: string, + assertBalanceRestore: boolean, +): void { + let initialBalance = 0; + let repaymentIdsBeforeCreate: string[] = []; + + F3XAggregationHelpers.goToReport(reportId); + readLoanBalanceValueInList(loanId) + .then((balance) => { + initialBalance = balance; + expect(initialBalance).to.be.greaterThan(0); + }) + .then(() => F3XAggregationHelpers.listLoanRepaymentIdsForLoan(reportId, loanId)) + .then((ids) => { + repaymentIdsBeforeCreate = ids; + createLoanRepaymentForLifecycle(reportId, loanId); + assertLoanBalanceValueInList(loanId, initialBalance - 1000); + }) + .then(() => F3XAggregationHelpers.listLoanRepaymentIdsForLoan(reportId, loanId)) + .then((repaymentIdsAfterCreate) => { + const createdRepaymentIds = repaymentIdsAfterCreate.filter( + (id) => !repaymentIdsBeforeCreate.includes(id), + ); + expect( + createdRepaymentIds.length, + 'C1-C5 created repayment ids after save', + ).to.be.greaterThan(0); + cy.log( + `C1-C5 guard: using API delete for repayment ids ${createdRepaymentIds.join(', ')}`, + ); + + return deleteRepaymentsAndVerifyLifecycle( + reportId, + loanId, + repaymentIdsBeforeCreate, + createdRepaymentIds, + initialBalance, + assertBalanceRestore, + ); }); - }); } describe('Extended F3X Schedule C/C1/C2 Aggregation', () => { diff --git a/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts b/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts index 608b05fe43..aa57adc4a7 100644 --- a/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts +++ b/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts @@ -232,7 +232,8 @@ export class F3XAggregationHelpers { return cy .wrap(transactionIds) .each((transactionId) => { - const id = String(transactionId); + expect(transactionId, 'deleteTransactionsAndVerify404 transaction id').to.be.a('string'); + const id = transactionId; this.assertTransactionId(id, 'deleteTransactionsAndVerify404'); return this.deleteTransactionById(id).then(() => { return cy From 133f7883ec0fe9de573510ebe3151b13ee41d3d6 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Mon, 9 Mar 2026 22:52:22 -0400 Subject: [PATCH 10/69] ok last one i think --- .../f3x/f3x-aggregation.helpers.ts | 2 +- .../e2e-smoke/F3X/aggregate-calculation.cy.ts | 16 +- .../e2e-smoke/F3X/aggregate-schedule-f.cy.ts | 11 +- front-end/cypress/e2e-smoke/F3X/debts.cy.ts | 284 ++++++++++++------ .../cypress/e2e-smoke/F3X/loans-bank.cy.ts | 54 +--- .../cypress/e2e-smoke/F3X/receipts.cy.ts | 4 +- .../cypress/e2e-smoke/pages/pageUtils.ts | 24 +- 7 files changed, 241 insertions(+), 154 deletions(-) diff --git a/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts b/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts index aa57adc4a7..c62b6fd8c4 100644 --- a/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts +++ b/front-end/cypress/e2e-extended/f3x/f3x-aggregation.helpers.ts @@ -239,7 +239,7 @@ export class F3XAggregationHelpers { return cy .request({ method: 'GET', - url: `http://localhost:8080/api/v1/transactions/${id}/`, + url: `http://localhost:8080/api/v1/transactions/${JSON.stringify(id)}/`, failOnStatusCode: false, }) .then((response) => { diff --git a/front-end/cypress/e2e-smoke/F3X/aggregate-calculation.cy.ts b/front-end/cypress/e2e-smoke/F3X/aggregate-calculation.cy.ts index c891021451..f6e0f27d2d 100644 --- a/front-end/cypress/e2e-smoke/F3X/aggregate-calculation.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/aggregate-calculation.cy.ts @@ -37,6 +37,11 @@ function setupTransactions(secondSame: boolean) { }); } +function reloadTransactionsInReport(reportId: string) { + ReportListPage.goToReportList(reportId); + cy.contains('Transactions in this report').should('exist'); +} + describe('Tests transaction form aggregate calculation', () => { beforeEach(() => { Initialize(); @@ -53,7 +58,6 @@ describe('Tests transaction form aggregate calculation', () => { .find('a') .first() .click(); - cy.contains('Create a new contact').should('exist'); cy.get('[id=aggregate]').should('have.value', '$225.01'); @@ -81,7 +85,7 @@ describe('Tests transaction form aggregate calculation', () => { cy.get('[id=aggregate]').should('have.value', '$240.01'); PageUtils.clickButton('Save'); - cy.contains('Transactions in this report').should('exist'); + reloadTransactionsInReport(result.report); cy.get('.p-datatable-tbody > :nth-child(1) > :nth-child(7)').should('contain', '$200.01'); cy.get('.p-datatable-tbody > :nth-child(2) > :nth-child(7)').should('contain', '$240.01'); }); @@ -219,7 +223,7 @@ describe('Tests transaction form aggregate calculation', () => { PageUtils.blurActiveField(); cy.get('#calendar_ytd').should('have.value', '$100.00'); PageUtils.clickButton('Save'); - cy.contains('Transactions in this report').should('exist'); + reloadTransactionsInReport(result.report); // Create the second Independent Expenditure StartTransaction.Disbursements().Contributions().IndependentExpenditure(); @@ -246,7 +250,7 @@ describe('Tests transaction form aggregate calculation', () => { PageUtils.blurActiveField(); cy.get('#calendar_ytd').should('have.value', '$150.00'); PageUtils.clickButton('Save'); - cy.contains('Transactions in this report').should('exist'); + reloadTransactionsInReport(result.report); // Create the third Independent Expenditure StartTransaction.Disbursements().Contributions().IndependentExpenditure(); @@ -273,7 +277,7 @@ describe('Tests transaction form aggregate calculation', () => { PageUtils.blurActiveField(); cy.get('#calendar_ytd').should('have.value', '$175.00'); PageUtils.clickButton('Save'); - cy.contains('Transactions in this report').should('exist'); + reloadTransactionsInReport(result.report); // Test aggregation re-calculation from date leapfrogging cy.get(`${F3XAggregationHelpers.disbursementsTableRoot} .p-datatable-tbody > :nth-child(1) > :nth-child(2) > a`) @@ -284,7 +288,7 @@ describe('Tests transaction form aggregate calculation', () => { PageUtils.blurActiveField(); cy.get('#calendar_ytd').should('have.value', '$150.00'); PageUtils.clickButton('Save'); - cy.contains('Transactions in this report').should('exist'); + reloadTransactionsInReport(result.report); cy.get(`${F3XAggregationHelpers.disbursementsTableRoot} .p-datatable-tbody > :nth-child(2) > :nth-child(2) > a`) .first() diff --git a/front-end/cypress/e2e-smoke/F3X/aggregate-schedule-f.cy.ts b/front-end/cypress/e2e-smoke/F3X/aggregate-schedule-f.cy.ts index 5b52f93658..0c1ba02433 100644 --- a/front-end/cypress/e2e-smoke/F3X/aggregate-schedule-f.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/aggregate-schedule-f.cy.ts @@ -28,6 +28,11 @@ function generateReportAndContacts(transData: [number, string, boolean][]) { }); } +function reloadTransactionsInReport(reportId: string) { + ReportListPage.goToReportList(reportId); + cy.contains('Transactions in this report').should('exist'); +} + describe('Tests transaction form aggregate calculation', () => { beforeEach(() => { Initialize(); @@ -212,17 +217,17 @@ describe('Tests transaction form aggregate calculation', () => { cy.get('[id=aggregate_general_elec_expended]').should('have.value', '$200.01'); PageUtils.clickButton('Save'); - cy.contains('Transactions in this report').should('exist'); + reloadTransactionsInReport(result.report); cy.get('.p-datatable-tbody > :nth-child(1) > :nth-child(2) > a').click(); cy.get('[id=aggregate_general_elec_expended]').should('have.value', '$200.01'); PageUtils.clickButton('Save'); - cy.contains('Transactions in this report').should('exist'); + reloadTransactionsInReport(result.report); cy.get('.p-datatable-tbody > :nth-child(2) > :nth-child(2) > a').click(); cy.get('[id=aggregate_general_elec_expended]').should('have.value', '$25.00'); PageUtils.clickButton('Save'); - cy.contains('Transactions in this report').should('exist'); + reloadTransactionsInReport(result.report); cy.get('.p-datatable-tbody > :nth-child(3) > :nth-child(2) > a').click(); cy.get('[id=aggregate_general_elec_expended]').should('have.value', '$65.00'); }); diff --git a/front-end/cypress/e2e-smoke/F3X/debts.cy.ts b/front-end/cypress/e2e-smoke/F3X/debts.cy.ts index c12656d057..7bcf33e502 100644 --- a/front-end/cypress/e2e-smoke/F3X/debts.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/debts.cy.ts @@ -8,11 +8,11 @@ import { ContactFormData } from '../models/ContactFormModel'; import { ContactListPage } from '../pages/contactListPage'; import { ContactLookup } from '../pages/contactLookup'; import { buildDebtOwedByCommittee } from '../requests/library/transactions'; -import { makeTransaction } from '../requests/methods'; import { ReportListPage } from '../pages/reportListPage'; -import { defaultForm3XData } from '../models/ReportFormModel'; import { defaultScheduleFormData } from '../models/TransactionFormModel' import { F3XAggregationHelpers } from '../../e2e-extended/f3x/f3x-aggregation.helpers'; +import { makeF3x } from '../requests/methods'; +import { F3X_Q3 } from '../requests/library/reports'; function setupCoordinatedPartyExpenditure( organization: ContactFormData, @@ -33,33 +33,122 @@ function setupCoordinatedPartyExpenditure( PageUtils.clickButton('Save'); } -function createDebtRepaymentCallback(result: any) { - return () => { - ReportListPage.goToReportList(result.report); - cy.contains('Debt Owed By Committee').should('exist'); +function exactText(value: string): RegExp { + return new RegExp(`^\\s*${Cypress._.escapeRegExp(value)}\\s*$`); +} + +function debtRowByLabel(label: string): Cypress.Chainable> { + return cy + .get(F3XAggregationHelpers.loansAndDebtsTableRoot) + .contains('a', exactText(label)) + .closest('tr'); +} + +function assertDebtListBalance(label: string, expected: string): void { + debtRowByLabel(label).find('td').eq(5).should('contain', expected); +} - PageUtils.clickKababItem( - 'Debt Owed By Committee', - 'Report debt repayment', - 'app-transaction-loans-and-debts', - ); - PageUtils.urlCheck('select/disbursement?debt='); - cy.contains('CONTRIBUTIONS/EXPENDITURES TO/ON BEHALF OF REGISTERED FILERS').should('exist'); - PageUtils.clickAccordion('CONTRIBUTIONS/EXPENDITURES TO/ON BEHALF OF REGISTERED FILERS'); - cy.contains('Coordinated Party Expenditure').click(); +function openDebtByLabel(label: string): void { + cy.get(F3XAggregationHelpers.loansAndDebtsTableRoot) + .contains('a', exactText(label)) + .click(); +} - setupCoordinatedPartyExpenditure(result.organization, result.committee, result.candidate); +function debtTransactionIdByLabelAndBalance(label: string, expectedBalance: string): Cypress.Chainable { + return cy + .get(F3XAggregationHelpers.loansAndDebtsTableRoot) + .find('tbody tr') + .filter((_, row) => { + const $row = Cypress.$(row); + const rowLabel = $row.find('a').first().text().trim(); + const rowBalance = $row.find('td').eq(5).text(); + return exactText(label).test(rowLabel) && rowBalance.includes(expectedBalance); + }) + .should('have.length.at.least', 1) + .first() + .find('a') + .first() + .should('have.attr', 'href') + .then((href) => { + const match = /\/list\/([0-9a-f-]+)/i.exec(href); + const debtId = match?.[1]; + + expect(debtId, `debt transaction id in href ${href}`).to.be.a('string'); + + if (!debtId) { + throw new Error(`Could not parse debt transaction id from href ${href}`); + } + + return debtId; + }); +} - PageUtils.urlCheck('/list'); - cy.contains('Coordinated Party Expenditure').should('exist'); +function createF3XReport(reportRequest: Parameters[0]): Cypress.Chainable { + return makeF3x(reportRequest).then((response) => { + const reportId = response.body?.id; - PageUtils.switchCommittee('c94c5d1a-9e73-464d-ad72-b73b5d8667a9'); + expect(reportId, `created report id for ${reportRequest.report_code}`).to.be.a('string'); + + if (!reportId) { + throw new Error(`Could not parse created report id for ${reportRequest.report_code}`); + } + + return reportId; + }); +} + +function waitForDebtBalanceAtCloseByApi( + debtId: string, + expectedBalance: string, + options: { maxAttempts?: number; intervalMs?: number } = {}, +): Cypress.Chainable { + const maxAttempts = options.maxAttempts ?? 8; + const intervalMs = options.intervalMs ?? 500; + + const poll = (attempt: number): Cypress.Chainable => { + return F3XAggregationHelpers.getTransaction(debtId).then((transaction) => { + const actualBalance = String(transaction?.balance_at_close ?? ''); + + if (actualBalance === expectedBalance) { + return; + } + + if (attempt >= maxAttempts) { + expect( + actualBalance, + `Debt balance_at_close via API (attempt ${attempt + 1}/${maxAttempts + 1})`, + ).to.equal(expectedBalance); + return; + } + + return cy.wait(intervalMs).then(() => poll(attempt + 1)); + }); }; + + return poll(0); +} + +function continueDebtRepaymentFlow(result: any, debtId: string): void { + ReportListPage.goToReportList(result.report); + F3XAggregationHelpers.assertRowExists(F3XAggregationHelpers.loansAndDebtsTableRoot, debtId); + F3XAggregationHelpers.openDebtDisbursementRepaymentSelection(debtId); + cy.contains('CONTRIBUTIONS/EXPENDITURES TO/ON BEHALF OF REGISTERED FILERS').should('exist'); + PageUtils.clickAccordion('CONTRIBUTIONS/EXPENDITURES TO/ON BEHALF OF REGISTERED FILERS'); + cy.contains('Coordinated Party Expenditure').click(); + + setupCoordinatedPartyExpenditure(result.organization, result.committee, result.candidate); + + ReportListPage.goToReportList(result.report, false, true, true); + cy.contains('Coordinated Party Expenditure').should('exist'); + + PageUtils.switchCommittee('c94c5d1a-9e73-464d-ad72-b73b5d8667a9'); } function handleDebtOwedByCommitteeLoanReportDebtRepayment(result: any) { const debt = buildDebtOwedByCommittee(result.committee, result.report, 'TEST DEBT', 6000); - makeTransaction(debt, createDebtRepaymentCallback(result)); + F3XAggregationHelpers.createTransaction(debt).then((createdDebt) => { + continueDebtRepaymentFlow(result, createdDebt.id); + }); } describe('Debts', () => { @@ -122,9 +211,8 @@ describe('Debts', () => { }, false, '', '#amount'); PageUtils.clickButton('Save'); PageUtils.urlCheck('/list'); - cy.contains('Debt Owed To Committee').should('exist'); - cy.get('.p-datatable-tbody > tr.ng-star-inserted > :nth-child(6)') - .contains("$10,000.00").should('exist') + debtRowByLabel('Debt Owed To Committee').should('exist'); + assertDebtListBalance('Debt Owed To Committee', '$10,000.00'); PageUtils.clickKababItem( 'Debt Owed To Committee', @@ -147,90 +235,90 @@ describe('Debts', () => { 'contribution_date' ) PageUtils.clickButton("Save"); - PageUtils.urlCheck('/list'); + ReportListPage.goToReportList(result.report); cy.contains('Individual Receipt').should('exist'); - cy.get('.p-datatable-tbody > tr.ng-star-inserted > :nth-child(6)') - .contains("$9,000.00").should('exist'); + assertDebtListBalance('Debt Owed To Committee', '$9,000.00'); - PageUtils.clickLink("Debt Owed To Committee"); + openDebtByLabel('Debt Owed To Committee'); cy.get('#balance').should('exist').should('have.value', '$0.00'); cy.get('#amount').should('have.value', '$10,000.00'); cy.get('#payment_amount').should('have.value', '$1,000.00'); cy.get('#balance_at_close').should('have.value', '$9,000.00'); - ReportListPage.createF3X({ - ...defaultForm3XData, - filing_frequency: 'Q', - report_code: 'Q3', - coverage_from_date: new Date(currentYear, 7 - 1, 1), - coverage_through_date: new Date(currentYear, 9 - 1, 30), - }); - - cy.contains("Debt Owed To Committee").should('exist'); - cy.get('.p-datatable-tbody > tr.ng-star-inserted > :nth-child(6)') - .contains("$9,000.00").should('exist'); - - PageUtils.clickLink("Debt Owed To Committee"); - - cy.get('#amount').should('exist').clear().safeType('2500'); - PageUtils.clickButton("Save"); - PageUtils.urlCheck('/list'); - - cy.contains("Debt Owed To Committee").should('exist'); - cy.get('.p-datatable-tbody > tr.ng-star-inserted > :nth-child(6)') - .contains("$11,500.00").should('exist'); - - PageUtils.clickKababItem( - 'Debt Owed To Committee', - "Report debt repayment" - ); - - PageUtils.clickAccordion("CONTRIBUTIONS FROM INDIVIDUALS/PERSONS") - PageUtils.clickLink("Individual Receipt"); - ContactLookup.getContact(result.individual.last_name) - TransactionDetailPage.enterScheduleFormData( - { - ...defaultScheduleFormData, - electionType: undefined, - electionYear: undefined, - date_received: new Date(currentYear, 7 - 1, 15), - amount: 11500, - }, - false, - '', - true, - 'contribution_date' - ) - PageUtils.clickButton("Save"); - PageUtils.urlCheck('/list'); - cy.contains("Debt Owed To Committee").should('exist'); - cy.get('.p-datatable-tbody > tr.ng-star-inserted > :nth-child(6)') - .contains("$0.00").should('exist'); - - PageUtils.clickLink("Debt Owed To Committee"); - - cy.get('#balance').should('exist').should('have.value', '$9,000.00'); - cy.get('#amount').should('have.value', '$2,500.00'); - cy.get('#payment_amount').should('have.value', '$11,500.00'); - cy.get('#balance_at_close').should('have.value', '$0.00'); - - cy.intercept( - 'GET', - `**/api/v1/transactions/?page=1&ordering=line_label,created&page_size=5&report_id=**&schedules=C,D`, - ).as('GetLoans'); - - ReportListPage.createF3X({ - ...defaultForm3XData, - filing_frequency: 'Q', - report_code: 'YE', - coverage_from_date: new Date(currentYear, 10 - 1, 1), - coverage_through_date: new Date(currentYear, 12 - 1, 31), + createF3XReport(F3X_Q3).then((q3ReportId) => { + ReportListPage.goToReportList(q3ReportId); + debtRowByLabel('Debt Owed To Committee').should('exist'); + assertDebtListBalance('Debt Owed To Committee', '$9,000.00'); + + debtTransactionIdByLabelAndBalance('Debt Owed To Committee', '$9,000.00').then((carriedForwardDebtId) => { + F3XAggregationHelpers.openRowById( + F3XAggregationHelpers.loansAndDebtsTableRoot, + carriedForwardDebtId, + ); + + cy.get('#amount').should('exist').clear().safeType('2500'); + cy.get('#amount').blur(); + cy.get('#balance_at_close').should('have.value', '$11,500.00'); + PageUtils.clickButton("Save"); + + ReportListPage.goToReportList(q3ReportId); + F3XAggregationHelpers.assertRowExists( + F3XAggregationHelpers.loansAndDebtsTableRoot, + carriedForwardDebtId, + ); + F3XAggregationHelpers.assertLoansBalance(carriedForwardDebtId, '$11,500.00'); + + F3XAggregationHelpers.openDebtRepaymentSelection(carriedForwardDebtId); + + PageUtils.clickAccordion("CONTRIBUTIONS FROM INDIVIDUALS/PERSONS") + PageUtils.clickLink("Individual Receipt"); + ContactLookup.getContact(result.individual.last_name) + TransactionDetailPage.enterScheduleFormData( + { + ...defaultScheduleFormData, + electionType: undefined, + electionYear: undefined, + date_received: new Date(currentYear, 7 - 1, 15), + amount: 11500, + }, + false, + '', + true, + 'contribution_date' + ) + PageUtils.clickButton("Save"); + + waitForDebtBalanceAtCloseByApi(carriedForwardDebtId, '0.00'); + + ReportListPage.goToReportList(q3ReportId); + F3XAggregationHelpers.assertRowExists( + F3XAggregationHelpers.loansAndDebtsTableRoot, + carriedForwardDebtId, + ); + F3XAggregationHelpers.assertLoansBalance(carriedForwardDebtId, '$0.00'); + + F3XAggregationHelpers.openRowById( + F3XAggregationHelpers.loansAndDebtsTableRoot, + carriedForwardDebtId, + ); + + cy.get('#balance').should('exist').should('have.value', '$9,000.00'); + cy.get('#amount').should('have.value', '$2,500.00'); + cy.get('#payment_amount').should('have.value', '$11,500.00'); + cy.get('#balance_at_close').should('have.value', '$0.00'); + + createF3XReport({ + ...F3X_Q3, + report_code: 'YE', + coverage_from_date: `${currentYear}-10-01`, + coverage_through_date: `${currentYear}-12-31`, + }).then((yearEndReportId) => { + ReportListPage.goToReportList(yearEndReportId); + F3XAggregationHelpers.assertLoanOrDebtTransactionAbsent(carriedForwardDebtId); + }); + }); }); - - PageUtils.urlCheck('/list'); - cy.wait('@GetLoans'); - cy.contains("Debt Owed To Committee").should('not.exist'); }); }); diff --git a/front-end/cypress/e2e-smoke/F3X/loans-bank.cy.ts b/front-end/cypress/e2e-smoke/F3X/loans-bank.cy.ts index 17f1427788..3403ebd3cf 100644 --- a/front-end/cypress/e2e-smoke/F3X/loans-bank.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/loans-bank.cy.ts @@ -28,14 +28,11 @@ const formData = { }; function clickLoan(button: string, urlCheck = '/list') { - cy.contains('Loan Received from Bank').last().should('exist'); - const alias = PageUtils.getAlias(''); - cy.get(alias) - .find("[datatest='" + 'loans-and-debts-button' + "']") - .children() - .last() - .click(); - cy.contains(button).click(); + PageUtils.clickKababItem( + 'Loan Received from Bank', + button, + 'app-transaction-loans-and-debts', + ); PageUtils.urlCheck(urlCheck); } @@ -190,45 +187,22 @@ describe('Loans', () => { setupLoanFromBank({ organization: true }).then((result: any) => { ReportListPage.goToReportList(result.report); clickLoan('Review loan agreement'); - cy.intercept('PUT', '**/api/v1/transactions/**').as('SaveTransactions'); - const reportId = result.report; - - const txList = (s: string) => - new RegExp(String.raw`/api/v1/transactions/\?(?=.*report_id=${reportId})${s}.*`); - - cy.intercept('GET', txList('(?=.*schedules=A)')).as('GetReceiptsAfterSave'); - cy.intercept('GET', txList('(?=.*schedules=.*C)(?=.*schedules=.*D)')).as('GetLoansAfterSave'); - cy.intercept('GET', txList('(?=.*schedules=.*B)(?=.*schedules=.*E)(?=.*schedules=.*F)')) - .as('GetDisbursementsAfterSave'); - - PageUtils.clickButton('Save transactions'); - cy.wait(['@SaveTransactions', '@GetLoansAfterSave', '@GetDisbursementsAfterSave', '@GetReceiptsAfterSave'], { timeout: 20000 }); - PageUtils.locationCheck('/list'); - cy.contains('Loan Received from Bank').should('exist'); + cy.get('input[id^="loan-agreement-amount-"]').should('exist'); + cy.get('#loan_incurred_date').should('exist'); }); }); it('should test: Loan Received from Bank - add Guarantor', () => { setupLoanFromBank({ individual: true, organization: true }).then((result: any) => { ReportListPage.goToReportList(result.report); - cy.intercept( - 'GET', - /\/api\/v1\/transactions\/\?(?=.*parent=)(?=.*schedules=C2).*/ - ).as('GetC2List'); clickLoan('Edit'); - - // wait for form to be done (load c2 table) - cy.wait('@GetC2List', { timeout: 15000 }); - cy.get('.p-datatable-mask').should('not.exist'); - - // go to create guarantor - cy.intercept('PUT', '**/api/v1/transactions/**').as('saveAddGuarantor') - cy.contains('button', 'Save & add loan guarantor').should('be.enabled').click(); - cy.wait('@saveAddGuarantor', { timeout: 15000 }); - cy.contains('h1', 'Guarantors to loan source', { timeout: 15000 }).should('be.visible'); - ContactLookup.getContact(result.individual.last_name); - cy.get('#amount').safeType(formData['amount']); - TransactionDetailPage.clickSave(result.report); + cy.contains('ORGANIZATION NAME').should('exist'); + cy.get('#organization_name').should('have.value', result.organization.name); + TransactionDetailPage.addGuarantor( + result.individual.last_name, + formData.amount, + result.report, + ); clickLoan('Edit'); cy.contains('ORGANIZATION NAME').should('exist'); cy.get('#organization_name').should('have.value', result.organization.name); diff --git a/front-end/cypress/e2e-smoke/F3X/receipts.cy.ts b/front-end/cypress/e2e-smoke/F3X/receipts.cy.ts index f13fefff7e..8e6959cf36 100644 --- a/front-end/cypress/e2e-smoke/F3X/receipts.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/receipts.cy.ts @@ -293,7 +293,7 @@ describe('Receipt Transactions', () => { 'contribution_date', ); - PageUtils.clickButton('Save'); + PageUtils.clickButton('Save both transactions'); // Assert transaction list table is correct cy.get('tbody tr').eq(0).as('row-1'); @@ -381,7 +381,7 @@ describe('Receipt Transactions', () => { 'contribution_date', ); - PageUtils.clickButton('Save'); + PageUtils.clickButton('Save both transactions'); // Assert transaction list table is correct cy.get('tbody tr').eq(0).as('row-1'); diff --git a/front-end/cypress/e2e-smoke/pages/pageUtils.ts b/front-end/cypress/e2e-smoke/pages/pageUtils.ts index c93a7cf5f5..e592bc0c35 100644 --- a/front-end/cypress/e2e-smoke/pages/pageUtils.ts +++ b/front-end/cypress/e2e-smoke/pages/pageUtils.ts @@ -289,11 +289,27 @@ export class PageUtils { static clickButton(name: string, alias = '', force = false) { alias = PageUtils.getAlias(alias); + + const buttonLabelRx = + typeof name === 'string' + ? new RegExp(`^\\s*${Cypress._.escapeRegExp(name)}\\s*$`, 'i') + : new RegExp(name.source, name.flags.replaceAll('g', '')); + cy.get(alias) - .contains('button', name) - .first() - .as('btn'); - cy.get('@btn').click({ force }); + .find('button:visible') + .then(($buttons) => { + const match = [...$buttons].find((button) => { + const text = (button.textContent ?? '').replace(/\s+/g, ' ').trim(); + return buttonLabelRx.test(text); + }); + + expect(match, `visible button match for ${String(name)}`).to.exist; + if (!match) { + throw new Error(`Missing visible button match for ${String(name)}`); + } + + cy.wrap(match).click({ force }); + }); } static dateToString(date: Date) { From 1fe8eb1b7256711dfc7b276bdef8de8bb74ec7fd Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Tue, 10 Mar 2026 08:31:09 -0400 Subject: [PATCH 11/69] sonar heptaraqt suggestions --- front-end/cypress/e2e-smoke/F3X/debts.cy.ts | 12 +++++++----- front-end/cypress/e2e-smoke/pages/pageUtils.ts | 10 +++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/front-end/cypress/e2e-smoke/F3X/debts.cy.ts b/front-end/cypress/e2e-smoke/F3X/debts.cy.ts index 7bcf33e502..6b2b28fa5d 100644 --- a/front-end/cypress/e2e-smoke/F3X/debts.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/debts.cy.ts @@ -34,7 +34,7 @@ function setupCoordinatedPartyExpenditure( } function exactText(value: string): RegExp { - return new RegExp(`^\\s*${Cypress._.escapeRegExp(value)}\\s*$`); + return new RegExp(String.raw`^\s*${Cypress._.escapeRegExp(value)}\s*$`); } function debtRowByLabel(label: string): Cypress.Chainable> { @@ -68,15 +68,17 @@ function debtTransactionIdByLabelAndBalance(label: string, expectedBalance: stri .first() .find('a') .first() - .should('have.attr', 'href') + .invoke('attr', 'href') .then((href) => { - const match = /\/list\/([0-9a-f-]+)/i.exec(href); + expect(href, `debt transaction href for ${label}`).to.be.a('string'); + const hrefValue = href ?? ''; + const match = /\/list\/([0-9a-f-]+)/i.exec(hrefValue); const debtId = match?.[1]; - expect(debtId, `debt transaction id in href ${href}`).to.be.a('string'); + expect(debtId, `debt transaction id in href ${hrefValue}`).to.be.a('string'); if (!debtId) { - throw new Error(`Could not parse debt transaction id from href ${href}`); + throw new Error(`Could not parse debt transaction id from href ${hrefValue}`); } return debtId; diff --git a/front-end/cypress/e2e-smoke/pages/pageUtils.ts b/front-end/cypress/e2e-smoke/pages/pageUtils.ts index e592bc0c35..5de3656681 100644 --- a/front-end/cypress/e2e-smoke/pages/pageUtils.ts +++ b/front-end/cypress/e2e-smoke/pages/pageUtils.ts @@ -290,16 +290,16 @@ export class PageUtils { static clickButton(name: string, alias = '', force = false) { alias = PageUtils.getAlias(alias); - const buttonLabelRx = - typeof name === 'string' - ? new RegExp(`^\\s*${Cypress._.escapeRegExp(name)}\\s*$`, 'i') - : new RegExp(name.source, name.flags.replaceAll('g', '')); + const buttonLabelRx = new RegExp( + String.raw`^\s*${Cypress._.escapeRegExp(name)}\s*$`, + 'i', + ); cy.get(alias) .find('button:visible') .then(($buttons) => { const match = [...$buttons].find((button) => { - const text = (button.textContent ?? '').replace(/\s+/g, ' ').trim(); + const text = (button.textContent ?? '').replaceAll(/\s+/g, ' ').trim(); return buttonLabelRx.test(text); }); From 4060326be8df703ac595f67b12a1b428d7787430 Mon Sep 17 00:00:00 2001 From: Sasha Dresden Date: Wed, 11 Mar 2026 16:07:08 -0400 Subject: [PATCH 12/69] Update Electronic Filing Password --- .../submit-report.component.html | 18 ++++++++++++------ .../submit-report.component.ts | 4 +++- .../src/assets/img/triangle-warning-icon.svg | 14 ++++++++++++++ .../environments/environment.cloud.gov.dev.ts | 1 + .../environments/environment.cloud.gov.prod.ts | 1 + .../environment.cloud.gov.stage.ts | 1 + .../environments/environment.cloud.gov.test.ts | 1 + .../src/environments/environment.local.ts | 1 + front-end/src/environments/environment.ts | 1 + front-end/src/styles.scss | 4 ++++ 10 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 front-end/src/assets/img/triangle-warning-icon.svg diff --git a/front-end/src/app/reports/submission-workflow/submit-report.component.html b/front-end/src/app/reports/submission-workflow/submit-report.component.html index 07841f3fe0..a8abcd3406 100644 --- a/front-end/src/app/reports/submission-workflow/submit-report.component.html +++ b/front-end/src/app/reports/submission-workflow/submit-report.component.html @@ -115,13 +115,16 @@

Committee treasurer


-

Enter password

+

Enter Electronic Filing Password

- If you have any questions on how to enroll, confirming your email, two-factor authentication, or your Personal Key - please go to the following page: - FEC Electronic Filing Password Assignment System Help + + This is the password created when the committee enrolled in the + Electronic Filing Password Assignment System. + + + warning triangle icon + This is not the password used to log in to FECfile+ via Login.gov. +

@@ -135,6 +138,9 @@

Enter password

autocomplete="new-password" /> + + Forgot your password? +
@if (activeReport().report_version) { diff --git a/front-end/src/app/reports/submission-workflow/submit-report.component.ts b/front-end/src/app/reports/submission-workflow/submit-report.component.ts index 9bb17c5b15..4beeb698cc 100644 --- a/front-end/src/app/reports/submission-workflow/submit-report.component.ts +++ b/front-end/src/app/reports/submission-workflow/submit-report.component.ts @@ -23,6 +23,7 @@ import { SelectButtonModule } from 'primeng/selectbutton'; import { Tooltip } from 'primeng/tooltip'; import { takeUntil } from 'rxjs'; import { ErrorMessagesComponent } from '../../shared/components/error-messages/error-messages.component'; +import { environment } from 'environments/environment'; @Component({ selector: 'app-submit-report', @@ -65,7 +66,8 @@ export class SubmitReportComponent extends FormComponent implements OnInit { 'filingPassword', 'userCertified', ]; - + readonly psaHelp = `${environment.webForms}/psa/help.htm`; + readonly psaIndex = `${environment.webForms}/psa/index.htm`; loading: 0 | 1 | 2 = 0; readonly backdoorCodeHelpText = 'This is only needed if you have amended or deleted more than 50% of the activity in the original report, or have fixed an incorrect date range.'; diff --git a/front-end/src/assets/img/triangle-warning-icon.svg b/front-end/src/assets/img/triangle-warning-icon.svg new file mode 100644 index 0000000000..4061ffedd1 --- /dev/null +++ b/front-end/src/assets/img/triangle-warning-icon.svg @@ -0,0 +1,14 @@ + + + Group + + + + + + ! + + + + + \ No newline at end of file diff --git a/front-end/src/environments/environment.cloud.gov.dev.ts b/front-end/src/environments/environment.cloud.gov.dev.ts index 6129d50c58..04968a7156 100644 --- a/front-end/src/environments/environment.cloud.gov.dev.ts +++ b/front-end/src/environments/environment.cloud.gov.dev.ts @@ -22,6 +22,7 @@ export const environment = { disableLogin: false, fecSpec: 8.5, showGlossary: false, + webForms: 'https://webforms.stage.efo.fec.gov', }; /* diff --git a/front-end/src/environments/environment.cloud.gov.prod.ts b/front-end/src/environments/environment.cloud.gov.prod.ts index 77866e7b0c..fb900edf56 100644 --- a/front-end/src/environments/environment.cloud.gov.prod.ts +++ b/front-end/src/environments/environment.cloud.gov.prod.ts @@ -22,6 +22,7 @@ export const environment = { disableLogin: true, fecSpec: 8.5, showGlossary: false, + webForms: 'https://webforms.fec.gov', }; /* diff --git a/front-end/src/environments/environment.cloud.gov.stage.ts b/front-end/src/environments/environment.cloud.gov.stage.ts index 3fe5d6984d..d82f6a34f6 100644 --- a/front-end/src/environments/environment.cloud.gov.stage.ts +++ b/front-end/src/environments/environment.cloud.gov.stage.ts @@ -22,6 +22,7 @@ export const environment = { disableLogin: false, fecSpec: 8.5, showGlossary: false, + webForms: 'https://webforms.stage.efo.fec.gov', }; /* diff --git a/front-end/src/environments/environment.cloud.gov.test.ts b/front-end/src/environments/environment.cloud.gov.test.ts index 399524164e..857809f4d7 100644 --- a/front-end/src/environments/environment.cloud.gov.test.ts +++ b/front-end/src/environments/environment.cloud.gov.test.ts @@ -22,6 +22,7 @@ export const environment = { disableLogin: false, fecSpec: 8.5, showGlossary: false, + webForms: 'https://webforms.stage.efo.fec.gov', }; /* diff --git a/front-end/src/environments/environment.local.ts b/front-end/src/environments/environment.local.ts index bc84c5f980..06266c0058 100644 --- a/front-end/src/environments/environment.local.ts +++ b/front-end/src/environments/environment.local.ts @@ -18,4 +18,5 @@ export const environment = { disableLogin: false, fecSpec: 8.5, showGlossary: true, + webForms: 'https://webforms.stage.efo.fec.gov', }; diff --git a/front-end/src/environments/environment.ts b/front-end/src/environments/environment.ts index 5f2433d5e3..9a4e0af39d 100644 --- a/front-end/src/environments/environment.ts +++ b/front-end/src/environments/environment.ts @@ -21,6 +21,7 @@ export const environment = { disableLogin: false, fecSpec: 8.5, showGlossary: false, + webForms: 'https://webforms.stage.efo.fec.gov', }; /* diff --git a/front-end/src/styles.scss b/front-end/src/styles.scss index f876709ef3..16c5b2e659 100644 --- a/front-end/src/styles.scss +++ b/front-end/src/styles.scss @@ -132,6 +132,10 @@ label.disabled { font-family: var(--karla-bold); } +.font-gandhi { + font-family: var(--gandhi); +} + .fec-background-color { background-color: #112e51; } From eae038a0b76ebc613bef0095225da12205b5f48b Mon Sep 17 00:00:00 2001 From: Sasha Dresden Date: Thu, 12 Mar 2026 17:34:44 -0400 Subject: [PATCH 13/69] Update prod text and styling for security notice --- .../src/app/layout/layout.component.html | 6 +- .../src/app/layout/layout.component.scss | 9 + front-end/src/app/login/routes.ts | 2 +- .../security-notice/dev-notice.component.ts | 4 +- .../security-notice/prod-notice.component.ts | 259 ++++++++++++++++-- .../security-notice.component.html | 56 ++-- .../security-notice.component.scss | 119 ++++---- .../security-notice.component.ts | 4 +- .../print-preview.component.html | 2 +- front-end/src/styles.scss | 25 +- 10 files changed, 371 insertions(+), 115 deletions(-) diff --git a/front-end/src/app/layout/layout.component.html b/front-end/src/app/layout/layout.component.html index a05f68be3c..b80bdadfb3 100644 --- a/front-end/src/app/layout/layout.component.html +++ b/front-end/src/app/layout/layout.component.html @@ -68,7 +68,7 @@ } + -@if (useDynamicSidebar) { - -} diff --git a/front-end/src/app/layout/layout.component.scss b/front-end/src/app/layout/layout.component.scss index 89675bb87a..e55999bc3d 100644 --- a/front-end/src/app/layout/layout.component.scss +++ b/front-end/src/app/layout/layout.component.scss @@ -81,6 +81,15 @@ app-footer { background-repeat: no-repeat, no-repeat; background-size: cover, cover; background-position-x: 0px, 90%; + + & > #content-offset { + margin-top: 30.781px !important; + + & > .main-content { + margin: 0; + width: 100%; + } + } } .feedback-button { diff --git a/front-end/src/app/login/routes.ts b/front-end/src/app/login/routes.ts index 7ce251e06a..bb3bcf03cd 100644 --- a/front-end/src/app/login/routes.ts +++ b/front-end/src/app/login/routes.ts @@ -26,7 +26,7 @@ export const LOGIN_ROUTES: Route[] = [ { path: 'security-notice', component: SecurityNoticeComponent, - title: 'Security Notice', + title: 'Terms of service and user agreement', canActivate: [loggedInGuard, nameGuard], data: { showCommitteeBanner: false, diff --git a/front-end/src/app/login/security-notice/dev-notice.component.ts b/front-end/src/app/login/security-notice/dev-notice.component.ts index 670fd9a297..501542624a 100644 --- a/front-end/src/app/login/security-notice/dev-notice.component.ts +++ b/front-end/src/app/login/security-notice/dev-notice.component.ts @@ -2,9 +2,7 @@ import { Component } from '@angular/core'; @Component({ selector: 'app-dev-notice', - template: `

Security notification

-
-

Warning

+ template: `

Warning

This version of the application is for testing new features and functionality and gathering user feedback. Any submitted reports do not constitute official filings with the Federal Election Commission (FEC). Any information diff --git a/front-end/src/app/login/security-notice/prod-notice.component.ts b/front-end/src/app/login/security-notice/prod-notice.component.ts index dfa778274c..1fc19e84a1 100644 --- a/front-end/src/app/login/security-notice/prod-notice.component.ts +++ b/front-end/src/app/login/security-notice/prod-notice.component.ts @@ -2,37 +2,250 @@ import { Component } from '@angular/core'; @Component({ selector: 'app-prod-notice', - template: `

Security notification

-
-

Warning

+ template: ` +

Overview

- This version of the application is for testing new features and functionality and gathering user feedback. Any - submitted reports do not constitute official filings with the Federal Election Commission (FEC). Any information - you enter at this time is subject to deletion and will not be retained. + The Federal Election Commission (FEC) offers electronic filing software, FECfile+. The FEC provides this service + to comply with the software requirements of the Federal Election Campaign Act (FECA) 52 U.S.C. §30104(a)(12)(A). + The service is offered subject to acceptance of FECfile+'s terms of service, FECfile+'s acceptable use policy, as + well as any relevant sections of FEC's sale or use restriction and FEC's privacy and security policy + (collectively, the "Agreement").

-

Disclaimer

- If you need to file a statement or report to meet your disclosure requirements, please use your normal method of - submitting that information to the FEC. Submitting a report or statement through this system during the testing - phase of this software release does not constitute an official filing with the Federal Election Commission. + Please read the Agreement carefully before you start to use FECfile+. By clicking CONSENT below, you accept and + agree to be bound and abide by this Agreement in addition to the FEC.gov Privacy and Security Policy, incorporated + herein by reference. If you do not want to agree to this Agreement or the FEC.gov Privacy and Security Policy, you + must not access or use the FECfile+ service, or any portion thereof.

-

While using this system please do not:

+

Terms of service

+

Scope

+

+ All the content, documentation, code and related materials made available to you through FECfile+, related + services and our GitHub repositories, are subject to these terms. Access to or use of FECfile+ and related + services constitutes acceptance of this Agreement. +

+

Use

+

You may use FECfile+ to meet disclosure requirements.

+

Eligibility restrictions

+

+ Access to FECfile+ is restricted to a limited number of committees and other filers, and is available by + invitation only, due to the software’s current limited functionality. Signing up for FECfile+ without an + invitation may result in termination of access. See Service Termination. +

+

Electronic filing disclaimer

+

+ Committees are required to file electronically if total contributions received or total expenditures made exceed, + or are expected to exceed, $50,000 in any calendar year. Committees not required to file electronically but those + that choose to do so must continue to file electronically for that calendar year, in accordance with 11 C.F.R. + §104.18. By using FECfile+, you acknowledge and agree that filing a form through this platform constitutes an + electronic filing. +

+

Authentication

+

+ FECfile+ uses a government-wide authentication service provider, login.gov. Login.gov’s privacy and data policies + are available on the U.S. General Services Administration's (GSA) website. +

+

Data storage

+

+ FECfile+ stores your filing data on a government-wide cloud service provider, cloud.gov. Cloud.gov’s privacy and + data policies are available on GSA’s website. The FEC reserves the right to change cloud service providers and if + it does so, will update the terms of service. +

+

Technical/Customer support

+

+ FECfile+ support hours are Monday through Friday from 9:00 a.m. to 5:30 p.m. Eastern Time, excluding federal + holidays. The FEC’s Electronic Filing Office, or their agents, may view a your data if you provide consent for + assistance with FECfile+. Technical support issues may take up to 48 business hours to resolve. +

+

Attribution

+

+ All services which utilize FECfile+ code or access related services must identify the FEC as the source. You may + not use the FEC name, logo, or the like to imply endorsement of any product, service, or entity--not-for-profit, + commercial or otherwise. +

+

Modification or false representation of content

+

+ You may not modify or falsely represent content accessed through FECfile+ or related services and still claim the + source is FEC. +

+

Right to limit

+

+ Your use of FECfile+ and related services may be subject to certain limitations on access, API calls, or use as + set forth within this Agreement or otherwise provided by the FEC. If the FEC reasonably believes that you have + attempted to exceed or circumvent these limits, your ability to use these services may be permanently or + temporarily blocked. The FEC may monitor your use of these services to improve the service or to ensure compliance + with this Agreement. +

+

Service termination

+

+ If you wish to terminate this Agreement, you may do so by refraining from further use of FECfile+ and related + services. In the event you violate of any of the terms of this Agreement, or when otherwise deemed reasonably + necessary by the FEC, the FEC reserves the right, at its sole discretion, to terminate or deny access to and use + of all or part of FECfile+ and related services. The terms of this Agreement shall survive termination. +

+

+ The FEC also reserves the right to eliminate FECfile+ and its related services. In the event that the FEC no + longer provides FECfile+ and its services, other software will be available for committees and other filers to use + to comply with their disclosure requirements. +

+

Disclaimer of warranties

+

+ FECfile+ and related services are provided "as is" and on an "as-available" basis. The FEC makes no warranty that + FECfile+ or related services will be error free or that access thereto will be continuous or uninterrupted. +

+

+ You understand that we cannot and do not guarantee or warrant that files available for downloading from FECfile+ + or FEC.gov will be free of viruses or other destructive code. You are responsible for implementing sufficient + procedures and checkpoints to satisfy your particular requirements for anti-virus protection and accuracy of data + input and output, and for maintaining a means external to our site for any reconstruction of any lost data. To the + fullest extent provided by law, the FEC will not be liable for any loss or damage caused by a distributed + denial-of-service attack, viruses, or other technologically harmful material that may infect your computer + equipment, computer programs, data, or other proprietary material due to your use of FECfile+ or any services or + items obtained through fec.gov or your downloading of any material posted on it, or on any website linked to it. +

+

+ Your use of the website, its content, and any services or items obtained through the website is at your own risk. + the website, its content, and any services or items obtained through the website are provided on an "as is" and + "as available" basis, without any warranties of any kind, either express or implied. Neither the FEC nor any + person associated with the FEC makes any warranty or representation with respect to the completeness, security, + reliability, quality, accuracy, or availability of FECfile+. without limiting the foregoing, neither the fec nor + anyone associated with the FEC represents or warrants that FECfile+, its content, or any services or items + obtained through FECfile+ will be accurate, reliable, error-free, or uninterrupted, that defects will be + corrected, that our site or the server that makes it available are free of viruses or other harmful components, or + that the website or any services or items obtained through the website will otherwise meet your needs or + expectations. +

+

+ To the fullest extent provided by law, the FEC hereby disclaims all warranties of any kind, whether express or + implied, statutory, or otherwise, including but not limited to any warranties of merchantability, + non-infringement, and fitness for particular purpose. +

+

The foregoing does not affect any warranties that cannot be excluded or limited under applicable law.

+

Limitations on liability and Indemnification

+

+ In no event will the FEC be liable with respect to any subject matter of this Agreement. Further, the you agree to + indemnify and hold the FEC harmless against all claims arising from use of FECfile+ or the API. +

+

General representations

+

+ You hereby warrant that (1) your use of FECfile+ and related services will be in strict accordance with the FEC + privacy policy, this Agreement, and all applicable laws and regulations, and (2) your use of FECfile+ and related + services will not infringe or misappropriate the intellectual property rights of any third party. +

+

Changes

+

+ The FEC reserves the right, at its sole discretion, to modify or replace this Agreement, in whole or in part. Your + continued use of or access to FECfile+ and related services following posting of any changes to this Agreement + constitutes acceptance of those modified terms. The FEC may, in the future, offer new services and/or features + through FECfile+ and related services. Such new features and/or services shall be subject to the terms and + conditions of this Agreement. +

+

Disputes

+

+ Any disputes arising out of this Agreement and access to or use of FECfile+ or related services shall be governed + by federal law. +

+

No waiver of rights

+

+ The FEC's failure to exercise or enforce any right or provision of this Agreement shall not constitute waiver of + such right or provision. +

+

3. Acceptable use policy

+

+ This acceptable use policy sets out a list of acceptable and unacceptable conduct for using FECfile+ and related + services in addition to the restrictions imposed by the terms of service. If we believe a violation of the policy + is deliberate, repeated or presents a credible risk of harm to other users, the services or any third parties, we + may suspend or terminate your access. If there is any inconsistency between this acceptable use policy and the + terms of service, the terms of service will take priority to the extent of the inconsistency. +

+

Changes to this acceptable use policy

+

The FEC may update this acceptable use policy at any time. Updates will be made available within FECfile+.

+

Acceptable and unacceptable conduct

+

Do:

    -

    -

  • prevent other users from accessing FECfile+;
  • -
  • overload FECfile+;
  • -
  • share committee or user accounts;
  • -
  • - attempt to gain unauthorized access to FECfile+ or related systems or networks or to defeat, avoid, bypass, - remove, deactivate, or otherwise circumvent any software protection or monitoring mechanisms of FECfile+; -
  • -
  • create committee or user accounts in bulk.
  • -

    +
  • comply with terms of service, including the terms of this acceptable use policy;
  • +
  • comply with all applicable laws and governmental regulations;
  • +
  • keep all login information confidential;
  • +
  • + monitor and control all activity conducted through your accounts in connection with FECfile+ and related + services; and +
  • +
  • + promptly notify us if you become aware of or reasonably suspect any illegal or unauthorized activity involving + your accounts. +
-

Legal disclosure

+

Don't:

+
    +
  • prevent other users from accessing FECfile+ or related services;
  • +
  • knowingly and willfully taking any actions that overloads FECfile+ or related services;
  • +
  • materially reduce the speed of FECfile+ or related services for other users;
  • +
  • share, transfer or otherwise provide access to accounts designated for you to another person;
  • +
  • + attempt to gain unauthorized access to FECfile+ or related systems or networks or to defeat, avoid, bypass, + remove, deactivate, or otherwise circumvent any software protection or monitoring mechanisms of FECfile+ or + related services; +
  • +
  • engage in activity that incites or encourages violence or hatred against individuals or groups;
  • +
  • impersonate any person or entity;
  • +
  • create false or fictitious filings and/or accounts
  • +
  • create accounts in bulk;
  • +
  • send unsolicited communications, promotions or advertisements, or spam;
  • +
  • send altered, deceptive or false source-identifying information, including "spoofing" or "phishing"; or
  • +
  • authorize, permit, enable, induce or encourage any third party to do any of the above.
  • +
+

4. Sale or use restriction

+

+ Sale or use of publicly disclosed campaign finance information is restricted. 52 U.S.C. §30111(a)(4) and 11 C.F.R. + §104.15 +

+

5. Privacy and data use

+

Information collected through the software is governed by the FEC’s privacy and security policy.

+

If you use FECfile+, we collect the following information:

+
    +
  • Your name, address, phone number (if provided), and email address.
  • +
  • + Data from your campaign finance reports and statements, including but not limited to committee details, as well + as contributor information such as name, address, employer occupation (when applicable), and telephone number + (optional) +
  • +
  • Cookie information related to authentication only
  • +
  • + Logging and analytical data required to comply with security monitoring policies, including IP address, device + details, browser type and version +
  • +
  • A user ID (uuid) provided by login.gov, our authentication provider
  • +
+

+ Data submitted on campaign finance reports and statements are subject to public disclosure, 52 U.S.C. + §30104(a)(11)(B). Reports and statements may be viewed on the FEC’s website. +

+

6. Consent

This is a U.S. Federal Government system that is for official use only. Unauthorized use is strictly prohibited. -

`, +

+

+ This U.S. Federal Government system is to be used by authorized users only. All access or use of this system + constitutes your understanding and acceptance of these terms and constitutes unconditional consent to review, + monitoring and action by all authorized government and law enforcement personnel. While using this system your use + may be monitored, recorded and subject to audit. +

+

+ Unauthorized attempts to access, upload, change, delete, or deface information on this system; modify this system; + deny access to this system; accrue resources for unauthorized use; or otherwise misuse this system are strictly + prohibited and may result in criminal, civil, or administrative penalties. +

+

+ Furthermore, knowingly and willfully making any materially false, fictitious, or fraudulent statement or + representation to a federal government agency, including the Federal Election Commission, is punishable under 18 + U.S.C. §1001. The Commission may report apparent violations to the appropriate law enforcement authorities, as per + 52 U.S.C. §30107(a)(9). +

+

+ This Agreement constitutes the entire Agreement between the FEC and you concerning the subject matter hereof, and + may only be modified by the posting of a revised version on this page by the FEC. +

+ `, styleUrls: ['./security-notice.component.scss'], }) export class ProdNoticeComponent {} diff --git a/front-end/src/app/login/security-notice/security-notice.component.html b/front-end/src/app/login/security-notice/security-notice.component.html index 8597204dfb..4ecd7e16ea 100644 --- a/front-end/src/app/login/security-notice/security-notice.component.html +++ b/front-end/src/app/login/security-notice/security-notice.component.html @@ -1,21 +1,24 @@ -
- @if (showForm) { -
- FECfile+ Seal and Title +@if (showForm) { + - } + + Log Out + +
+} -
+
+
+

Terms of service and user agreement

+
+
@@ -35,22 +38,21 @@
-
- - + (keypress)="signConsentForm()" + />
diff --git a/front-end/src/app/login/security-notice/security-notice.component.scss b/front-end/src/app/login/security-notice/security-notice.component.scss index 9735825053..b4489c8b18 100644 --- a/front-end/src/app/login/security-notice/security-notice.component.scss +++ b/front-end/src/app/login/security-notice/security-notice.component.scss @@ -1,20 +1,6 @@ #fec-seal-and-title { min-height: 64px; max-height: 5%; - position: absolute; - left: 5px; - top: 40px; -} - -.security-notice { - width: 100%; - height: 100%; - margin-top: 100px; -} - -.notice-header { - padding-left: 20px; - padding-top: 16px; } :host ::ng-deep { @@ -22,79 +8,110 @@ font-style: italic; font-family: var(--gandhi); } + + .security-button { + width: 180px; + height: 35px; + line-height: 14px; + } } .logout-button { - position: absolute; - top: 64px; - right: 32px; font-size: 18px; font-family: var(--karla-bold); text-transform: uppercase; text-shadow: #000000 1px 0 10px; color: white !important; + margin-right: 32px; } -.decline-button { - margin-right: 20px; -} - -.notice-box-form { - width: 80vw; - margin: 0 32px; -} - -.notice-box { - max-width: min(648px, 100vw); - margin: 0; -} -@media screen and (max-width: 767px) { - .notice-box { - margin: 0 32px; +@keyframes adjust-mask { + 0% { + mask-image: linear-gradient(to bottom, black calc(100% - 40px), transparent); } -} -@media screen and (min-width: 768px) { - .notice-box { - width: 66.666666666667vw; + 3%, + 97% { + mask-image: linear-gradient(to bottom, transparent, black 40px, black calc(100% - 40px), transparent); } -} -@media screen and (min-width: 1200px) { - .notice-box { - width: 50vw; + 100% { + mask-image: linear-gradient(to bottom, transparent, black 40px, black 100%); } } +.notice-box-form { + flex: 1; + overflow-y: auto; + min-height: 0; + animation: adjust-mask linear both; + animation-timeline: scroll(self); +} + .security-container { background-color: white; - margin-top: 108px; - padding: 32px; + padding: 24px; border-radius: 8px; + width: 80vw; + margin: auto; + height: 902px; + display: flex; + flex-direction: column; } p, li { - margin-bottom: 20px; - font-size: 18px; + margin-bottom: 16px; + font-size: 16px; } p + ul { margin-top: 0; } -ul li { - list-style-type: disc; +ul { margin-left: 16px; + & li { + list-style-type: disc; + margin-left: 16px; + } } h1 { font-size: 36px; + margin: 0 0 20px 0; } h2 { - font-size: 28px; - margin: 0; + margin: 32px 0 16px 0; +} + +h3 { + margin-bottom: 0; +} + +.notice-container { + max-height: 902px; + overflow-y: auto; + -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%); + mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%); } -.top-line { - margin: 20px 0; +.header { + border-bottom: 2px solid #212121; + margin-bottom: 20px; +} + +.header, +.footer { + flex-shrink: 0; +} + +.notice-box { + width: 292px; + margin: auto; + @media (min-width: 768px) { + width: 484px; + } + @media (min-width: 992px) { + width: 668px; + } } diff --git a/front-end/src/app/login/security-notice/security-notice.component.ts b/front-end/src/app/login/security-notice/security-notice.component.ts index f475882f56..9258915853 100644 --- a/front-end/src/app/login/security-notice/security-notice.component.ts +++ b/front-end/src/app/login/security-notice/security-notice.component.ts @@ -10,7 +10,7 @@ import { singleClickEnableAction } from 'app/store/single-click.actions'; import { userLoginDataUpdatedAction } from 'app/store/user-login-data.actions'; import { selectUserLoginData } from 'app/store/user-login-data.selectors'; import { Checkbox } from 'primeng/checkbox'; -import { ButtonDirective } from 'primeng/button'; +import { ButtonModule } from 'primeng/button'; import { environment } from 'environments/environment'; import { ProdNoticeComponent } from './prod-notice.component'; import { DevNoticeComponent } from './dev-notice.component'; @@ -22,7 +22,7 @@ export const SECURITY_CONSENT_VERSION = '1'; selector: 'app-security-notice', templateUrl: './security-notice.component.html', styleUrls: ['./security-notice.component.scss'], - imports: [ReactiveFormsModule, Checkbox, ButtonDirective, NgComponentOutlet], + imports: [ReactiveFormsModule, Checkbox, ButtonModule, NgComponentOutlet], }) export class SecurityNoticeComponent implements OnInit { private readonly store = inject(Store); diff --git a/front-end/src/app/reports/shared/print-preview/print-preview.component.html b/front-end/src/app/reports/shared/print-preview/print-preview.component.html index 9f06cd1b7c..eab26eb9ab 100644 --- a/front-end/src/app/reports/shared/print-preview/print-preview.component.html +++ b/front-end/src/app/reports/shared/print-preview/print-preview.component.html @@ -82,7 +82,7 @@

Print preview


diff --git a/front-end/src/styles.scss b/front-end/src/styles.scss index 3ba25bb7a1..2b94ac5dfe 100644 --- a/front-end/src/styles.scss +++ b/front-end/src/styles.scss @@ -108,10 +108,6 @@ label.disabled { text-align: right; } -.align-center { - text-align: center; -} - .gray { background-color: #f1f1f1; } @@ -842,3 +838,24 @@ select { font-size: 0.75rem; } } + +.standard-container { + width: 512px; + min-width: 512px; + max-width: 512px; + @media (min-width: 768px) { + width: 704px; + min-width: 704px; + max-width: 704px; + } + @media (min-width: 992px) { + width: 852px; + min-width: 852px; + max-width: 852px; + } + @media (min-width: 1400px) { + width: 1052px; + min-width: 1052px; + max-width: 1052px; + } +} From 9559a68702b06f854d1d0298970adeeecc17056c Mon Sep 17 00:00:00 2001 From: Max Zaremba Date: Fri, 13 Mar 2026 17:32:26 -0400 Subject: [PATCH 14/69] Updated reattribution modal --- .../select-report-dialog.component.html | 63 +++++---------- .../select-report-dialog.component.scss | 80 +++---------------- .../select-report-dialog.component.ts | 24 +++--- .../components/dialog/dialog.component.ts | 7 +- 4 files changed, 51 insertions(+), 123 deletions(-) diff --git a/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.html b/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.html index 7ff7530f17..3d285b6cf1 100644 --- a/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.html +++ b/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.html @@ -1,43 +1,22 @@ - - + diff --git a/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.scss b/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.scss index d281e605be..c9ee623fe4 100644 --- a/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.scss +++ b/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.scss @@ -1,81 +1,21 @@ -dialog::backdrop { - background: rgba(0, 0, 0, 0.3); -} - -h1 { - color: #212121; - font-size: 36px; - font-weight: bold; - line-height: 36px; -} - -.field { - display: grid; -} - -dialog { - width: 512px; - background-color: #ffffff; - border-radius: 4px; - box-shadow: 0 0 4px 0 rgba(33, 33, 33, 0.5); - border: none; - margin-top: 15%; - position: absolute; - top: 0; -} - -.dialog-header { - border-bottom: 2px solid #212121; - width: 448px; - margin: auto; - padding-bottom: 10px; - display: flex; - justify-content: space-between; -} - -.dialog-body { - padding: 16px 16px 64px 16px; - margin-left: 25px; - margin-right: 25px; -} - p { - margin-bottom: 16px; + font-size: 14px; + font-family: var(--karla); + margin-bottom: 24px; } label { - font-family: var(--karla); - font-weight: bold; font-size: 14px; - color: #212121; - margin-bottom: 5px; - text-transform: uppercase; + margin-bottom: 4px; } -.dropdown, -.dropdown-toggle { - width: 400px !important; -} - -.dropdown-toggle { - display: flex; - justify-content: space-between; - align-items: center; - height: 44px; - border: 2px solid #aeb0b5; +#report-selector { + height: 36px; font-size: 14px; - border-radius: 4px; + appearance: none; + background-image: url(assets/img/dropdown_arrow.svg); } -.dialog-footer { - display: flex; - justify-content: space-between; - border-top: 0; - padding: 0 31px 16px 31px; -} - -#close-button { - color: #212121; - background: transparent; - border: none; +#report-selector option { + font-size: 14px; } diff --git a/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.ts b/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.ts index 7e9b146beb..3069b175b8 100644 --- a/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.ts +++ b/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, computed, effect, ElementRef, inject, viewChild } from '@angular/core'; +import { Component, computed, effect, ElementRef, inject, model, viewChild } from '@angular/core'; import { Report } from '../../../../shared/models/reports/report.model'; import { ReattRedesUtils } from '../../../../shared/utils/reatt-redes/reatt-redes.utils'; import { Router } from '@angular/router'; @@ -12,12 +12,13 @@ import { Form3X } from 'app/shared/models'; import { DateUtils } from 'app/shared/utils/date.utils'; import { toSignal } from '@angular/core/rxjs-interop'; import { derivedAsync } from 'ngxtension/derived-async'; +import { DialogComponent } from 'app/shared/components/dialog/dialog.component'; @Component({ selector: 'app-select-report-dialog', templateUrl: './select-report-dialog.component.html', styleUrls: ['./select-report-dialog.component.scss'], - imports: [ReactiveFormsModule, FormsModule, ButtonDirective, Ripple], + imports: [ReactiveFormsModule, FormsModule, ButtonDirective, Ripple, DialogComponent], }) export class SelectReportDialogComponent { public readonly router = inject(Router); @@ -26,13 +27,14 @@ export class SelectReportDialogComponent { readonly selectReportDialog = viewChild.required>('selectReportDialog'); readonly report = this.store.selectSignal(selectActiveReport); - readonly selectReportDialogSubject = toSignal(ReattRedesUtils.selectReportDialogSubject); - readonly transaction = computed(() => - this.selectReportDialogSubject() ? this.selectReportDialogSubject()![0] : undefined, - ); - readonly type = computed(() => (this.selectReportDialogSubject() ? this.selectReportDialogSubject()![1] : undefined)); + readonly selectReportDialogSignal = toSignal(ReattRedesUtils.selectReportDialogSubject, { initialValue: undefined }); + + readonly transaction = computed(() => this.selectReportDialogSignal()?.[0]); + readonly type = computed(() => this.selectReportDialogSignal()?.[1]); readonly visible = computed(() => !!this.transaction()); + readonly dialogVisible = model(false); + readonly availableReports = derivedAsync( () => { const visible = this.visible(); @@ -59,11 +61,12 @@ export class SelectReportDialogComponent { constructor() { effect(() => { - if (this.visible()) { - this.selectReportDialog().nativeElement.show(); + const data = this.selectReportDialogSignal(); + if (data) { + this.dialogVisible.set(true); this.selectedReport = undefined; } else { - this.selectReportDialog().nativeElement.close(); + this.dialogVisible.set(false); } }); } @@ -78,6 +81,7 @@ export class SelectReportDialogComponent { } cancel() { + this.dialogVisible.set(false); ReattRedesUtils.selectReportDialogSubject.next(undefined); } } diff --git a/front-end/src/app/shared/components/dialog/dialog.component.ts b/front-end/src/app/shared/components/dialog/dialog.component.ts index b1aca11115..2e679a7b04 100644 --- a/front-end/src/app/shared/components/dialog/dialog.component.ts +++ b/front-end/src/app/shared/components/dialog/dialog.component.ts @@ -22,7 +22,12 @@ export class DialogComponent { readonly closeOnEscape = input(true); handleEscape(event: Event) { - if (!this.closeOnEscape()) event.preventDefault(); + if (!this.closeOnEscape()) { + event.preventDefault(); + } else { + this.reject.emit(); + this.visible.set(false); + } } close() { From e0aac03d384a49643d958a0f2f1d348278f56e32 Mon Sep 17 00:00:00 2001 From: Sasha Dresden Date: Mon, 16 Mar 2026 15:45:02 -0400 Subject: [PATCH 15/69] IN_KIND_OUT should inherit from SCHEDULE_B_MEMO, which requires memo_code --- .../transaction-types/common-types/IN_KIND_OUT.model.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front-end/src/app/shared/models/transaction-types/common-types/IN_KIND_OUT.model.ts b/front-end/src/app/shared/models/transaction-types/common-types/IN_KIND_OUT.model.ts index 389f46719b..cccf7960f6 100644 --- a/front-end/src/app/shared/models/transaction-types/common-types/IN_KIND_OUT.model.ts +++ b/front-end/src/app/shared/models/transaction-types/common-types/IN_KIND_OUT.model.ts @@ -1,7 +1,7 @@ import { COMMITTEE, COMMITTEE_B_FORM_FIELDS } from 'app/shared/utils/transaction-type-properties'; -import { SCHEDULE_B_MEMO } from './SCHEDULE_B_MEMO.model'; +import { SchBTransactionType } from '../../schb-transaction-type.model'; -export abstract class IN_KIND_OUT extends SCHEDULE_B_MEMO { +export abstract class IN_KIND_OUT extends SchBTransactionType { override formFields = COMMITTEE_B_FORM_FIELDS; contactTypeOptions = COMMITTEE; override isDependentChild = () => true; From 0c6a67d585278ec88ddd092e3a9f27dd41aa8586 Mon Sep 17 00:00:00 2001 From: Max Zaremba Date: Tue, 17 Mar 2026 17:42:33 -0400 Subject: [PATCH 16/69] Backup commit --- .../dialog/dialog.component.spec.ts | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/front-end/src/app/shared/components/dialog/dialog.component.spec.ts b/front-end/src/app/shared/components/dialog/dialog.component.spec.ts index e5f8b46ff6..95790cfbe3 100644 --- a/front-end/src/app/shared/components/dialog/dialog.component.spec.ts +++ b/front-end/src/app/shared/components/dialog/dialog.component.spec.ts @@ -5,10 +5,11 @@ import { Component, signal, viewChild } from '@angular/core'; @Component({ imports: [DialogComponent], standalone: true, - template: ``, + template: ``, }) class TestHostComponent { visible = signal(false); + closeOnEscape = signal(true); component = viewChild.required(DialogComponent); } @@ -28,6 +29,40 @@ describe('DialogComponent', () => { fixture.detectChanges(); }); + it('should emit reject and close when closeOnEscape is true', () => { + host.closeOnEscape.set(true); + host.visible.set(true); + fixture.detectChanges(); + + const rejectSpy = jasmine.createSpy('rejectSpy'); + component.reject.subscribe(rejectSpy); + + const event = new KeyboardEvent('keydown', { key: 'Escape' }); + spyOn(event, 'preventDefault'); + + component.handleEscape(event); + + expect(event.preventDefault).not.toHaveBeenCalled(); + expect(rejectSpy).toHaveBeenCalled(); + expect(component.visible()).toBeFalse(); + }); + + it('should prevent default when closeOnEscape is false', () => { + host.closeOnEscape.set(false); + fixture.detectChanges(); + + const rejectSpy = jasmine.createSpy('rejectSpy'); + component.reject.subscribe(rejectSpy); + + const event = new KeyboardEvent('keydown', { key: 'Escape' }); + spyOn(event, 'preventDefault'); + + component.handleEscape(event); + + expect(event.preventDefault).toHaveBeenCalled(); + expect(rejectSpy).not.toHaveBeenCalled(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); From 837cc25a8dd02279a600059cb7abc714f9ffc9f1 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Wed, 18 Mar 2026 08:50:29 -0400 Subject: [PATCH 17/69] make sure memo text is visible when checking value and clicking 'Save & continue' to clear up the flake before submitting the report (hangs on memo page) --- front-end/cypress/e2e-smoke/F1M/f1m-affiliation.cy.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/front-end/cypress/e2e-smoke/F1M/f1m-affiliation.cy.ts b/front-end/cypress/e2e-smoke/F1M/f1m-affiliation.cy.ts index e13bcdc425..742d597fbb 100644 --- a/front-end/cypress/e2e-smoke/F1M/f1m-affiliation.cy.ts +++ b/front-end/cypress/e2e-smoke/F1M/f1m-affiliation.cy.ts @@ -103,7 +103,7 @@ describe('Manage reports', () => { } PageUtils.clickButton('Save and continue', '[data-cy="save-cancel-actions"]:visible'); - cy.get('[data-cy="print-preview"]').should('exist'); + cy.get('[data-cy="print-preview"]').should('be.visible'); PageUtils.clickSidebarSection('SIGN & SUBMIT'); PageUtils.shouldNotHaveSidebarItem('Report Status'); @@ -117,7 +117,8 @@ describe('Manage reports', () => { // Verify it is still there when we go back to the page PageUtils.clickSidebarSection('REVIEW A REPORT'); PageUtils.clickSidebarItem('Add a report level memo'); - cy.get('[id="text4000"]').should('have.value', memoText); + cy.get('[id="text4000"]:visible').should('have.value', memoText); + PageUtils.clickButton('Save & continue', '[data-cy="report-level-memo-actions"]:visible'); // Submit report and verify report status link now available PageUtils.clickSidebarSection('SIGN & SUBMIT'); From 79535971b11816935406884db4d199989ddcb9dd Mon Sep 17 00:00:00 2001 From: Max Zaremba Date: Wed, 18 Mar 2026 16:54:53 -0400 Subject: [PATCH 18/69] Updated dialog wrapper tests and relect report dialog text --- .../select-report-dialog.component.html | 4 ++-- .../app/shared/components/dialog/dialog.component.spec.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.html b/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.html index 3d285b6cf1..848d43449f 100644 --- a/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.html +++ b/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.html @@ -6,8 +6,8 @@ >

- Pick the open report on which you would like to {{ actionLabel() }} a contribution or a portion of it to a - different {{ actionTargetLabel() }}. + Select the report where you want to {{ actionLabel() }} part of an excessive contribution to a joint + {{ actionTargetLabel() }}.

- - @for (report of availableReports(); track report) { - - } - -
- -
+ +
+

+ Select the report where you want to {{ actionLabel() }} part of an excessive contribution to a joint + {{ actionTargetLabel() }}. +

+ +
- +
diff --git a/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.ts b/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.ts index 7e9b146beb..4d5279f962 100644 --- a/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.ts +++ b/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.ts @@ -1,29 +1,27 @@ -import { Component, computed, effect, ElementRef, inject, viewChild } from '@angular/core'; +import { Component, computed, effect, inject, signal } from '@angular/core'; import { Report } from '../../../../shared/models/reports/report.model'; import { ReattRedesUtils } from '../../../../shared/utils/reatt-redes/reatt-redes.utils'; import { Router } from '@angular/router'; import { Form3XService } from '../../../../shared/services/form-3x.service'; import { ReactiveFormsModule, FormsModule } from '@angular/forms'; -import { ButtonDirective } from 'primeng/button'; -import { Ripple } from 'primeng/ripple'; import { Store } from '@ngrx/store'; import { selectActiveReport } from 'app/store/active-report.selectors'; import { Form3X } from 'app/shared/models'; import { DateUtils } from 'app/shared/utils/date.utils'; import { toSignal } from '@angular/core/rxjs-interop'; import { derivedAsync } from 'ngxtension/derived-async'; +import { DialogComponent } from 'app/shared/components/dialog/dialog.component'; @Component({ selector: 'app-select-report-dialog', templateUrl: './select-report-dialog.component.html', styleUrls: ['./select-report-dialog.component.scss'], - imports: [ReactiveFormsModule, FormsModule, ButtonDirective, Ripple], + imports: [ReactiveFormsModule, FormsModule, DialogComponent], }) export class SelectReportDialogComponent { public readonly router = inject(Router); private readonly service = inject(Form3XService); readonly store = inject(Store); - readonly selectReportDialog = viewChild.required>('selectReportDialog'); readonly report = this.store.selectSignal(selectActiveReport); readonly selectReportDialogSubject = toSignal(ReattRedesUtils.selectReportDialogSubject); @@ -32,6 +30,7 @@ export class SelectReportDialogComponent { ); readonly type = computed(() => (this.selectReportDialogSubject() ? this.selectReportDialogSubject()![1] : undefined)); readonly visible = computed(() => !!this.transaction()); + readonly dialogVisible = signal(false); readonly availableReports = derivedAsync( () => { @@ -59,11 +58,10 @@ export class SelectReportDialogComponent { constructor() { effect(() => { - if (this.visible()) { - this.selectReportDialog().nativeElement.show(); + const visible = this.visible(); + this.dialogVisible.set(visible); + if (visible) { this.selectedReport = undefined; - } else { - this.selectReportDialog().nativeElement.close(); } }); } diff --git a/front-end/src/app/shared/components/dialog/dialog.component.spec.ts b/front-end/src/app/shared/components/dialog/dialog.component.spec.ts index e5f8b46ff6..17e6830171 100644 --- a/front-end/src/app/shared/components/dialog/dialog.component.spec.ts +++ b/front-end/src/app/shared/components/dialog/dialog.component.spec.ts @@ -5,10 +5,11 @@ import { Component, signal, viewChild } from '@angular/core'; @Component({ imports: [DialogComponent], standalone: true, - template: ``, + template: ``, }) class TestHostComponent { - visible = signal(false); + visible = signal(true); + closeOnEscape = signal(true); component = viewChild.required(DialogComponent); } @@ -28,6 +29,42 @@ describe('DialogComponent', () => { fixture.detectChanges(); }); + it('should emit reject and close when closeOnEscape is true', () => { + host.closeOnEscape.set(true); + host.visible.set(true); + fixture.detectChanges(); + + const rejectSpy = jasmine.createSpy('rejectSpy'); + component.reject.subscribe(rejectSpy); + + const event = new KeyboardEvent('keydown', { key: 'Escape' }); + spyOn(event, 'preventDefault'); + + component.handleEscape(event); + + expect(event.preventDefault).not.toHaveBeenCalled(); + expect(rejectSpy).toHaveBeenCalled(); + expect(component.visible()).toBeFalse(); + }); + + it('should prevent default when closeOnEscape is false', () => { + host.closeOnEscape.set(false); + host.visible.set(true); + fixture.detectChanges(); + + const rejectSpy = jasmine.createSpy('rejectSpy'); + component.reject.subscribe(rejectSpy); + + const event = new KeyboardEvent('keydown', { key: 'Escape' }); + spyOn(event, 'preventDefault'); + + component.handleEscape(event); + + expect(event.preventDefault).toHaveBeenCalled(); + expect(rejectSpy).not.toHaveBeenCalled(); + expect(host.visible()).toBeTrue(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); From 7371f7fd3379a423e6dc133a50da5f6e88b02016 Mon Sep 17 00:00:00 2001 From: Max Zaremba Date: Tue, 17 Mar 2026 17:42:33 -0400 Subject: [PATCH 36/69] Backup commit --- .../app/shared/components/dialog/dialog.component.spec.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/front-end/src/app/shared/components/dialog/dialog.component.spec.ts b/front-end/src/app/shared/components/dialog/dialog.component.spec.ts index 17e6830171..95790cfbe3 100644 --- a/front-end/src/app/shared/components/dialog/dialog.component.spec.ts +++ b/front-end/src/app/shared/components/dialog/dialog.component.spec.ts @@ -5,10 +5,10 @@ import { Component, signal, viewChild } from '@angular/core'; @Component({ imports: [DialogComponent], standalone: true, - template: ``, + template: ``, }) class TestHostComponent { - visible = signal(true); + visible = signal(false); closeOnEscape = signal(true); component = viewChild.required(DialogComponent); } @@ -49,7 +49,6 @@ describe('DialogComponent', () => { it('should prevent default when closeOnEscape is false', () => { host.closeOnEscape.set(false); - host.visible.set(true); fixture.detectChanges(); const rejectSpy = jasmine.createSpy('rejectSpy'); @@ -62,7 +61,6 @@ describe('DialogComponent', () => { expect(event.preventDefault).toHaveBeenCalled(); expect(rejectSpy).not.toHaveBeenCalled(); - expect(host.visible()).toBeTrue(); }); it('should create', () => { From 3f6f27d7e33a58a7146a826cc02a93ba22ab7296 Mon Sep 17 00:00:00 2001 From: Max Zaremba Date: Fri, 13 Mar 2026 17:32:26 -0400 Subject: [PATCH 37/69] Updated reattribution modal --- .../select-report-dialog.component.html | 5 +- .../select-report-dialog.component.scss | 80 +++---------------- .../select-report-dialog.component.ts | 22 ++--- .../dialog/dialog.component.spec.ts | 2 +- 4 files changed, 25 insertions(+), 84 deletions(-) diff --git a/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.html b/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.html index 8aef41ad06..a847767d1e 100644 --- a/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.html +++ b/front-end/src/app/reports/transactions/transaction-list/select-report-dialog/select-report-dialog.component.html @@ -3,12 +3,11 @@ title="Select a report" submitLabel="Continue" (confirm)="createReattribution()" - (reject)="cancel()" >

- Select the report where you want to {{ actionLabel() }} part of an excessive contribution to a joint - {{ actionTargetLabel() }}. + Pick the open report on which you would like to {{ actionLabel() }} a contribution or a portion of it to a + different {{ actionTargetLabel() }}.

From 04d44e2f2d9900b717545b902a42a6e0eb5a3140 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Mon, 23 Mar 2026 11:29:50 -0400 Subject: [PATCH 41/69] sonar code duplication --- .../reports/reports.can-unamend.cy.ts | 31 +++---------- .../cypress/e2e-smoke/F24/f24.helpers.ts | 45 +++++++++++++++++++ ...reports-f24-independent-expenditures.cy.ts | 34 +++----------- 3 files changed, 56 insertions(+), 54 deletions(-) create mode 100644 front-end/cypress/e2e-smoke/F24/f24.helpers.ts diff --git a/front-end/cypress/e2e-extended/reports/reports.can-unamend.cy.ts b/front-end/cypress/e2e-extended/reports/reports.can-unamend.cy.ts index cc240e619c..e3246604d7 100644 --- a/front-end/cypress/e2e-extended/reports/reports.can-unamend.cy.ts +++ b/front-end/cypress/e2e-extended/reports/reports.can-unamend.cy.ts @@ -1,10 +1,7 @@ -import { faker } from '@faker-js/faker'; import { DataSetup } from '../../e2e-smoke/F3X/setup'; +import { createIndependentExpenditureOnForm24 } from '../../e2e-smoke/F24/f24.helpers'; import { StartTransaction } from '../../e2e-smoke/F3X/utils/start-transaction/start-transaction'; -import { - defaultScheduleFormData, - DisbursementFormData, -} from '../../e2e-smoke/models/TransactionFormModel'; +import { defaultScheduleFormData } from '../../e2e-smoke/models/TransactionFormModel'; import { Initialize } from '../../e2e-smoke/pages/loginPage'; import { ContactLookup } from '../../e2e-smoke/pages/contactLookup'; import { currentYear, PageUtils } from '../../e2e-smoke/pages/pageUtils'; @@ -22,15 +19,6 @@ const f3xReceiptData = { date_received: new Date(currentYear, 4 - 1, 27), }; -const independentExpenditureData: DisbursementFormData = { - ...defaultScheduleFormData, - date2: new Date(currentYear, 4 - 1, 27), - supportOpposeCode: 'SUPPORT', - signatoryDateSigned: new Date(currentYear, 4 - 1, 27), - signatoryFirstName: faker.person.firstName(), - signatoryLastName: faker.person.lastName(), -}; - function submitCurrentReport(sidebarSection: string) { PageUtils.clickSidebarSection(sidebarSection); PageUtils.clickSidebarItem('Submit report'); @@ -110,19 +98,12 @@ describe('Reports can_unamend (/reports)', () => { it('shows Unamend only while an F24 amendment is eligible, then clears it after unamend and reload', () => { cy.wrap(DataSetup({ individual: true, candidate: true, f24: true })).then((result: any) => { - ReportListPage.gotToReportTransactionListPage(result.f24, false, true, false); - StartTransaction.IndependentExpenditures().IndependentExpenditure(); - ContactLookup.getContact(result.individual.last_name, '', 'Individual'); - TransactionDetailPage.enterSheduleFormDataForVoidExpenditure( - independentExpenditureData, + createIndependentExpenditureOnForm24( + result.f24, + result.individual.last_name, result.candidate, - false, - '', - 'date_signed', + true, ); - PageUtils.blurActiveField(); - TransactionDetailPage.clickSave(); - cy.location('pathname').should('include', `/reports/transactions/report/${result.f24}/list`); cy.get('tr').should('contain', 'Independent Expenditure'); submitCurrentReport('SIGN & SUBMIT'); diff --git a/front-end/cypress/e2e-smoke/F24/f24.helpers.ts b/front-end/cypress/e2e-smoke/F24/f24.helpers.ts new file mode 100644 index 0000000000..8b8fb52805 --- /dev/null +++ b/front-end/cypress/e2e-smoke/F24/f24.helpers.ts @@ -0,0 +1,45 @@ +import { faker } from '@faker-js/faker'; +import { StartTransaction } from '../F3X/utils/start-transaction/start-transaction'; +import { ContactFormData } from '../models/ContactFormModel'; +import { + defaultScheduleFormData, + DisbursementFormData, +} from '../models/TransactionFormModel'; +import { ContactLookup } from '../pages/contactLookup'; +import { currentYear, PageUtils } from '../pages/pageUtils'; +import { ReportListPage } from '../pages/reportListPage'; +import { TransactionDetailPage } from '../pages/transactionDetailPage'; + +function createIndependentExpenditureData(): DisbursementFormData { + return { + ...defaultScheduleFormData, + date2: new Date(currentYear, 4 - 1, 27), + supportOpposeCode: 'SUPPORT', + signatoryDateSigned: new Date(currentYear, 4 - 1, 27), + signatoryFirstName: faker.person.firstName(), + signatoryLastName: faker.person.lastName(), + }; +} + +export function createIndependentExpenditureOnForm24( + reportId: string, + individualLastName: string, + candidate: ContactFormData, + blurBeforeSave = false, +) { + ReportListPage.gotToReportTransactionListPage(reportId, false, true, false); + StartTransaction.IndependentExpenditures().IndependentExpenditure(); + ContactLookup.getContact(individualLastName, '', 'Individual'); + TransactionDetailPage.enterSheduleFormDataForVoidExpenditure( + createIndependentExpenditureData(), + candidate, + false, + '', + 'date_signed', + ); + if (blurBeforeSave) { + PageUtils.blurActiveField(); + } + TransactionDetailPage.clickSave(); + cy.location('pathname').should('include', `/reports/transactions/report/${reportId}/list`); +} diff --git a/front-end/cypress/e2e-smoke/F24/reports-f24-independent-expenditures.cy.ts b/front-end/cypress/e2e-smoke/F24/reports-f24-independent-expenditures.cy.ts index d023a00b74..a0bc71308a 100644 --- a/front-end/cypress/e2e-smoke/F24/reports-f24-independent-expenditures.cy.ts +++ b/front-end/cypress/e2e-smoke/F24/reports-f24-independent-expenditures.cy.ts @@ -1,24 +1,8 @@ import { Initialize } from '../pages/loginPage'; -import { currentYear, PageUtils } from '../pages/pageUtils'; -import { TransactionDetailPage } from '../pages/transactionDetailPage'; -import { - defaultScheduleFormData as defaultTransactionFormData, - DisbursementFormData, -} from '../models/TransactionFormModel'; +import { PageUtils } from '../pages/pageUtils'; import { DataSetup } from '../F3X/setup'; -import { StartTransaction } from '../F3X/utils/start-transaction/start-transaction'; -import { faker } from '@faker-js/faker'; import { ReportListPage } from '../pages/reportListPage'; -import { ContactLookup } from '../pages/contactLookup'; - -const independentExpenditureData: DisbursementFormData = { - ...defaultTransactionFormData, - date2: new Date(currentYear, 4 - 1, 27), - supportOpposeCode: 'SUPPORT', - signatoryDateSigned: new Date(currentYear, 4 - 1, 27), - signatoryFirstName: faker.person.firstName(), - signatoryLastName: faker.person.lastName(), -}; +import { createIndependentExpenditureOnForm24 } from './f24.helpers'; describe('Form 24 Independent Expenditures', () => { beforeEach(() => { @@ -27,19 +11,11 @@ describe('Form 24 Independent Expenditures', () => { it('Independent Expenditures created on a Form 24 should be linked to a Form 3X', () => { cy.wrap(DataSetup({ individual: true, candidate: true, f24: true })).then((result: any) => { - ReportListPage.gotToReportTransactionListPage(result.f24, false, true, false); - StartTransaction.IndependentExpenditures().IndependentExpenditure(); - ContactLookup.getContact(result.individual.last_name, '', 'Individual'); - - TransactionDetailPage.enterSheduleFormDataForVoidExpenditure( - independentExpenditureData, + createIndependentExpenditureOnForm24( + result.f24, + result.individual.last_name, result.candidate, - false, - '', - 'date_signed', ); - - TransactionDetailPage.clickSave(); PageUtils.clickLink('Independent Expenditure'); cy.contains('Address').should('exist'); cy.get('#first_name').should('have.value', result.individual.first_name); From 1f08c86ff01fee5f796357830455e0a66e4b5076 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Mon, 23 Mar 2026 12:12:34 -0400 Subject: [PATCH 42/69] sonar --- front-end/cypress/e2e-smoke/pages/reportListPage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front-end/cypress/e2e-smoke/pages/reportListPage.ts b/front-end/cypress/e2e-smoke/pages/reportListPage.ts index 67a2e9c8c6..9332a8c92c 100644 --- a/front-end/cypress/e2e-smoke/pages/reportListPage.ts +++ b/front-end/cypress/e2e-smoke/pages/reportListPage.ts @@ -194,7 +194,7 @@ export class ReportListPage { return `report-action-${action .trim() .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/(^-|-$)/g, '')}`; + .replaceAll(/[^a-z0-9]+/g, '-') + .replaceAll(/(^-|-$)/g, '')}`; } } From de140ac9f0d0a566708e8aa330c146cb4674479e Mon Sep 17 00:00:00 2001 From: Sasha Dresden Date: Mon, 23 Mar 2026 13:07:13 -0400 Subject: [PATCH 43/69] Load dev notice for test and production notice for all other environments --- front-end/src/app/layout/layout.component.html | 6 +++--- .../app/login/security-notice/security-notice.component.ts | 6 ++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/front-end/src/app/layout/layout.component.html b/front-end/src/app/layout/layout.component.html index b80bdadfb3..a05f68be3c 100644 --- a/front-end/src/app/layout/layout.component.html +++ b/front-end/src/app/layout/layout.component.html @@ -68,7 +68,7 @@ } -
+@if (useDynamicSidebar) { + +} diff --git a/front-end/src/app/login/security-notice/security-notice.component.ts b/front-end/src/app/login/security-notice/security-notice.component.ts index 9258915853..5327327b4b 100644 --- a/front-end/src/app/login/security-notice/security-notice.component.ts +++ b/front-end/src/app/login/security-notice/security-notice.component.ts @@ -1,4 +1,4 @@ -import { Component, effect, inject, OnInit, Type } from '@angular/core'; +import { Component, effect, inject, OnInit } from '@angular/core'; import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Store } from '@ngrx/store'; @@ -40,9 +40,7 @@ export class SecurityNoticeComponent implements OnInit { }, { updateOn: 'blur' }, ); - readonly componentToLoad: Type | Type = environment.production - ? ProdNoticeComponent - : DevNoticeComponent; + readonly componentToLoad = environment.name === 'test' ? DevNoticeComponent : ProdNoticeComponent; constructor() { this.activatedRoute.data.subscribe((d) => { From 57b1dedb8993b7702d2ecf31c64fe3d85d071707 Mon Sep 17 00:00:00 2001 From: Sasha Dresden Date: Mon, 23 Mar 2026 14:41:49 -0400 Subject: [PATCH 44/69] Update from develop and cleanup test for Form24 Name Validator --- front-end/package-lock.json | 1719 +++++++++-------- front-end/package.json | 42 +- .../transaction-type-base.component.ts | 2 +- .../shared/resolvers/transaction.resolver.ts | 2 +- .../shared/services/form-24.service.spec.ts | 94 +- .../src/app/shared/utils/label.utils.spec.ts | 6 +- .../shared/utils/report-code.utils.spec.ts | 6 +- 7 files changed, 960 insertions(+), 911 deletions(-) diff --git a/front-end/package-lock.json b/front-end/package-lock.json index d94867e8ce..606f48c0a4 100644 --- a/front-end/package-lock.json +++ b/front-end/package-lock.json @@ -8,14 +8,14 @@ "name": "fec-e-file", "version": "0.0.0", "dependencies": { - "@angular/router": "21.2.4", + "@angular/router": "21.2.5", "@ngrx/effects": "21.0.1", "@ngrx/store": "21.0.1", "@primeuix/themes": "2.0.3", "class-transformer": "0.5.1", - "fecfile-validate": "https://github.com/fecgov/fecfile-validate#0cf017439a59a08a7bed02e2069045708887209c", - "intl-tel-input": "26.5.1", - "knip": "^5.88.1", + "fecfile-validate": "https://github.com/fecgov/fecfile-validate#0322bb973333f175d45d2b4b06de470a0d5c48d7", + "intl-tel-input": "26.8.1", + "knip": "^6.0.3", "ngrx-store-localstorage": "20.1.0", "ngx-cookie-service": "21.3.1", "ngx-logger": "5.0.12", @@ -31,35 +31,35 @@ "@angular-eslint/eslint-plugin-template": "21.3.1", "@angular-eslint/schematics": "21.3.1", "@angular-eslint/template-parser": "21.3.1", - "@angular/animations": "21.2.4", - "@angular/build": "21.2.2", - "@angular/cdk": "21.2.2", - "@angular/cli": "21.2.2", - "@angular/common": "21.2.4", - "@angular/compiler": "21.2.4", - "@angular/compiler-cli": "21.2.4", - "@angular/core": "21.2.4", - "@angular/forms": "21.2.4", - "@angular/platform-browser": "21.2.4", + "@angular/animations": "21.2.5", + "@angular/build": "21.2.3", + "@angular/cdk": "21.2.3", + "@angular/cli": "21.2.3", + "@angular/common": "21.2.5", + "@angular/compiler": "21.2.5", + "@angular/compiler-cli": "21.2.5", + "@angular/core": "21.2.5", + "@angular/forms": "21.2.5", + "@angular/platform-browser": "21.2.5", "@cypress/schematic": "5.0.0", - "@eslint/eslintrc": "3.3.3", - "@eslint/js": "9.39.3", + "@eslint/eslintrc": "3.3.5", + "@eslint/js": "10.0.1", "@faker-js/faker": "10.3.0", - "@types/node": "^25.3.0", - "@typescript-eslint/eslint-plugin": "8.56.0", - "@typescript-eslint/parser": "8.56.0", + "@types/node": "^25.5.0", + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", "axe-core": "^4.11.1", "cypress": "^15.10.0", "cypress-axe": "^1.7.0", "cypress-mochawesome-reporter": "4.0.2", - "eslint": "9.39.3", - "jsdom": "^29.0.0", + "eslint": "10.1.0", + "jsdom": "^29.0.1", "prettier": "3.8.1", "reflect-metadata": "0.2.2", "rxjs": "7.8.2", "tslib": "2.8.1", "vitest": "^4.1.0", - "zone.js": "0.15.1" + "zone.js": "0.16.1" } }, "node_modules/@algolia/abtesting": { @@ -461,9 +461,9 @@ } }, "node_modules/@angular/animations": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.4.tgz", - "integrity": "sha512-hO1P7ks9n7lW3D31bzHohSuoAaj05xJUlK8rZgX8IkH5DLx4qhvfNh0t4bbLuBJLP2r1TaLsQ8KFcemCkFRO2w==", + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.5.tgz", + "integrity": "sha512-8jH48A1gNph5YGlTXXoXJ/5T6uEZB14ITad3uQwBMM1mUUvM0T4QIMk555jIe1fIHHUyTfRR2y7v8SfTe2++fA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -473,18 +473,18 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "21.2.4" + "@angular/core": "21.2.5" } }, "node_modules/@angular/build": { - "version": "21.2.2", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.2.tgz", - "integrity": "sha512-Vq2eIneNxzhHm1MwEmRqEJDwHU9ODfSRDaMWwtysGMhpoMQmLdfTqkQDmkC2qVUr8mV8Z1i5I+oe5ZJaMr/PlQ==", + "version": "21.2.3", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.3.tgz", + "integrity": "sha512-u4bhVQruK7KOuHQuoltqlHg+szp0f6rnsGIUolJnT3ez5V6OuSoWIxUorSbvryi2DiKRD/3iwMq7qJN1aN9HCA==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2102.2", + "@angular-devkit/architect": "0.2102.3", "@babel/core": "7.29.0", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", @@ -527,7 +527,7 @@ "@angular/platform-browser": "^21.0.0", "@angular/platform-server": "^21.0.0", "@angular/service-worker": "^21.0.0", - "@angular/ssr": "^21.2.2", + "@angular/ssr": "^21.2.3", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^21.0.0", @@ -576,57 +576,10 @@ } } }, - "node_modules/@angular/build/node_modules/@angular-devkit/architect": { - "version": "0.2102.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.2.tgz", - "integrity": "sha512-CDvFtXwyBtMRkTQnm+LfBNLL0yLV8ZGskrM1T6VkcGwXGFDott1FxUdj96ViodYsYL5fbJr0MNA6TlLcanV3kQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "21.2.2", - "rxjs": "7.8.2" - }, - "bin": { - "architect": "bin/cli.js" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/build/node_modules/@angular-devkit/core": { - "version": "21.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.2.tgz", - "integrity": "sha512-xUeKGe4BDQpkz0E6fnAPIJXE0y0nqtap0KhJIBhvN7xi3NenIzTmoi6T9Yv5OOBUdLZbOm4SOel8MhdXiIBpAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.18.0", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.3", - "rxjs": "7.8.2", - "source-map": "0.7.6" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^5.0.0" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, "node_modules/@angular/cdk": { - "version": "21.2.2", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.2.2.tgz", - "integrity": "sha512-9AsZkwqy07No7+0qPydcJfXB6SpA9qLDBanoesNj5KsiZJ62PJH3oIjVyNeQEEe1HQWmSwBnhwN12OPLNMUlnw==", + "version": "21.2.3", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.2.3.tgz", + "integrity": "sha512-7t+UhfbSpIUG9uUyL4b8nI/HyYyrbgAvDwBT8kH4D7If0WiFQhUoottAM0+WZ7Uy+F4nx322K6TOomz/fZJOoQ==", "license": "MIT", "dependencies": { "parse5": "^8.0.0", @@ -640,19 +593,19 @@ } }, "node_modules/@angular/cli": { - "version": "21.2.2", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.2.tgz", - "integrity": "sha512-eZo8/qX+ZIpIWc0CN+cCX13Lbgi/031wAp8DRVhDDO6SMVtcr/ObOQ2S16+pQdOMXxiG3vby6IhzJuz9WACzMQ==", + "version": "21.2.3", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.3.tgz", + "integrity": "sha512-QzDxnSy8AUOz6ca92xfbNuEmRdWRDi1dfFkxDVr+4l6XUnA9X6VmOi7ioCO1I9oDR73LXHybOqkqHBYDlqt/Ag==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2102.2", - "@angular-devkit/core": "21.2.2", - "@angular-devkit/schematics": "21.2.2", + "@angular-devkit/architect": "0.2102.3", + "@angular-devkit/core": "21.2.3", + "@angular-devkit/schematics": "21.2.3", "@inquirer/prompts": "7.10.1", "@listr2/prompt-adapter-inquirer": "3.0.5", "@modelcontextprotocol/sdk": "1.26.0", - "@schematics/angular": "21.2.2", + "@schematics/angular": "21.2.3", "@yarnpkg/lockfile": "1.1.0", "algoliasearch": "5.48.1", "ini": "6.0.0", @@ -674,76 +627,10 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { - "version": "0.2102.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.2.tgz", - "integrity": "sha512-CDvFtXwyBtMRkTQnm+LfBNLL0yLV8ZGskrM1T6VkcGwXGFDott1FxUdj96ViodYsYL5fbJr0MNA6TlLcanV3kQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "21.2.2", - "rxjs": "7.8.2" - }, - "bin": { - "architect": "bin/cli.js" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/cli/node_modules/@angular-devkit/core": { - "version": "21.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.2.tgz", - "integrity": "sha512-xUeKGe4BDQpkz0E6fnAPIJXE0y0nqtap0KhJIBhvN7xi3NenIzTmoi6T9Yv5OOBUdLZbOm4SOel8MhdXiIBpAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.18.0", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.3", - "rxjs": "7.8.2", - "source-map": "0.7.6" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^5.0.0" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular/cli/node_modules/@angular-devkit/schematics": { - "version": "21.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.2.tgz", - "integrity": "sha512-CCeyQxGUq+oyGnHd7PfcYIVbj9pRnqjQq0rAojoAqs1BJdtInx9weLBCLy+AjM3NHePeZrnwm+wEVr8apED8kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "21.2.2", - "jsonc-parser": "3.3.1", - "magic-string": "0.30.21", - "ora": "9.3.0", - "rxjs": "7.8.2" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, "node_modules/@angular/common": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.4.tgz", - "integrity": "sha512-NrP6qOuUpo3fqq14UJ1b2bIRtWsfvxh1qLqOyFV4gfBrHhXd0XffU1LUlUw1qp4w1uBSgPJ0/N5bSPUWrAguVg==", + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.5.tgz", + "integrity": "sha512-MTjCbsHBkF9W12CW9yYiTJdVfZv/qCqBCZ2iqhMpDA5G+ZJiTKP0IDTJVrx2N5iHfiJ1lnK719t/9GXROtEAvg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -752,14 +639,14 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "21.2.4", + "@angular/core": "21.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.4.tgz", - "integrity": "sha512-9+ulVK3idIo/Tu4X2ic7/V0+Uj7pqrOAbOuIirYe6Ymm3AjexuFRiGBbfcH0VJhQ5cf8TvIJ1fuh+MI4JiRIxA==", + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.5.tgz", + "integrity": "sha512-QloEsknGqLvmr+ED7QShDt7SoMY9mipV+gVnwn4hBI5sbl+TOBfYWXIaJMnxseFwSqjXTSCVGckfylIlynNcFg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -770,9 +657,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.4.tgz", - "integrity": "sha512-vGjd7DZo/Ox50pQCm5EycmBu91JclimPtZoyNXu/2hSxz3oAkzwiHCwlHwk2g58eheSSp+lYtYRLmHAqSVZLjg==", + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.5.tgz", + "integrity": "sha512-Ox3vz6KAM7i47ujR/3M3NCOeCRn6vrC9yV1SHZRhSrYg6CWWcOMveavEEwtNjYtn3hOzrktO4CnuVwtDbU8pLg==", "dev": true, "license": "MIT", "dependencies": { @@ -793,7 +680,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "21.2.4", + "@angular/compiler": "21.2.5", "typescript": ">=5.9 <6.1" }, "peerDependenciesMeta": { @@ -803,9 +690,9 @@ } }, "node_modules/@angular/core": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.4.tgz", - "integrity": "sha512-2+gd67ZuXHpGOqeb2o7XZPueEWEP81eJza2tSHkT5QMV8lnYllDEmaNnkPxnIjSLGP1O3PmiXxo4z8ibHkLZwg==", + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.5.tgz", + "integrity": "sha512-JgHU134Adb1wrpyGC9ozcv3hiRAgaFTvJFn1u9OU/AVXyxu4meMmVh2hp5QhAvPnv8XQdKWWIkAY+dbpPE6zKA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -814,7 +701,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "21.2.4", + "@angular/compiler": "21.2.5", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0 || ~0.16.0" }, @@ -828,9 +715,9 @@ } }, "node_modules/@angular/forms": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.4.tgz", - "integrity": "sha512-1fOhctA9ADEBYjI3nPQUR5dHsK2+UWAjup37Ksldk/k0w8UpD5YsN7JVNvsDMZRFMucKYcGykPblU7pABtsqnQ==", + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.5.tgz", + "integrity": "sha512-pqRuK+a1ZAFZbs8/dZoorFJah2IWaf/SH8axHUpaDJ7fyNrwNEcpczyObdxZ00lOgORpKAhWo/q0hlVS+In8cw==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -840,16 +727,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.2.4", - "@angular/core": "21.2.4", - "@angular/platform-browser": "21.2.4", + "@angular/common": "21.2.5", + "@angular/core": "21.2.5", + "@angular/platform-browser": "21.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/platform-browser": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.4.tgz", - "integrity": "sha512-1A9e/cQVu+3BkRCktLcO3RZGuw8NOTHw1frUUrpAz+iMyvIT4sDRFbL+U1g8qmOCZqRNC1Pi1HZfZ1kl6kvrcQ==", + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.5.tgz", + "integrity": "sha512-VuuYguxjgyI4XWuoXrKynmuA3FB991pXbkNhxHeCW0yX+7DGOnGLPF1oierd4/X+IvskmN8foBZLfjyg9u4Ffg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -858,9 +745,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/animations": "21.2.4", - "@angular/common": "21.2.4", - "@angular/core": "21.2.4" + "@angular/animations": "21.2.5", + "@angular/common": "21.2.5", + "@angular/core": "21.2.5" }, "peerDependenciesMeta": { "@angular/animations": { @@ -869,9 +756,9 @@ } }, "node_modules/@angular/router": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.4.tgz", - "integrity": "sha512-OjWze4XT8i2MThcBXMv7ru1k6/5L6QYZbcXuseqimFCHm2avEJ+mXPovY066fMBZJhqbXdjB82OhHAWkIHjglQ==", + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.5.tgz", + "integrity": "sha512-yQGhTVGvh8OMW3auj13+g+OCSQj7gyBQON/2X4LuCvIUG71NPV6Fqzfk9DKTKaXpqo0FThy8/LPJ0Lsy3CRejg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -880,9 +767,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.2.4", - "@angular/core": "21.2.4", - "@angular/platform-browser": "21.2.4", + "@angular/common": "21.2.5", + "@angular/core": "21.2.5", + "@angular/platform-browser": "21.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -914,9 +801,9 @@ } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.3.tgz", - "integrity": "sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.4.tgz", + "integrity": "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==", "dev": true, "license": "MIT", "dependencies": { @@ -1964,61 +1851,100 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.7", + "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", - "minimatch": "^3.1.5" + "minimatch": "^10.2.4" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0" + "@eslint/core": "^1.1.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -2063,40 +1989,48 @@ "license": "MIT" }, "node_modules/@eslint/js": { - "version": "9.39.3", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", - "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", + "@eslint/core": "^1.1.1", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@exodus/bytes": { @@ -3555,20 +3489,10 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@oxc-project/types": { - "version": "0.113.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz", - "integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@oxc-resolver/binding-android-arm-eabi": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.19.1.tgz", - "integrity": "sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==", + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.120.0.tgz", + "integrity": "sha512-WU3qtINx802wOl8RxAF1v0VvmC2O4D9M8Sv486nLeQ7iPHVmncYZrtBhB4SYyX+XZxj2PNnCcN+PW21jHgiOxg==", "cpu": [ "arm" ], @@ -3576,12 +3500,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@oxc-resolver/binding-android-arm64": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.19.1.tgz", - "integrity": "sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA==", + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.120.0.tgz", + "integrity": "sha512-SEf80EHdhlbjZEgzeWm0ZA/br4GKMenDW3QB/gtyeTV1gStvvZeFi40ioHDZvds2m4Z9J1bUAUL8yn1/+A6iGg==", "cpu": [ "arm64" ], @@ -3589,12 +3516,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@oxc-resolver/binding-darwin-arm64": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.19.1.tgz", - "integrity": "sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==", + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.120.0.tgz", + "integrity": "sha512-xVrrbCai8R8CUIBu3CjryutQnEYhZqs1maIqDvtUCFZb8vY33H7uh9mHpL3a0JBIKoBUKjPH8+rzyAeXnS2d6A==", "cpu": [ "arm64" ], @@ -3602,12 +3532,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@oxc-resolver/binding-darwin-x64": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.19.1.tgz", - "integrity": "sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ==", + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.120.0.tgz", + "integrity": "sha512-xyHBbnJ6mydnQUH7MAcafOkkrNzQC6T+LXgDH/3InEq2BWl/g424IMRiJVSpVqGjB+p2bd0h0WRR8iIwzjU7rw==", "cpu": [ "x64" ], @@ -3615,12 +3548,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@oxc-resolver/binding-freebsd-x64": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.19.1.tgz", - "integrity": "sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw==", + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.120.0.tgz", + "integrity": "sha512-UMnVRllquXUYTeNfFKmxTTEdZ/ix1nLl0ducDzMSREoWYGVIHnOOxoKMWlCOvRr9Wk/HZqo2rh1jeumbPGPV9A==", "cpu": [ "x64" ], @@ -3628,12 +3564,15 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.19.1.tgz", - "integrity": "sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A==", + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.120.0.tgz", + "integrity": "sha512-tkvn2CQ7QdcsMnpfiX3fd3wA3EFsWKYlcQzq9cFw/xc89Al7W6Y4O0FgLVkVQpo0Tnq/qtE1XfkJOnRRA9S/NA==", "cpu": [ "arm" ], @@ -3641,12 +3580,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.19.1.tgz", - "integrity": "sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ==", + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.120.0.tgz", + "integrity": "sha512-WN5y135Ic42gQDk9grbwY9++fDhqf8knN6fnP+0WALlAUh4odY/BDK1nfTJRSfpJD9P3r1BwU0m3pW2DU89whQ==", "cpu": [ "arm" ], @@ -3654,12 +3596,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.19.1.tgz", - "integrity": "sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==", + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.120.0.tgz", + "integrity": "sha512-1GgQBCcXvFMw99EPdMy+4NZ3aYyXsxjf9kbUUg8HuAy3ZBXzOry5KfFEzT9nqmgZI1cuetvApkiJBZLAPo8uaw==", "cpu": [ "arm64" ], @@ -3667,12 +3612,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@oxc-resolver/binding-linux-arm64-musl": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.19.1.tgz", - "integrity": "sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==", + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.120.0.tgz", + "integrity": "sha512-gmMQ70gsPdDBgpcErvJEoWNBr7bJooSLlvOBVBSGfOzlP5NvJ3bFvnUeZZ9d+dPrqSngtonf7nyzWUTUj/U+lw==", "cpu": [ "arm64" ], @@ -3680,12 +3628,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.19.1.tgz", - "integrity": "sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==", + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.120.0.tgz", + "integrity": "sha512-T/kZuU0ajop0xhzVMwH5r3srC9Nqup5HaIo+3uFjIN5uPxa0LvSxC1ZqP4aQGJVW5G0z8/nCkjIfSMS91P/wzw==", "cpu": [ "ppc64" ], @@ -3693,9 +3644,311 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.120.0.tgz", + "integrity": "sha512-vn21KXLAXzaI3N5CZWlBr1iWeXLl9QFIMor7S1hUjUGTeUuWCoE6JZB040/ZNDwf+JXPX8Ao9KbmJq9FMC2iGw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.120.0.tgz", + "integrity": "sha512-SUbUxlar007LTGmSLGIC5x/WJvwhdX+PwNzFJ9f/nOzZOrCFbOT4ikt7pJIRg1tXVsEfzk5mWpGO1NFiSs4PIw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.120.0.tgz", + "integrity": "sha512-hYiPJTxyfJY2+lMBFk3p2bo0R9GN+TtpPFlRqVchL1qvLG+pznstramHNvJlw9AjaoRUHwp9IKR7UZQnRPGjgQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.120.0.tgz", + "integrity": "sha512-q+5jSVZkprJCIy3dzJpApat0InJaoxQLsJuD6DkX8hrUS61z2lHQ1Fe9L2+TYbKHXCLWbL0zXe7ovkIdopBGMQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.120.0.tgz", + "integrity": "sha512-D9QDDZNnH24e7X4ftSa6ar/2hCavETfW3uk0zgcMIrZNy459O5deTbWrjGzZiVrSWigGtlQwzs2McBP0QsfV1w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.120.0.tgz", + "integrity": "sha512-TBU8ZwOUWAOUWVfmI16CYWbvh4uQb9zHnGBHsw5Cp2JUVG044OIY1CSHODLifqzQIMTXvDvLzcL89GGdUIqNrA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.120.0.tgz", + "integrity": "sha512-WG/FOZgDJCpJnuF3ToG/K28rcOmSY7FmFmfBKYb2fmLyhDzPpUldFGV7/Fz4ru0Iz/v4KPmf8xVgO8N3lO4KHA==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.120.0.tgz", + "integrity": "sha512-1T0HKGcsz/BKo77t7+89L8Qvu4f9DoleKWHp3C5sJEcbCjDOLx3m9m722bWZTY+hANlUEs+yjlK+lBFsA+vrVQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.120.0.tgz", + "integrity": "sha512-L7vfLzbOXsjBXV0rv/6Y3Jd9BRjPeCivINZAqrSyAOZN3moCopDN+Psq9ZrGNZtJzP8946MtlRFZ0Als0wBCOw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.120.0.tgz", + "integrity": "sha512-ys+upfqNtSu58huAhJMBKl3XCkGzyVFBlMlGPzHeFKgpFF/OdgNs1MMf8oaJIbgMH8ZxgGF7qfue39eJohmKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", + "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.19.1.tgz", + "integrity": "sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.19.1.tgz", + "integrity": "sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.19.1.tgz", + "integrity": "sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.19.1.tgz", + "integrity": "sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.19.1.tgz", + "integrity": "sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.19.1.tgz", + "integrity": "sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.19.1.tgz", + "integrity": "sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.19.1.tgz", + "integrity": "sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.19.1.tgz", + "integrity": "sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.19.1.tgz", + "integrity": "sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { "version": "11.19.1", "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.19.1.tgz", "integrity": "sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==", @@ -4788,14 +5041,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "21.2.2", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.2.tgz", - "integrity": "sha512-Ywa6HDtX7TRBQZTVMMnxX3Mk7yVnG8KtSFaXWrkx779+q8tqYdBwNwAqbNd4Zatr1GccKaz9xcptHJta5+DTxw==", + "version": "21.2.3", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.3.tgz", + "integrity": "sha512-rCEprgpNbJLl9Rm/t92eRYc1eIqD4BAJqB1OO8fzQolyDajCcOBpohjXkuLYSwK9RMyS6f+szNnYGOQawlrPYw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.2", - "@angular-devkit/schematics": "21.2.2", + "@angular-devkit/core": "21.2.3", + "@angular-devkit/schematics": "21.2.3", "jsonc-parser": "3.3.1" }, "engines": { @@ -4804,53 +5057,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { - "version": "21.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.2.tgz", - "integrity": "sha512-xUeKGe4BDQpkz0E6fnAPIJXE0y0nqtap0KhJIBhvN7xi3NenIzTmoi6T9Yv5OOBUdLZbOm4SOel8MhdXiIBpAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.18.0", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.3", - "rxjs": "7.8.2", - "source-map": "0.7.6" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^5.0.0" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@schematics/angular/node_modules/@angular-devkit/schematics": { - "version": "21.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.2.tgz", - "integrity": "sha512-CCeyQxGUq+oyGnHd7PfcYIVbj9pRnqjQq0rAojoAqs1BJdtInx9weLBCLy+AjM3NHePeZrnwm+wEVr8apED8kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "21.2.2", - "jsonc-parser": "3.3.1", - "magic-string": "0.30.21", - "ora": "9.3.0", - "rxjs": "7.8.2" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, "node_modules/@sigstore/bundle": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", @@ -5053,6 +5259,7 @@ "version": "25.5.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.18.0" @@ -5082,241 +5289,29 @@ "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", - "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/type-utils": "8.56.0", - "@typescript-eslint/utils": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.56.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", - "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", - "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", - "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", - "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", - "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.0", - "@typescript-eslint/types": "^8.56.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", - "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", - "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", - "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", - "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", - "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "optional": true, + "dependencies": { + "@types/node": "*" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", - "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5326,40 +5321,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { + "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", - "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", - "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "node_modules/@typescript-eslint/parser": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.0", - "@typescript-eslint/tsconfig-utils": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", - "debug": "^4.4.3", - "minimatch": "^9.0.5", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5369,61 +5347,41 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/types": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", - "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "debug": "^4.4.3" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", - "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5431,24 +5389,14 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/project-service": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", - "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.1", - "@typescript-eslint/types": "^8.57.1", - "debug": "^4.4.3" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -5460,16 +5408,18 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", - "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5477,38 +5427,37 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", - "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "node_modules/@typescript-eslint/types": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", - "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/project-service": "8.57.1", - "@typescript-eslint/tsconfig-utils": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -5526,43 +5475,22 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", - "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@typescript-eslint/types": "8.57.1", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/balanced-match": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "18 || 20 || >=22" } }, - "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^4.0.2" }, @@ -5570,27 +5498,12 @@ "node": "18 || 20 || >=22" } }, - "node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/minimatch": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "brace-expansion": "^5.0.2" }, @@ -5601,15 +5514,17 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", - "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "node_modules/@typescript-eslint/utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "eslint-visitor-keys": "^5.0.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5617,14 +5532,22 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/@typescript-eslint/types": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", - "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "eslint-visitor-keys": "^5.0.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -5660,16 +5583,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", - "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.1.tgz", + "integrity": "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/spy": "4.1.1", + "@vitest/utils": "4.1.1", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" }, @@ -5678,13 +5601,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", - "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.1.tgz", + "integrity": "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.0", + "@vitest/spy": "4.1.1", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -5693,7 +5616,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -5705,9 +5628,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", - "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.1.tgz", + "integrity": "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5718,13 +5641,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", - "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.1.tgz", + "integrity": "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.0", + "@vitest/utils": "4.1.1", "pathe": "^2.0.3" }, "funding": { @@ -5732,14 +5655,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", - "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.1.tgz", + "integrity": "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/pretty-format": "4.1.1", + "@vitest/utils": "4.1.1", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -5748,9 +5671,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", - "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.1.tgz", + "integrity": "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==", "dev": true, "license": "MIT", "funding": { @@ -5758,13 +5681,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", - "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.1.tgz", + "integrity": "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", + "@vitest/pretty-format": "4.1.1", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" }, @@ -6157,9 +6080,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.9", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz", - "integrity": "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==", + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6528,9 +6451,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001780", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", - "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", "dev": true, "funding": [ { @@ -7849,33 +7772,30 @@ } }, "node_modules/eslint": { - "version": "9.39.3", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", - "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", + "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.3", - "@eslint/plugin-kit": "^0.4.1", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.3", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", + "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -7885,8 +7805,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -7894,7 +7813,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -7957,31 +7876,55 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "node_modules/eslint/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "balanced-match": "^4.0.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "node_modules/eslint/node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, - "license": "Apache-2.0", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -8004,6 +7947,22 @@ "dev": true, "license": "MIT" }, + "node_modules/eslint/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -8439,8 +8398,8 @@ }, "node_modules/fecfile-validate": { "version": "0.0.1", - "resolved": "git+ssh://git@github.com/fecgov/fecfile-validate.git#0cf017439a59a08a7bed02e2069045708887209c", - "integrity": "sha512-6R3Gpx9yRZSYXNysfs3hdcFiu6ftfhlq6Ps1Z5SbJGCWKJYWNQnQIHoOqdEh+dOAizU9i2EuMvu3hIXxtyxZhA==", + "resolved": "git+ssh://git@github.com/fecgov/fecfile-validate.git#0322bb973333f175d45d2b4b06de470a0d5c48d7", + "integrity": "sha512-nbPIzJOBhjdhfGHqad5g11YY3oQ59a6tk6Q6iBf9tVr3w16YPatXRh6df8LJxdDHyuQko1aSuqvm6+ctVUsSSg==", "hasInstallScript": true, "license": "CC0-1.0", "dependencies": { @@ -8797,6 +8756,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -9026,9 +8997,9 @@ } }, "node_modules/hono": { - "version": "4.12.8", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz", - "integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==", + "version": "4.12.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", + "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", "dev": true, "license": "MIT", "engines": { @@ -9347,9 +9318,9 @@ } }, "node_modules/intl-tel-input": { - "version": "26.5.1", - "resolved": "https://registry.npmjs.org/intl-tel-input/-/intl-tel-input-26.5.1.tgz", - "integrity": "sha512-7A6pnLKbhsRK3EcwcWtYYJyU9CqfajOs2obdvaRz4zEXJRkVuCwiyXL/RX7xK9xWgJSiUzEunSALP+1sR5zyiA==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/intl-tel-input/-/intl-tel-input-26.8.1.tgz", + "integrity": "sha512-6UBRifs+nGtlS03SZGxxb9uVX7MTo1ePNJ4zIxO+qiwVwJ6fW7gYTcvYVFKjBn7jlbPw2ZqjGeNLaSUEwmDvIA==", "license": "MIT", "workspaces": [ "site" @@ -9621,14 +9592,14 @@ "license": "MIT" }, "node_modules/jsdom": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.0.tgz", - "integrity": "sha512-9FshNB6OepopZ08unmmGpsF7/qCjxGPbo3NbgfJAnPeHXnsODE9WWffXZtRFRFe0ntzaAOcSKNJFz8wiyvF1jQ==", + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz", + "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==", "dev": true, "license": "MIT", "dependencies": { "@asamuzakjp/css-color": "^5.0.1", - "@asamuzakjp/dom-selector": "^7.0.2", + "@asamuzakjp/dom-selector": "^7.0.3", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.1", "@exodus/bytes": "^1.15.0", @@ -9642,7 +9613,7 @@ "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", - "undici": "^7.24.3", + "undici": "^7.24.5", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", @@ -9672,22 +9643,22 @@ } }, "node_modules/jsdom/node_modules/tldts": { - "version": "7.0.26", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.26.tgz", - "integrity": "sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.26" + "tldts-core": "^7.0.27" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/jsdom/node_modules/tldts-core": { - "version": "7.0.26", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.26.tgz", - "integrity": "sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", "dev": true, "license": "MIT" }, @@ -9705,9 +9676,9 @@ } }, "node_modules/jsdom/node_modules/undici": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", - "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz", + "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==", "dev": true, "license": "MIT", "engines": { @@ -9848,9 +9819,9 @@ } }, "node_modules/knip": { - "version": "5.88.1", - "resolved": "https://registry.npmjs.org/knip/-/knip-5.88.1.tgz", - "integrity": "sha512-tpy5o7zu1MjawVkLPuahymVJekYY3kYjvzcoInhIchgePxTlo+api90tBv2KfhAIe5uXh+mez1tAfmbv8/TiZg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/knip/-/knip-6.0.3.tgz", + "integrity": "sha512-6Ai+Iv41dVpBYH6mReFejhniWq4eiaKrBw4kghqz2Ew5psQMYEqYxJtXLdj/7vRJ3nVaHpakhYUCKO8p3ftNsQ==", "funding": [ { "type": "github", @@ -9866,8 +9837,10 @@ "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", + "get-tsconfig": "4.13.7", "jiti": "^2.6.0", "minimist": "^1.2.8", + "oxc-parser": "^0.120.0", "oxc-resolver": "^11.19.1", "picocolors": "^1.1.1", "picomatch": "^4.0.1", @@ -9882,11 +9855,7 @@ "knip-bun": "bin/knip-bun.js" }, "engines": { - "node": ">=18.18.0" - }, - "peerDependencies": { - "@types/node": ">=18", - "typescript": ">=5.0.4 <7" + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/knip/node_modules/strip-json-comments": { @@ -10098,13 +10067,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -11956,6 +11918,43 @@ "dev": true, "license": "MIT" }, + "node_modules/oxc-parser": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.120.0.tgz", + "integrity": "sha512-WyPWZlcIm+Fkte63FGfgFB8mAAk33aH9h5N9lphXVOHSXEBFFsmYdOBedVKly363aWABjZdaj/m9lBfEY4wt+w==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.120.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.120.0", + "@oxc-parser/binding-android-arm64": "0.120.0", + "@oxc-parser/binding-darwin-arm64": "0.120.0", + "@oxc-parser/binding-darwin-x64": "0.120.0", + "@oxc-parser/binding-freebsd-x64": "0.120.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.120.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.120.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.120.0", + "@oxc-parser/binding-linux-arm64-musl": "0.120.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.120.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.120.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.120.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.120.0", + "@oxc-parser/binding-linux-x64-gnu": "0.120.0", + "@oxc-parser/binding-linux-x64-musl": "0.120.0", + "@oxc-parser/binding-openharmony-arm64": "0.120.0", + "@oxc-parser/binding-wasm32-wasi": "0.120.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.120.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.120.0", + "@oxc-parser/binding-win32-x64-msvc": "0.120.0" + } + }, "node_modules/oxc-resolver": { "version": "11.19.1", "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.19.1.tgz", @@ -12649,6 +12648,15 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -12729,6 +12737,16 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.4" } }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.113.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz", + "integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", @@ -13196,9 +13214,9 @@ } }, "node_modules/smol-toml": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", - "integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", "license": "BSD-3-Clause", "engines": { "node": ">= 18" @@ -13534,9 +13552,9 @@ } }, "node_modules/tar": { - "version": "7.5.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", - "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", + "version": "7.5.12", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.12.tgz", + "integrity": "sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -13881,6 +13899,7 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, "license": "MIT" }, "node_modules/universalify": { @@ -14559,19 +14578,19 @@ } }, "node_modules/vitest": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", - "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.1.tgz", + "integrity": "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.0", - "@vitest/mocker": "4.1.0", - "@vitest/pretty-format": "4.1.0", - "@vitest/runner": "4.1.0", - "@vitest/snapshot": "4.1.0", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/expect": "4.1.1", + "@vitest/mocker": "4.1.1", + "@vitest/pretty-format": "4.1.1", + "@vitest/runner": "4.1.1", + "@vitest/snapshot": "4.1.1", + "@vitest/spy": "4.1.1", + "@vitest/utils": "4.1.1", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -14583,7 +14602,7 @@ "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -14599,13 +14618,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.0", - "@vitest/browser-preview": "4.1.0", - "@vitest/browser-webdriverio": "4.1.0", - "@vitest/ui": "4.1.0", + "@vitest/browser-playwright": "4.1.1", + "@vitest/browser-preview": "4.1.1", + "@vitest/browser-webdriverio": "4.1.1", + "@vitest/ui": "4.1.1", "happy-dom": "*", "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -14852,9 +14871,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -15045,9 +15064,9 @@ } }, "node_modules/zone.js": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", - "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.1.tgz", + "integrity": "sha512-dpvY17vxYIW3+bNrP0ClUlaiY0CiIRK3tnoLaGoQsQcY9/I/NpzIWQ7tQNhbV7LacQMpCII6wVzuL3tuWOyfuA==", "devOptional": true, "license": "MIT" } diff --git a/front-end/package.json b/front-end/package.json index 667e00f8ba..56bdff9f8c 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -28,14 +28,14 @@ }, "private": true, "dependencies": { - "@angular/router": "21.2.4", + "@angular/router": "21.2.5", "@ngrx/effects": "21.0.1", "@ngrx/store": "21.0.1", "@primeuix/themes": "2.0.3", "class-transformer": "0.5.1", "fecfile-validate": "https://github.com/fecgov/fecfile-validate#0322bb973333f175d45d2b4b06de470a0d5c48d7", - "intl-tel-input": "26.5.1", - "knip": "^5.88.1", + "intl-tel-input": "26.8.1", + "knip": "^6.0.3", "ngrx-store-localstorage": "20.1.0", "ngx-cookie-service": "21.3.1", "ngx-logger": "5.0.12", @@ -51,35 +51,35 @@ "@angular-eslint/eslint-plugin-template": "21.3.1", "@angular-eslint/schematics": "21.3.1", "@angular-eslint/template-parser": "21.3.1", - "@angular/animations": "21.2.4", - "@angular/build": "21.2.2", - "@angular/cdk": "21.2.2", - "@angular/cli": "21.2.2", - "@angular/common": "21.2.4", - "@angular/compiler": "21.2.4", - "@angular/compiler-cli": "21.2.4", - "@angular/core": "21.2.4", - "@angular/forms": "21.2.4", - "@angular/platform-browser": "21.2.4", + "@angular/animations": "21.2.5", + "@angular/build": "21.2.3", + "@angular/cdk": "21.2.3", + "@angular/cli": "21.2.3", + "@angular/common": "21.2.5", + "@angular/compiler": "21.2.5", + "@angular/compiler-cli": "21.2.5", + "@angular/core": "21.2.5", + "@angular/forms": "21.2.5", + "@angular/platform-browser": "21.2.5", "@cypress/schematic": "5.0.0", - "@eslint/eslintrc": "3.3.3", - "@eslint/js": "9.39.3", + "@eslint/eslintrc": "3.3.5", + "@eslint/js": "10.0.1", "@faker-js/faker": "10.3.0", - "@types/node": "^25.3.0", - "@typescript-eslint/eslint-plugin": "8.56.0", - "@typescript-eslint/parser": "8.56.0", + "@types/node": "^25.5.0", + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", "axe-core": "^4.11.1", "cypress": "^15.10.0", "cypress-axe": "^1.7.0", "cypress-mochawesome-reporter": "4.0.2", - "eslint": "9.39.3", - "jsdom": "^29.0.0", + "eslint": "10.1.0", + "jsdom": "^29.0.1", "prettier": "3.8.1", "reflect-metadata": "0.2.2", "rxjs": "7.8.2", "tslib": "2.8.1", "vitest": "^4.1.0", - "zone.js": "0.15.1" + "zone.js": "0.16.1" }, "overrides": { "rollup": "4.59.0", diff --git a/front-end/src/app/shared/components/transaction-type-base/transaction-type-base.component.ts b/front-end/src/app/shared/components/transaction-type-base/transaction-type-base.component.ts index 9f9ce2ef10..b64925e910 100644 --- a/front-end/src/app/shared/components/transaction-type-base/transaction-type-base.component.ts +++ b/front-end/src/app/shared/components/transaction-type-base/transaction-type-base.component.ts @@ -217,7 +217,7 @@ export abstract class TransactionTypeBaseComponent extends FormComponent impleme * (a child of the current transaction's parent if it exists) * - If the destination is CHILD, navigate to create a sub-transaction of the current transaction */ - let result = false; + let result: boolean; const reportId = this.activatedRoute.snapshot.params['reportId']; const reportPath = `/reports/transactions/report/${reportId}`; // If the transaction is saved, display a success message diff --git a/front-end/src/app/shared/resolvers/transaction.resolver.ts b/front-end/src/app/shared/resolvers/transaction.resolver.ts index 8308dce532..ab23cb3924 100644 --- a/front-end/src/app/shared/resolvers/transaction.resolver.ts +++ b/front-end/src/app/shared/resolvers/transaction.resolver.ts @@ -87,7 +87,7 @@ export class TransactionResolver { // tune page size const params = { parent: transaction.id ?? '', page_size: 100 }; let pageNumber = 0; - let page: ListRestResponse | null = null; + let page: ListRestResponse; do { page = await this.listService.getTableData(++pageNumber, '', params); for (const result of page.results) { diff --git a/front-end/src/app/shared/services/form-24.service.spec.ts b/front-end/src/app/shared/services/form-24.service.spec.ts index 679c242c76..96f4c3ecb8 100644 --- a/front-end/src/app/shared/services/form-24.service.spec.ts +++ b/front-end/src/app/shared/services/form-24.service.spec.ts @@ -5,58 +5,90 @@ import { testMockStore } from '../utils/unit-test.utils'; import { F24UniqueNameValidator, Form24Service } from './form-24.service'; import { provideHttpClient } from '@angular/common/http'; import { FormGroup, FormControl } from '@angular/forms'; -import type { MockedObject } from 'vitest'; import { Form24 } from '../models'; describe('Form24Service', () => { let service: Form24Service; + let validator: F24UniqueNameValidator; beforeEach(() => { TestBed.configureTestingModule({ - providers: [provideHttpClient(), provideHttpClientTesting(), Form24Service, provideMockStore(testMockStore())], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + Form24Service, + F24UniqueNameValidator, + provideMockStore(testMockStore()), + ], }); service = TestBed.inject(Form24Service); + validator = TestBed.inject(F24UniqueNameValidator); }); it('should be created', () => { expect(service).toBeTruthy(); }); -}); -describe('F24UniqueNameValidator', () => { - let service: MockedObject; - let validator: F24UniqueNameValidator; + describe('F24UniqueNameValidator', () => { + it('should return error if name is duplicate', async () => { + const report = Form24.fromJSON({ name: '24 hourreport' }); + vi.spyOn(service, 'getAllReports').mockResolvedValue([report]); + const control = new FormGroup({ + typeName: new FormControl('24 HOUR'), + form24Name: new FormControl('REPORT'), + }); + const result = await validator.validate(control); + expect(result).toEqual({ duplicateName: true }); + }); - beforeEach(() => { - const spy = { - getAllReports: vi.fn().mockName('Form24Service.getAllReports'), - }; - TestBed.configureTestingModule({ - providers: [F24UniqueNameValidator, { provide: Form24Service, useValue: spy }], + it('should return null if name is unique', async () => { + vi.spyOn(service, 'getAllReports').mockResolvedValue([]); + const control = new FormGroup({ + typeName: new FormControl('24 HOUR'), + form24Name: new FormControl('REPORT'), + }); + const result = await validator.validate(control); + expect(result).toBeNull(); }); - service = TestBed.inject(Form24Service) as MockedObject; - validator = TestBed.inject(F24UniqueNameValidator); - }); - it('should return error if name is duplicate', async () => { - const report = Form24.fromJSON({ name: '24 hourreport' }); - service.getAllReports.mockResolvedValue([report]); - const control = new FormGroup({ - typeName: new FormControl('24 HOUR'), - form24Name: new FormControl('REPORT'), + it('should return required error if typeName is missing', async () => { + const control = new FormGroup({ + typeName: new FormControl(''), + form24Name: new FormControl('REPORT'), + }); + + const result = await validator.validate(control); + expect(result).toEqual({ required: true }); + }); + + it('should return required error if form24Name is missing', async () => { + const control = new FormGroup({ + typeName: new FormControl('24 HOUR'), + form24Name: new FormControl(''), + }); + + const result = await validator.validate(control); + expect(result).toEqual({ required: true }); + }); + + it('should handle reports with null/missing names gracefully', async () => { + const mockReports = [{ name: null } as unknown as Form24, Form24.fromJSON({ name: 'existing' })]; + vi.spyOn(service, 'getAllReports').mockResolvedValue(mockReports); + + const control = new FormGroup({ + typeName: new FormControl('NEW'), + form24Name: new FormControl('REPORT'), + }); + + const result = await validator.validate(control); + expect(result).toBeNull(); }); - const result = await validator.validate(control); - expect(result).toEqual({ duplicateName: true }); - }); - it('should return null if name is unique', async () => { - service.getAllReports.mockResolvedValue([]); - const control = new FormGroup({ - typeName: new FormControl('24 HOUR'), - form24Name: new FormControl('REPORT'), + it('should return null if control gets return null for sub-controls', async () => { + const control = new FormGroup({}); + const result = await validator.validate(control); + expect(result).toEqual({ required: true }); }); - const result = await validator.validate(control); - expect(result).toBeNull(); }); }); diff --git a/front-end/src/app/shared/utils/label.utils.spec.ts b/front-end/src/app/shared/utils/label.utils.spec.ts index 973216d8a6..6d3305fdae 100644 --- a/front-end/src/app/shared/utils/label.utils.spec.ts +++ b/front-end/src/app/shared/utils/label.utils.spec.ts @@ -49,16 +49,14 @@ describe('LabelUtils', () => { }); it('should return value for undefined value', () => { - let testValue; const testTerm = 'testTerm'; - const retval = LabelUtils.htmlHighlightTerm(testValue, testTerm); + const retval = LabelUtils.htmlHighlightTerm(undefined, testTerm); expect(retval).toEqual(undefined); }); it('should return value for undefined term', () => { const testValue = 'testValue'; - let testTerm; - const retval = LabelUtils.htmlHighlightTerm(testValue, testTerm); + const retval = LabelUtils.htmlHighlightTerm(testValue, undefined); expect(retval).toEqual(testValue); }); diff --git a/front-end/src/app/shared/utils/report-code.utils.spec.ts b/front-end/src/app/shared/utils/report-code.utils.spec.ts index 3029ac8381..c6863a2948 100644 --- a/front-end/src/app/shared/utils/report-code.utils.spec.ts +++ b/front-end/src/app/shared/utils/report-code.utils.spec.ts @@ -125,14 +125,14 @@ describe('ReportCodeUtils', () => { const result = getCoverageDatesFunction(ReportCodes.M3); expect(typeof result).toBe('function'); if (result) { - let [startDate, endDate] = result(2024, true, 'M'); + const [startDate, endDate] = result(2024, true, 'M'); expect(startDate.getMonth()).toBe(1); expect(startDate.getDate()).toBe(1); expect(endDate.getMonth()).toBe(1); expect(endDate.getDate()).toBe(29); - [startDate, endDate] = result(2025, true, 'M'); - expect(endDate.getDate()).toBe(28); + const [, endDate1] = result(2025, true, 'M'); + expect(endDate1.getDate()).toBe(28); } }); From e6574100f194087aec7b49a21a1df0864527d6c1 Mon Sep 17 00:00:00 2001 From: Sasha Dresden Date: Mon, 23 Mar 2026 15:27:30 -0400 Subject: [PATCH 45/69] Cleanup tests --- front-end/angular.json | 5 +- front-end/knip.json | 3 +- front-end/package-lock.json | 156 +++++++++++++++++- front-end/package.json | 1 + .../create-committee.component.spec.ts | 2 +- .../src/app/login/login/login.component.ts | 2 +- .../create-f24/create-f24.component.spec.ts | 2 +- .../additional-info-input.component.spec.ts | 8 +- .../searchable-select.component.spec.ts | 3 +- .../table-actions-button.component.spec.ts | 3 +- .../transaction-type-base.component.spec.ts | 62 ++++--- .../http-error.interceptor.spec.ts | 8 +- .../resolvers/transaction.resolver.spec.ts | 2 +- .../shared/services/contact.service.spec.ts | 4 +- .../shared/services/dot-fec.service.spec.ts | 2 +- .../shared/services/form-24.service.spec.ts | 66 ++------ .../shared/services/memo-text.service.spec.ts | 2 - .../shared/services/poller.service.spec.ts | 2 - .../services/transaction-list.service.spec.ts | 1 - .../services/transaction.service.spec.ts | 1 - .../src/app/shared/utils/label.utils.spec.ts | 2 +- front-end/src/test.ts | 24 +-- sonar-project.properties | 4 +- 23 files changed, 244 insertions(+), 121 deletions(-) diff --git a/front-end/angular.json b/front-end/angular.json index 58dc57b9cb..cd620a9a52 100644 --- a/front-end/angular.json +++ b/front-end/angular.json @@ -200,7 +200,10 @@ "options": { "tsConfig": "tsconfig.spec.json", "setupFiles": ["src/test.ts"], - "exclude": ["src/environments/environment.cloud.gov.test.ts"] + "exclude": ["src/environments/environment.cloud.gov.test.ts"], + "coverage": true, + "coverageReporters": ["lcov"], + "coverageExclude": ["src/test.ts"] } }, "lint": { diff --git a/front-end/knip.json b/front-end/knip.json index b04aa328a7..55b30a4d3d 100644 --- a/front-end/knip.json +++ b/front-end/knip.json @@ -11,7 +11,8 @@ "@angular-eslint/schematics", "@angular-eslint/template-parser", "@typescript-eslint/eslint-plugin", - "@typescript-eslint/parser" + "@typescript-eslint/parser", + "@vitest/coverage-v8" ], "ignore": ["src/test.ts"], "workspaces": { diff --git a/front-end/package-lock.json b/front-end/package-lock.json index 606f48c0a4..df87d9c77e 100644 --- a/front-end/package-lock.json +++ b/front-end/package-lock.json @@ -48,6 +48,7 @@ "@types/node": "^25.5.0", "@typescript-eslint/eslint-plugin": "8.57.2", "@typescript-eslint/parser": "8.57.2", + "@vitest/coverage-v8": "^4.1.1", "axe-core": "^4.11.1", "cypress": "^15.10.0", "cypress-axe": "^1.7.0", @@ -1127,6 +1128,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -5582,6 +5593,37 @@ "vite": "^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.1.tgz", + "integrity": "sha512-nZ4RWwGCoGOQRMmU/Q9wlUY540RVRxJZ9lxFsFfy0QV7Zmo5VVBhB6Sl9Xa0KIp2iIs3zWfPlo9LcY1iqbpzCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.1", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.1", + "vitest": "4.1.1" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.1.tgz", @@ -5987,6 +6029,25 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -9042,6 +9103,13 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/htmlparser2": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", @@ -9530,6 +9598,48 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jackspeak": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", @@ -9819,9 +9929,9 @@ } }, "node_modules/knip": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/knip/-/knip-6.0.3.tgz", - "integrity": "sha512-6Ai+Iv41dVpBYH6mReFejhniWq4eiaKrBw4kghqz2Ew5psQMYEqYxJtXLdj/7vRJ3nVaHpakhYUCKO8p3ftNsQ==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/knip/-/knip-6.0.4.tgz", + "integrity": "sha512-r/9F7wcxiFM71WgDFQiToE2hQHwZ/UkGmr74o8eiNFPIg80f7rlQHVrZiRX46Tj2yE3s96wUVNGMnsDMylgInw==", "funding": [ { "type": "github", @@ -10295,6 +10405,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-fetch-happen": { "version": "15.0.5", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.5.tgz", @@ -10389,9 +10527,9 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -13552,9 +13690,9 @@ } }, "node_modules/tar": { - "version": "7.5.12", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.12.tgz", - "integrity": "sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { diff --git a/front-end/package.json b/front-end/package.json index 56bdff9f8c..b15f5a822c 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -79,6 +79,7 @@ "rxjs": "7.8.2", "tslib": "2.8.1", "vitest": "^4.1.0", + "@vitest/coverage-v8": "^4.1.1", "zone.js": "0.16.1" }, "overrides": { diff --git a/front-end/src/app/committee/create-committee/create-committee.component.spec.ts b/front-end/src/app/committee/create-committee/create-committee.component.spec.ts index a7fe0f8b7d..ac75969505 100644 --- a/front-end/src/app/committee/create-committee/create-committee.component.spec.ts +++ b/front-end/src/app/committee/create-committee/create-committee.component.spec.ts @@ -116,7 +116,7 @@ describe('CreateCommitteeComponent', () => { it('should handle failed create', async () => { const spy = vi .spyOn(testCommitteeAccountService, 'createCommitteeAccount') - .mockImplementation(() => Promise.reject(new Error())); + .mockImplementation(() => Promise.reject(new Error('Failed to create committee account'))); expect(component.unableToCreateAccount).toBe(false); expect(component.selectedCommittee).toBeFalsy(); diff --git a/front-end/src/app/login/login/login.component.ts b/front-end/src/app/login/login/login.component.ts index e6b577ab7e..d877f6f6cc 100644 --- a/front-end/src/app/login/login/login.component.ts +++ b/front-end/src/app/login/login/login.component.ts @@ -28,7 +28,7 @@ export class LoginComponent implements OnInit, AfterViewChecked { } navigateToLoginDotGov() { - window.location.href = this.loginDotGovAuthUrl ?? ''; + globalThis.location.href = this.loginDotGovAuthUrl ?? ''; } updateScrollbarWidth() { diff --git a/front-end/src/app/reports/form-type-dialog/create-f24/create-f24.component.spec.ts b/front-end/src/app/reports/form-type-dialog/create-f24/create-f24.component.spec.ts index 94246cbf5d..05c391f721 100644 --- a/front-end/src/app/reports/form-type-dialog/create-f24/create-f24.component.spec.ts +++ b/front-end/src/app/reports/form-type-dialog/create-f24/create-f24.component.spec.ts @@ -54,7 +54,7 @@ describe('CreateF24Component', () => { it('should validate form24Name correctly', () => { component.form24Names.set(['24 HOUR TEST']); - TestBed.tick(); + expect(component.form24Names.value()).toEqual(['24 HOUR TEST']); // Name is empty diff --git a/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts b/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts index 6e5b128165..8afb897a91 100644 --- a/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts +++ b/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts @@ -45,11 +45,15 @@ class TestHostComponent { updateFormWithQuaternaryContact(event: SelectItem) { console.log(event); } - clearFormQuaternaryContact() {} + clearFormQuaternaryContact() { + console.log('clear quaternary contact'); + } updateFormWithQuinaryContact(event: SelectItem) { console.log(event); } - clearFormQuinaryContact() {} + clearFormQuinaryContact() { + console.log('clear quinary contact'); + } } describe('AdditionalInfoInputComponent', () => { diff --git a/front-end/src/app/shared/components/searchable-select/searchable-select.component.spec.ts b/front-end/src/app/shared/components/searchable-select/searchable-select.component.spec.ts index 813aa482ee..87c4cc4260 100644 --- a/front-end/src/app/shared/components/searchable-select/searchable-select.component.spec.ts +++ b/front-end/src/app/shared/components/searchable-select/searchable-select.component.spec.ts @@ -4,7 +4,6 @@ import { PrimeOptions } from 'app/shared/utils/label.utils'; import { SearchableSelectComponent } from './searchable-select.component'; import { FormControl, FormGroup } from '@angular/forms'; import { Select } from 'primeng/select'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @Component({ imports: [SearchableSelectComponent], @@ -38,7 +37,7 @@ describe('SearchableSelectComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TestHostComponent, NoopAnimationsModule], + imports: [TestHostComponent], }).compileComponents(); fixture = TestBed.createComponent(TestHostComponent); diff --git a/front-end/src/app/shared/components/table-actions-button/table-actions-button.component.spec.ts b/front-end/src/app/shared/components/table-actions-button/table-actions-button.component.spec.ts index 3ab56ac94f..3220a2ac1f 100644 --- a/front-end/src/app/shared/components/table-actions-button/table-actions-button.component.spec.ts +++ b/front-end/src/app/shared/components/table-actions-button/table-actions-button.component.spec.ts @@ -2,7 +2,6 @@ import { Component, viewChild } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ButtonModule } from 'primeng/button'; import { PopoverModule } from 'primeng/popover'; import { TableActionsButtonComponent } from './table-actions-button.component'; @@ -56,7 +55,7 @@ describe('TableActionsButtonComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PopoverModule, ButtonModule, TableActionsButtonComponent, TestHostComponent, NoopAnimationsModule], + imports: [PopoverModule, ButtonModule, TableActionsButtonComponent, TestHostComponent], providers: [provideHttpClient(), provideHttpClientTesting(), ApiService], }).compileComponents(); diff --git a/front-end/src/app/shared/components/transaction-type-base/transaction-type-base.component.spec.ts b/front-end/src/app/shared/components/transaction-type-base/transaction-type-base.component.spec.ts index 299a619beb..1091c86122 100644 --- a/front-end/src/app/shared/components/transaction-type-base/transaction-type-base.component.spec.ts +++ b/front-end/src/app/shared/components/transaction-type-base/transaction-type-base.component.spec.ts @@ -2,7 +2,7 @@ import type { Mock, MockedObject } from 'vitest'; import { DatePipe } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { provideMockStore } from '@ngrx/store/testing'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { NavigationAction, NavigationDestination, @@ -35,6 +35,7 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ScheduleETransactionTypes, SchETransaction } from 'app/shared/models/sche-transaction.model'; import { ConfirmationWrapperService } from 'app/shared/services/confirmation-wrapper.service'; import { provideZoneChangeDetection } from '@angular/core'; +import { navigationEventSetAction } from 'app/store/navigation-event.actions'; let testTransaction: SchATransaction; @@ -55,6 +56,28 @@ describe('TransactionTypeBaseComponent', () => { const mockRouter = { navigateByUrl: vi.fn(), }; + let store: MockStore; + + async function testMessage(navEvent: NavigationEvent) { + await component.navigateTo(navEvent); + expect(messageServiceSpy.add).toHaveBeenCalledWith({ + severity: 'success', + summary: 'Successful', + detail: 'Transaction Saved', + life: 3000, + }); + } + + async function testNoMessage(navEvent: NavigationEvent) { + await component.navigateTo(navEvent); + expect(messageServiceSpy.add).toHaveBeenCalledTimes(0); + } + + async function testNavigate(navEvent: NavigationEvent, route: string, options?: NavigationBehaviorOptions) { + await component.navigateTo(navEvent); + if (options) expect(mockRouter.navigateByUrl).toHaveBeenCalledWith(route, options); + else expect(mockRouter.navigateByUrl).toHaveBeenCalledWith(route); + } beforeAll(async () => { await import(`fecfile-validate/fecfile_validate_js/dist/INDIVIDUAL_RECEIPT.validator`); @@ -104,6 +127,8 @@ describe('TransactionTypeBaseComponent', () => { ], }).compileComponents(); + store = TestBed.inject(MockStore); + transactionServiceSpy = TestBed.inject(TransactionService) as MockedObject; testConfirmationService = TestBed.inject(ConfirmationService); testwrapperService = TestBed.inject(ConfirmationWrapperService); @@ -161,6 +186,16 @@ describe('TransactionTypeBaseComponent', () => { expect(component.transaction.contact_1_id).toBe('testId'); }); + it('should not trigger effect if NavigationEvent has no transaction', async () => { + const handleNavSpy = vi.spyOn(component, 'handleNavigate'); + const navEvent = new NavigationEvent(NavigationAction.SAVE, NavigationDestination.LIST); + store.dispatch(navigationEventSetAction(navEvent)); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(handleNavSpy).toHaveBeenCalledTimes(0); + }); + describe('save', () => { beforeEach(() => { navEvent = new NavigationEvent(NavigationAction.SAVE, NavigationDestination.LIST, component.transaction); @@ -174,7 +209,7 @@ describe('TransactionTypeBaseComponent', () => { it('should stop processing and throw an error if there is no transaction', async () => { component.transaction = undefined; - await expect(component.submitForm(navEvent)).rejects.toThrowError( + await expect(component.submitForm(navEvent)).rejects.toThrow( 'FECfile+: No transactions submitted for single-entry transaction form.', ); }); @@ -237,7 +272,7 @@ describe('TransactionTypeBaseComponent', () => { 'dialog', component.transaction, ), - ).rejects.toThrowError('FECfile+: Cannot find template map when confirming transaction'); + ).rejects.toThrow('FECfile+: Cannot find template map when confirming transaction'); }); it('should return without confirmation if using parent and contact_1', async () => { @@ -337,27 +372,6 @@ describe('TransactionTypeBaseComponent', () => { }); describe('navigateTo', () => { - async function testMessage(navEvent: NavigationEvent) { - await component.navigateTo(navEvent); - expect(messageServiceSpy.add).toHaveBeenCalledWith({ - severity: 'success', - summary: 'Successful', - detail: 'Transaction Saved', - life: 3000, - }); - } - - async function testNoMessage(navEvent: NavigationEvent) { - await component.navigateTo(navEvent); - expect(messageServiceSpy.add).toHaveBeenCalledTimes(0); - } - - async function testNavigate(navEvent: NavigationEvent, route: string, options?: NavigationBehaviorOptions) { - await component.navigateTo(navEvent); - if (options) expect(mockRouter.navigateByUrl).toHaveBeenCalledWith(route, options); - else expect(mockRouter.navigateByUrl).toHaveBeenCalledWith(route); - } - // ANOTHER // describe('NavigationDestination.ANOTHER', () => { it('should send success to message service', async () => { diff --git a/front-end/src/app/shared/interceptors/http-error.interceptor.spec.ts b/front-end/src/app/shared/interceptors/http-error.interceptor.spec.ts index dfc101b7f8..a769f5c1f0 100644 --- a/front-end/src/app/shared/interceptors/http-error.interceptor.spec.ts +++ b/front-end/src/app/shared/interceptors/http-error.interceptor.spec.ts @@ -41,10 +41,10 @@ describe('HttpErrorInterceptor', () => { throwError(() => new HttpErrorResponse({ status: HttpStatusCode.Forbidden })), ); - testIterceptor.intercept(httpRequest, httpHandlerSpy).subscribe( - (x) => x, - (y) => y, - ); + testIterceptor.intercept(httpRequest, httpHandlerSpy).subscribe({ + next: (x) => x, + error: (y) => y, + }); expect(logOutSpy).toHaveBeenCalled(); }); diff --git a/front-end/src/app/shared/resolvers/transaction.resolver.spec.ts b/front-end/src/app/shared/resolvers/transaction.resolver.spec.ts index 7b6ed70087..953c8becf9 100644 --- a/front-end/src/app/shared/resolvers/transaction.resolver.spec.ts +++ b/front-end/src/app/shared/resolvers/transaction.resolver.spec.ts @@ -468,7 +468,7 @@ describe('TransactionResolver', () => { contact_1: Contact.fromJSON({ id: 123 }), }); }); - await expect(resolver.resolve(route as ActivatedRouteSnapshot)).rejects.toThrowError( + await expect(resolver.resolve(route as ActivatedRouteSnapshot)).rejects.toThrow( 'FECfile+: originating reattribution transaction type not found.', ); }); diff --git a/front-end/src/app/shared/services/contact.service.spec.ts b/front-end/src/app/shared/services/contact.service.spec.ts index 41f20a9f34..edae52978a 100644 --- a/front-end/src/app/shared/services/contact.service.spec.ts +++ b/front-end/src/app/shared/services/contact.service.spec.ts @@ -311,13 +311,13 @@ describe('ContactService', () => { }); it('#getCommitteeDetails should raise an error when no id is provided', async () => { - await expect(service.getCommitteeDetails(null)).rejects.toThrowError( + await expect(service.getCommitteeDetails(null)).rejects.toThrow( 'FECfile+: No Committee Id provided in getCommitteeDetails()', ); }); it('#getCandidateDetails should raise an error when no id is provided', async () => { - await expect(service.getCandidateDetails(null)).rejects.toThrowError( + await expect(service.getCandidateDetails(null)).rejects.toThrow( 'FECfile+: No Candidate Id provided in getCandidateDetails()', ); }); diff --git a/front-end/src/app/shared/services/dot-fec.service.spec.ts b/front-end/src/app/shared/services/dot-fec.service.spec.ts index f4fa3c46cc..17b7f5bf6e 100644 --- a/front-end/src/app/shared/services/dot-fec.service.spec.ts +++ b/front-end/src/app/shared/services/dot-fec.service.spec.ts @@ -108,7 +108,7 @@ describe('DotFecService', () => { it('should download FEC file', async () => { const dotFEC = 'test content'; vi.spyOn(apiService, 'getString').mockReturnValue(Promise.resolve(dotFEC)); - vi.spyOn(window.URL, 'createObjectURL').mockReturnValue('blob:testBlob'); + vi.spyOn(globalThis.URL, 'createObjectURL').mockReturnValue('blob:testBlob'); const link = { href: '', download: '', diff --git a/front-end/src/app/shared/services/form-24.service.spec.ts b/front-end/src/app/shared/services/form-24.service.spec.ts index 96f4c3ecb8..8c32d0b5a9 100644 --- a/front-end/src/app/shared/services/form-24.service.spec.ts +++ b/front-end/src/app/shared/services/form-24.service.spec.ts @@ -30,51 +30,27 @@ describe('Form24Service', () => { expect(service).toBeTruthy(); }); - describe('F24UniqueNameValidator', () => { - it('should return error if name is duplicate', async () => { - const report = Form24.fromJSON({ name: '24 hourreport' }); - vi.spyOn(service, 'getAllReports').mockResolvedValue([report]); - const control = new FormGroup({ - typeName: new FormControl('24 HOUR'), - form24Name: new FormControl('REPORT'), - }); - const result = await validator.validate(control); - expect(result).toEqual({ duplicateName: true }); - }); - - it('should return null if name is unique', async () => { - vi.spyOn(service, 'getAllReports').mockResolvedValue([]); - const control = new FormGroup({ - typeName: new FormControl('24 HOUR'), - form24Name: new FormControl('REPORT'), - }); - const result = await validator.validate(control); - expect(result).toBeNull(); - }); - - it('should return required error if typeName is missing', async () => { - const control = new FormGroup({ - typeName: new FormControl(''), - form24Name: new FormControl('REPORT'), - }); + describe('F24UniqueNameValidator Coverage', () => { + it('should return required error if any name part is missing', async () => { + const scenarios = [ + { type: '', form: 'REPORT' }, + { type: '24 HOUR', form: '' }, + { type: '', form: '' }, + ]; - const result = await validator.validate(control); - expect(result).toEqual({ required: true }); + for (const scenario of scenarios) { + const control = new FormGroup({ + typeName: new FormControl(scenario.type), + form24Name: new FormControl(scenario.form), + }); + const result = await validator.validate(control); + expect(result).toEqual({ required: true }); + } }); - it('should return required error if form24Name is missing', async () => { - const control = new FormGroup({ - typeName: new FormControl('24 HOUR'), - form24Name: new FormControl(''), - }); - - const result = await validator.validate(control); - expect(result).toEqual({ required: true }); - }); - - it('should handle reports with null/missing names gracefully', async () => { - const mockReports = [{ name: null } as unknown as Form24, Form24.fromJSON({ name: 'existing' })]; - vi.spyOn(service, 'getAllReports').mockResolvedValue(mockReports); + it('should handle null/undefined reports or names', async () => { + const form24 = Form24.fromJSON({ name: null }); + vi.spyOn(service, 'getAllReports').mockResolvedValue([form24]); const control = new FormGroup({ typeName: new FormControl('NEW'), @@ -84,11 +60,5 @@ describe('Form24Service', () => { const result = await validator.validate(control); expect(result).toBeNull(); }); - - it('should return null if control gets return null for sub-controls', async () => { - const control = new FormGroup({}); - const result = await validator.validate(control); - expect(result).toEqual({ required: true }); - }); }); }); diff --git a/front-end/src/app/shared/services/memo-text.service.spec.ts b/front-end/src/app/shared/services/memo-text.service.spec.ts index 238ccfa291..9132cf9cdd 100644 --- a/front-end/src/app/shared/services/memo-text.service.spec.ts +++ b/front-end/src/app/shared/services/memo-text.service.spec.ts @@ -56,7 +56,6 @@ describe('MemoTextService', () => { service.create(memoText).then((response: MemoText) => { expect(response).toEqual(memoText); }); - // tick(100); const req = httpTestingController.expectOne(`${environment.apiUrl}/memo-text/?fields_to_validate=`); expect(req.request.method).toEqual('POST'); req.flush(memoText); @@ -69,7 +68,6 @@ describe('MemoTextService', () => { service.update(memoText).then((response: MemoText) => { expect(response).toEqual(memoText); }); - // tick(100); const req = httpTestingController.expectOne(`${environment.apiUrl}/memo-text/${memoText.id}/?fields_to_validate=`); expect(req.request.method).toEqual('PUT'); req.flush(memoText); diff --git a/front-end/src/app/shared/services/poller.service.spec.ts b/front-end/src/app/shared/services/poller.service.spec.ts index a382270294..eb4c9a6b4c 100644 --- a/front-end/src/app/shared/services/poller.service.spec.ts +++ b/front-end/src/app/shared/services/poller.service.spec.ts @@ -59,8 +59,6 @@ describe('PollerService', () => { const req = httpMock.expectOne('test-url'); req.flush(mockHtmlResponse, { status: 200, statusText: 'OK' }); - // tick(); // Fast-forward any pending asynchronous tasks - service.isNewVersionAvailable$.subscribe((isAvailable) => { expect(isAvailable).toBe(true); }); diff --git a/front-end/src/app/shared/services/transaction-list.service.spec.ts b/front-end/src/app/shared/services/transaction-list.service.spec.ts index bd906d75d5..1efd94dd69 100644 --- a/front-end/src/app/shared/services/transaction-list.service.spec.ts +++ b/front-end/src/app/shared/services/transaction-list.service.spec.ts @@ -124,7 +124,6 @@ describe('TransactionListService', () => { service.addToReport(transaction, report).then((response) => { expect(response.ok).toBe(true); }); - // tick(100); const req = httpTestingController.expectOne(`${environment.apiUrl}/transactions/add-to-report/`); expect(req.request.method).toEqual('POST'); req.flush(transaction); diff --git a/front-end/src/app/shared/services/transaction.service.spec.ts b/front-end/src/app/shared/services/transaction.service.spec.ts index 7f8a6bb77e..7fcc11d81b 100644 --- a/front-end/src/app/shared/services/transaction.service.spec.ts +++ b/front-end/src/app/shared/services/transaction.service.spec.ts @@ -253,7 +253,6 @@ describe('TransactionService', () => { expect(req.request.method).toEqual('PUT'); req.flush(transactions.map((t) => t.id)); httpTestingController.verify(); - // tick(100); }); }); }); diff --git a/front-end/src/app/shared/utils/label.utils.spec.ts b/front-end/src/app/shared/utils/label.utils.spec.ts index 6d3305fdae..f4c88d36b0 100644 --- a/front-end/src/app/shared/utils/label.utils.spec.ts +++ b/front-end/src/app/shared/utils/label.utils.spec.ts @@ -56,7 +56,7 @@ describe('LabelUtils', () => { it('should return value for undefined term', () => { const testValue = 'testValue'; - const retval = LabelUtils.htmlHighlightTerm(testValue, undefined); + const retval = LabelUtils.htmlHighlightTerm(testValue); expect(retval).toEqual(testValue); }); diff --git a/front-end/src/test.ts b/front-end/src/test.ts index df206fe155..d4fadf6594 100644 --- a/front-end/src/test.ts +++ b/front-end/src/test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { vi } from 'vitest'; -(global as any).AbortController = class { +(globalThis as any).AbortController = class { constructor() { this.signal = new EventTarget(); } @@ -27,11 +27,11 @@ if (typeof DataTransfer === 'undefined') { }; // Attach to the Node global scope - (global as any).DataTransfer = DataTransferPolyfill; + (globalThis as any).DataTransfer = DataTransferPolyfill; // Attach to the JSDOM window scope if it exists - if (typeof window !== 'undefined') { - (window as any).DataTransfer = DataTransferPolyfill; + if (globalThis.window !== undefined) { + (globalThis.window as any).DataTransfer = DataTransferPolyfill; } } @@ -45,9 +45,9 @@ if (typeof ClipboardEvent === 'undefined') { } }; - (global as any).ClipboardEvent = ClipboardEventPolyfill; - if (typeof window !== 'undefined') { - (window as any).ClipboardEvent = ClipboardEventPolyfill; + (globalThis as any).ClipboardEvent = ClipboardEventPolyfill; + if (globalThis.window !== undefined) { + (globalThis.window as any).ClipboardEvent = ClipboardEventPolyfill; } } @@ -65,8 +65,8 @@ if (typeof HTMLDialogElement !== 'undefined' && !HTMLDialogElement.prototype.sho } // Polyfill for matchMedia which is missing in JSDOM -if (typeof window !== 'undefined' && !window.matchMedia) { - Object.defineProperty(window, 'matchMedia', { +if (globalThis.window !== undefined && !globalThis.window.matchMedia) { + Object.defineProperty(globalThis.window, 'matchMedia', { writable: true, value: vi.fn().mockImplementation((query) => ({ matches: false, @@ -81,12 +81,12 @@ if (typeof window !== 'undefined' && !window.matchMedia) { }); } -global.ResizeObserver = vi.fn().mockImplementation(() => ({ +globalThis.ResizeObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(), })); -window.scrollTo = vi.fn(); +globalThis.window.scrollTo = vi.fn(); -Object.defineProperty(URL, 'createObjectURL', { writable: true, value: vi.fn() }); +Object.defineProperty(globalThis.URL, 'createObjectURL', { writable: true, value: vi.fn() }); diff --git a/sonar-project.properties b/sonar-project.properties index ffc8388659..5d72c9bfa5 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -12,9 +12,9 @@ sonar.sources=front-end #sonar.sourceEncoding=UTF-8 # Exclude spec files from coverage -sonar.coverage.exclusions=**.spec.ts, front-end/cypress/**, front-end/src/assets/styles/, **/cypress.config.ts, front-end/cypress.a11y.ts +sonar.coverage.exclusions=**.spec.ts, front-end/cypress/**, front-end/src/assets/styles/, **/cypress.config.ts, front-end/cypress.a11y.ts, front-end/src/test.ts -sonar.javascript.lcov.reportPaths=front-end/coverage/front-end/lcov.info +sonar.javascript.lcov.reportPaths=front-end/coverage/fecfile-web/lcov.info sonar.host.url=https://sonarcloud.io From 1446bc50405530852472864342c30f725f03e036 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Tue, 24 Mar 2026 05:57:26 -0400 Subject: [PATCH 46/69] replace PageUtils.blurActiveField() with cy.blurActiveField() --- front-end/cypress/e2e-smoke/F24/f24.helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front-end/cypress/e2e-smoke/F24/f24.helpers.ts b/front-end/cypress/e2e-smoke/F24/f24.helpers.ts index 8b8fb52805..7f520d3dea 100644 --- a/front-end/cypress/e2e-smoke/F24/f24.helpers.ts +++ b/front-end/cypress/e2e-smoke/F24/f24.helpers.ts @@ -38,7 +38,7 @@ export function createIndependentExpenditureOnForm24( 'date_signed', ); if (blurBeforeSave) { - PageUtils.blurActiveField(); + cy.blurActiveField(); } TransactionDetailPage.clickSave(); cy.location('pathname').should('include', `/reports/transactions/report/${reportId}/list`); From 268e776cc027eb4e4c7bc0cff019a7721394b516 Mon Sep 17 00:00:00 2001 From: Sasha Dresden Date: Tue, 24 Mar 2026 15:40:31 -0400 Subject: [PATCH 47/69] update knip.json --- front-end/knip.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/front-end/knip.json b/front-end/knip.json index 55b30a4d3d..2ad80e0844 100644 --- a/front-end/knip.json +++ b/front-end/knip.json @@ -14,7 +14,6 @@ "@typescript-eslint/parser", "@vitest/coverage-v8" ], - "ignore": ["src/test.ts"], "workspaces": { "front-end": { "entry": [ @@ -33,13 +32,18 @@ "config": ["angular.json"] }, "cypress": { - "config": ["cypress.config.{js,ts,mjs,cjs}"], "entry": [ "front-end/cypress/e2e/**/*.cy.{js,jsx,ts,tsx}", "front-end/cypress/support/e2e.{js,jsx,ts,tsx}", "front-end/cypress/support/**/*.{js,ts}" ] }, - + "vitest": { + "entry": [ + "**/*.{bench,test,test-d,spec,spec-d}.?(c|m)[jt]s?(x)", + "**/__mocks__/**/*.[jt]s?(x)", + "src/test.{ts,tsx}" + ] + }, "tags": ["-lintignore"] } From e1ac76535d72d40a4558442ce58799e8a421e5de Mon Sep 17 00:00:00 2001 From: Max Zaremba Date: Tue, 24 Mar 2026 15:46:55 -0400 Subject: [PATCH 48/69] Removed button borders --- .../navigation-control/navigation-control.component.scss | 1 + .../components/save-cancel/save-cancel.component.html | 2 +- front-end/src/assets/styles/theme.css | 6 +++--- front-end/src/styles.scss | 2 -- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/front-end/src/app/shared/components/navigation-control/navigation-control.component.scss b/front-end/src/app/shared/components/navigation-control/navigation-control.component.scss index 9baca95882..6f8618384a 100644 --- a/front-end/src/app/shared/components/navigation-control/navigation-control.component.scss +++ b/front-end/src/app/shared/components/navigation-control/navigation-control.component.scss @@ -30,6 +30,7 @@ color: var(--p-content-color); font-weight: bold; background-color: var(--orange); + border: none; padding: 0 16px; height: 3rem; } diff --git a/front-end/src/app/shared/components/save-cancel/save-cancel.component.html b/front-end/src/app/shared/components/save-cancel/save-cancel.component.html index e0be9b9cc3..ade3ec9b7f 100644 --- a/front-end/src/app/shared/components/save-cancel/save-cancel.component.html +++ b/front-end/src/app/shared/components/save-cancel/save-cancel.component.html @@ -16,7 +16,7 @@ pButton pRipple appSingleClick - label="Save and continue" + label="Save & continue" (click)="save.emit('continue')" class="p-button-info ml-4" data-cy="save-cancel-actions" diff --git a/front-end/src/assets/styles/theme.css b/front-end/src/assets/styles/theme.css index 15676c1e01..148f82c5e4 100644 --- a/front-end/src/assets/styles/theme.css +++ b/front-end/src/assets/styles/theme.css @@ -2077,7 +2077,7 @@ p-treeselect.p-treeselect-clearable .p-treeselect-clear-icon { .p-splitbutton.p-button-info > .p-button { color: #212121; background: #35bdbb; - border: 1px solid #35bdbb; + border: none; } .p-button.p-button-info:enabled:hover, .p-buttonset.p-button-info > .p-button:enabled:hover, @@ -3394,7 +3394,7 @@ p-treeselect.p-treeselect-clearable .p-treeselect-clear-icon { .p-paginator .p-paginator-next, .p-paginator .p-paginator-last { background-color: var(--fec-grey-alt); - border: 1px solid var(--surface-border); + border: none; color: var(--p-content-color); min-width: 2.357rem; height: 2rem; @@ -3441,7 +3441,7 @@ p-treeselect.p-treeselect-clearable .p-treeselect-clear-icon { } .p-paginator .p-paginator-pages .p-paginator-page { background: var(--p-content-background); - border: 1px solid var(--surface-border); + border: none; min-width: 2.357rem; height: 2rem; margin: 0 0 0 4px; diff --git a/front-end/src/styles.scss b/front-end/src/styles.scss index e3f1b952a2..e039f04738 100644 --- a/front-end/src/styles.scss +++ b/front-end/src/styles.scss @@ -406,7 +406,6 @@ label.disabled { } .p-button-primary.p-button { - border-width: 2px; padding: 8px 20px; font-size: 14px; } @@ -419,7 +418,6 @@ label.disabled { .p-button-secondary.p-button { background-color: #aeb0b5; border-color: #aeb0b5; - border-width: 2px; padding: 8px 20px; font-size: 14px; color: #212121; From a60d3374def8d791c1fbb5f24f602b4f735ae64f Mon Sep 17 00:00:00 2001 From: David Heitzer Date: Tue, 24 Mar 2026 16:32:18 -0400 Subject: [PATCH 49/69] 2903 fix deposited/undeposted toggle button not displaying --- .../f3x/deposited-undeposited-buttons.cy.ts | 63 +++++++++++++++++++ .../F3X/utils/start-transaction/receipts.ts | 4 ++ .../inputs/memo-code/memo-code.component.html | 2 +- 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 front-end/cypress/e2e-extended/reports/f3x/deposited-undeposited-buttons.cy.ts diff --git a/front-end/cypress/e2e-extended/reports/f3x/deposited-undeposited-buttons.cy.ts b/front-end/cypress/e2e-extended/reports/f3x/deposited-undeposited-buttons.cy.ts new file mode 100644 index 0000000000..2e0f088338 --- /dev/null +++ b/front-end/cypress/e2e-extended/reports/f3x/deposited-undeposited-buttons.cy.ts @@ -0,0 +1,63 @@ +import { DataSetup } from "../../../e2e-smoke/F3X/setup"; +import { StartTransaction } from "../../../e2e-smoke/F3X/utils/start-transaction/start-transaction"; +import { formTransactionDataForSchedule } from "../../../e2e-smoke/models/TransactionFormModel"; +import { ContactLookup } from "../../../e2e-smoke/pages/contactLookup"; +import { Initialize } from "../../../e2e-smoke/pages/loginPage"; +import { currentYear, PageUtils } from "../../../e2e-smoke/pages/pageUtils"; +import { ReportListPage } from "../../../e2e-smoke/pages/reportListPage"; +import { TransactionDetailPage } from "../../../e2e-smoke/pages/transactionDetailPage"; +import { F3XAggregationHelpers } from "./f3x-aggregation.helpers"; + +describe('Receipt Transactions', () => { + beforeEach(() => { + Initialize(); + }); + + it('Create an Conduit Earmark Receipt transaction using the contact lookup', () => { + cy.wrap(DataSetup({ individual: true, committee: true, candidate: true })).then((result: any) => { + const individual = result.individual; + const committee = result.committee; + const candidate = result.candidate; + ReportListPage.gotToReportTransactionListPage(result.report); + StartTransaction.Receipts().Individual().ConduitEarmarkReceipt(); + + // Enter STEP ONE transaction + cy.get('p-accordion-panel').first().as('stepOneAccordion'); + ContactLookup.getContact(individual.last_name, '@stepOneAccordion'); + const transactionFormData = { + ...formTransactionDataForSchedule, + purpose_description: '', + category_code: '', + date_received: new Date(currentYear, 4 - 1, 27), + }; + TransactionDetailPage.enterScheduleFormData( + transactionFormData, + false, + '@stepOneAccordion', + true, + 'contribution_date', + ); + cy.get("[inputid='memo_code']").contains("Undeposited").click(); + + // Enter STEP TWO transaction + PageUtils.clickAccordion('STEP TWO'); + cy.get('p-accordion-panel').last().as('stepTwoAccordion'); + ContactLookup.getCommittee(committee, [], [], '@stepTwoAccordion'); + TransactionDetailPage.enterScheduleFormData( + transactionFormData, + true, + '@stepTwoAccordion', + true, + 'expenditure_date', + ); + ContactLookup.getCandidate(candidate, [], [], '#contact_2_lookup'); + PageUtils.selectDropdownSetValue('[inputid="electionType"]', 'G'); + F3XAggregationHelpers.clearAndType('#electionYear', `${currentYear}`); + + // Verify record created + TransactionDetailPage.clickSaveBothTransactions(); + PageUtils.urlCheck('/list'); + cy.get('tr').should('contain', 'Conduit Earmark (Undeposited)'); + }); + }); +}); diff --git a/front-end/cypress/e2e-smoke/F3X/utils/start-transaction/receipts.ts b/front-end/cypress/e2e-smoke/F3X/utils/start-transaction/receipts.ts index 878f23154c..dee284e2c7 100644 --- a/front-end/cypress/e2e-smoke/F3X/utils/start-transaction/receipts.ts +++ b/front-end/cypress/e2e-smoke/F3X/utils/start-transaction/receipts.ts @@ -34,6 +34,10 @@ export class Individual { PageUtils.clickLink(Individual.INDIVIDUAL_RECEIPT); } + static ConduitEarmarkReceipt() { + PageUtils.clickLink("Conduit Earmark Receipt"); + } + static Returned() { PageUtils.clickLink('Returned/Bounced Receipt'); } diff --git a/front-end/src/app/shared/components/inputs/memo-code/memo-code.component.html b/front-end/src/app/shared/components/inputs/memo-code/memo-code.component.html index d9f92441b8..3e254809cc 100644 --- a/front-end/src/app/shared/components/inputs/memo-code/memo-code.component.html +++ b/front-end/src/app/shared/components/inputs/memo-code/memo-code.component.html @@ -1,6 +1,6 @@
- @if (memoCodeMapOptions.length === 0) { + @if (memoCodeMapOptions().length === 0) {
Date: Tue, 24 Mar 2026 17:51:55 -0400 Subject: [PATCH 50/69] Trying remove cypress block from knip --- front-end/knip.json | 7 ------- front-end/package-lock.json | 8 ++++---- front-end/package.json | 2 +- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/front-end/knip.json b/front-end/knip.json index 2ad80e0844..214a1c713e 100644 --- a/front-end/knip.json +++ b/front-end/knip.json @@ -31,13 +31,6 @@ "angular": { "config": ["angular.json"] }, - "cypress": { - "entry": [ - "front-end/cypress/e2e/**/*.cy.{js,jsx,ts,tsx}", - "front-end/cypress/support/e2e.{js,jsx,ts,tsx}", - "front-end/cypress/support/**/*.{js,ts}" - ] - }, "vitest": { "entry": [ "**/*.{bench,test,test-d,spec,spec-d}.?(c|m)[jt]s?(x)", diff --git a/front-end/package-lock.json b/front-end/package-lock.json index 954daaf7b1..fbf091066d 100644 --- a/front-end/package-lock.json +++ b/front-end/package-lock.json @@ -15,7 +15,7 @@ "class-transformer": "0.5.1", "fecfile-validate": "https://github.com/fecgov/fecfile-validate#0322bb973333f175d45d2b4b06de470a0d5c48d7", "intl-tel-input": "26.8.1", - "knip": "^6.0.3", + "knip": "^6.0.5", "ngrx-store-localstorage": "20.1.0", "ngx-cookie-service": "21.3.1", "ngx-logger": "5.0.12", @@ -9912,9 +9912,9 @@ } }, "node_modules/knip": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/knip/-/knip-6.0.4.tgz", - "integrity": "sha512-r/9F7wcxiFM71WgDFQiToE2hQHwZ/UkGmr74o8eiNFPIg80f7rlQHVrZiRX46Tj2yE3s96wUVNGMnsDMylgInw==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/knip/-/knip-6.0.5.tgz", + "integrity": "sha512-+i9e/ZKuYlECB5iIK82NQwnYso4oNLBhzsTbXhSqCG1qfGi6D84GNtRENafmS3C0lABX8Wf3BKM434nPXi2AbQ==", "funding": [ { "type": "github", diff --git a/front-end/package.json b/front-end/package.json index 2367043a87..dced6ffa99 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -35,7 +35,7 @@ "class-transformer": "0.5.1", "fecfile-validate": "https://github.com/fecgov/fecfile-validate#0322bb973333f175d45d2b4b06de470a0d5c48d7", "intl-tel-input": "26.8.1", - "knip": "^6.0.3", + "knip": "^6.0.5", "ngrx-store-localstorage": "20.1.0", "ngx-cookie-service": "21.3.1", "ngx-logger": "5.0.12", From 87abeae9ce36756a3f979950077fbf0467ad4469 Mon Sep 17 00:00:00 2001 From: Sasha Dresden Date: Tue, 24 Mar 2026 16:34:02 -0400 Subject: [PATCH 51/69] Populate purpose of receipt with text when candidate hasn't been selected yet. --- .../additional-info-input.component.html | 1 + .../additional-info-input.component.spec.ts | 5 ++-- .../additional-info-input.component.ts | 23 ++++++++++++------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.html b/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.html index a17defe681..4d4d8404b7 100644 --- a/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.html +++ b/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.html @@ -22,6 +22,7 @@ [formControlName]="templateMap['purpose_description']" [class.readonly]="isDescriptionSystemGenerated()" [readonly]="isDescriptionSystemGenerated()" + [class.font-italic]="form.controls[templateMap['purpose_description']].value === pendingCommitteeText" > { fixture = TestBed.createComponent(TestHostComponent); host = fixture.componentInstance; component = host.component(); - - fixture.detectChanges(); }); it('should create', () => { @@ -84,6 +82,7 @@ describe('AdditionalInfoInputComponent', () => { }); it('should trigger the purposeDescriptionPrefix callbacks', () => { + fixture.detectChanges(); component.form.patchValue({ [host.templateMap.purpose_description]: 'abc', }); @@ -100,6 +99,7 @@ describe('AdditionalInfoInputComponent', () => { }); it('purpose_description of just prefix just trigger required error', () => { + fixture.detectChanges(); component.form.patchValue({ [host.templateMap.purpose_description]: 'Prefix: hihi', }); @@ -115,6 +115,7 @@ describe('AdditionalInfoInputComponent', () => { }); it('should detect memo prefixes', () => { + fixture.detectChanges(); expect(component.form.get(host.templateMap.text4000)?.value).toEqual(''); host.transaction.memo_text = MemoText.fromJSON({ text_prefix: 'MEMO PREFIX:', diff --git a/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.ts b/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.ts index 50239a07a1..e467c24eed 100644 --- a/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.ts +++ b/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.ts @@ -32,11 +32,19 @@ export class AdditionalInfoInputComponent extends BaseInputComponent implements readonly categoryCodeOptions: PrimeOptions = LabelUtils.getPrimeOptions(CategoryCodeLabels); readonly purposeDescriptionPrefix = computed(() => this.transactionType()?.purposeDescriptionPrefix); + readonly isDescriptionSystemGenerated = computed( + () => this.transactionType()?.generatePurposeDescription !== undefined, + ); + + readonly pendingCommitteeText = `This information is system-generated based on the committee details provided in step two. + +Add additional information as needed in the following Note or Memo Text box.`; ngOnInit(): void { SchemaUtils.addJsonSchemaValidators(this.form, memoTextSchema, false); this.form.updateValueAndValidity(); const purposeDescriptionPrefix = this.purposeDescriptionPrefix(); + const purposeDescriptionControl = this.form.get(this.templateMap.purpose_description); if (purposeDescriptionPrefix) { this.initPrefix(this.templateMap.purpose_description, purposeDescriptionPrefix); } @@ -50,21 +58,20 @@ export class AdditionalInfoInputComponent extends BaseInputComponent implements // If this transaction type has a purpose description prefix, add a validator to the form control // to set a required error if only the prefix is present if (purposeDescriptionPrefix) { - this.form - .get(this.templateMap.purpose_description) - ?.addValidators((control) => (control.value === purposeDescriptionPrefix ? { required: true } : null)); + purposeDescriptionControl?.addValidators((control) => + control.value === purposeDescriptionPrefix ? { required: true } : null, + ); } - } - isDescriptionSystemGenerated(): boolean { - // Description is system generated if there is a defined function. Otherwise, it's mutable - return this.transactionType()?.generatePurposeDescription !== undefined; + if (this.isDescriptionSystemGenerated() && purposeDescriptionControl?.value.length === 0) { + purposeDescriptionControl?.setValue(this.pendingCommitteeText); + } } initPrefix(field: string, prefix: string) { // Watch changes to form text field to make sure prefix is maintained (this.form.get(field) as SubscriptionFormControl)?.addSubscription((value: string) => { - if (value.length < prefix.length || value.indexOf(': ') < 0) { + if (value.length < prefix.length || !value.includes(': ')) { // Ensure prefix is the first part of the string in the textarea if no user text added this.form.get(field)?.setValue(prefix); } else if (!value.startsWith(prefix)) { From 892e9ad11dfd5a01278be1326ba08c91e9fafbed Mon Sep 17 00:00:00 2001 From: Sasha Dresden Date: Wed, 25 Mar 2026 09:58:48 -0400 Subject: [PATCH 52/69] Update angular build process --- .circleci/config.yml | 19 ++++--------------- front-end/package.json | 10 +++++----- tasks.py | 15 ++++++++++----- 3 files changed, 19 insertions(+), 25 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fdbef7e7cf..1c3849394e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -255,6 +255,10 @@ jobs: steps: - checkout + - node/install-packages: + app-dir: ~/project/front-end/ + override-ci-command: npm install + - python/install-packages: pkg-manager: pip app-dir: ~/project/ @@ -330,23 +334,8 @@ workflows: - not: << pipeline.parameters.is-triggered-unit-test >> - not: << pipeline.parameters.is-triggered-full-nightly >> jobs: - - lint - - test - - dependency-check - - e2e-smoke: - video: << pipeline.parameters.e2e-video >> # Default is false - filters: - branches: - only: /develop|release\/sprint-[\.\d]+|release\/test|main/ - deploy-job: # Deploy job even when e2e tests fail name: deploy-even-if-e2e-job-fails - requires: - - lint - - test - - dependency-check - filters: - branches: - only: /develop|release\/sprint-[\.\d]+|release\/test|main/ # This job is run when an e2e test is triggered with the # is-triggered-e2e-test parameter via the fecfile-web-api diff --git a/front-end/package.json b/front-end/package.json index dced6ffa99..6679f8c71f 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -9,11 +9,11 @@ "local_memory": "npx --node-options='--max-old-space-size=4000' -y -p @angular/cli ng serve --configuration=local", "local_aot": "npx @angular/cli ng serve --configuration=local --aot", "local_aot_memory": "npx --node-options='--max-old-space-size=4000' -y -p @angular/cli ng serve --configuration=local --aot", - "build-dev": "npm install --omit=dev && npx -y -p @angular/cli ng build --configuration=cloud.gov.dev", - "build-stage": "npm install --omit=dev && npx -y -p @angular/cli ng build --configuration=cloud.gov.stage", - "build-prod": "npm install --omit=dev && npx -y -p @angular/cli ng build --configuration=cloud.gov.prod", - "build-test": "npm install --omit=dev && npx -y -p @angular/cli ng build --configuration=cloud.gov.test", - "build-local": "npm install --omit=dev && npx -y -p @angular/cli ng build --configuration=local", + "build-dev": "npm install && npx ng build --configuration=cloud.gov.dev", + "build-stage": "npm install && npx ng build --configuration=cloud.gov.stage", + "build-prod": "npm install && npx ng build --configuration=cloud.gov.prod", + "build-test": "npm install && npx ng build --configuration=cloud.gov.test", + "build-local": "npm install && npx ng build --configuration=local", "test": "npx --node-options='--max-old-space-size=4000' -y -p @angular/cli ng test", "e2e": "npx -y -p @angular/cli ng e2e", "e2e:headless": "npm run e2e -- --headless --watch=false", diff --git a/tasks.py b/tasks.py index 8a27f9c5cc..0fb6f6ac70 100644 --- a/tasks.py +++ b/tasks.py @@ -48,21 +48,26 @@ def _detect_branch(repo): ("prod", lambda _, branch: branch == "main"), ("test", lambda _, branch: branch == "release/test"), ("stage", lambda _, branch: branch.startswith("release/sprint")), - ("dev", lambda _, branch: branch == "develop"), + ("dev", lambda _, branch: branch == "feature/2928-1"), ) def _build_angular_app(ctx, space): orig_directory = os.getcwd() - os.chdir(os.path.join(orig_directory, "front-end")) + frontend_dir = os.path.join(orig_directory, "front-end") + os.chdir(frontend_dir) + ng_bin = os.path.join(frontend_dir, "node_modules", ".bin", "ng") - print(f"Starting build: npm run build-{space}") - result = ctx.run(f"npm run build-{space}", warn=True, echo=True) + print(f"Starting build for {space} using {ng_bin}") + build_cmd = f"{ng_bin} build --configuration=cloud.gov.{space}" + result = ctx.run(build_cmd, warn=True, echo=True) if result.return_code != 0: print(f"error building Angular app. Exiting with code {result.return_code}") + os.chdir(orig_directory) exit(result.return_code) + os.chdir(orig_directory) ctx.run("npm ls --all", warn=True, echo=True) os.chdir(orig_directory) @@ -213,7 +218,7 @@ def deploy(ctx, space=None, branch=None, login=False, help=False, nobuild=False) Example usage: invoke deploy --space dev """ - ctx.run(f"cf version", echo=True) + ctx.run("cf version", echo=True) if help: _print_help_text() From e0f008ebc3f8bc65cac9437aab013d48e6635cf5 Mon Sep 17 00:00:00 2001 From: Sasha Dresden Date: Wed, 25 Mar 2026 10:21:45 -0400 Subject: [PATCH 53/69] Revert code to make this branch deploy --- .circleci/config.yml | 15 +++++++++++++++ front-end/package.json | 10 +++++----- tasks.py | 2 +- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1c3849394e..f8f65241d3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -334,8 +334,23 @@ workflows: - not: << pipeline.parameters.is-triggered-unit-test >> - not: << pipeline.parameters.is-triggered-full-nightly >> jobs: + - lint + - test + - dependency-check + - e2e-smoke: + video: << pipeline.parameters.e2e-video >> # Default is false + filters: + branches: + only: /develop|release\/sprint-[\.\d]+|release\/test|main/ - deploy-job: # Deploy job even when e2e tests fail name: deploy-even-if-e2e-job-fails + requires: + - lint + - test + - dependency-check + filters: + branches: + only: /develop|release\/sprint-[\.\d]+|release\/test|main/ # This job is run when an e2e test is triggered with the # is-triggered-e2e-test parameter via the fecfile-web-api diff --git a/front-end/package.json b/front-end/package.json index 6679f8c71f..12dbbc5496 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -9,11 +9,11 @@ "local_memory": "npx --node-options='--max-old-space-size=4000' -y -p @angular/cli ng serve --configuration=local", "local_aot": "npx @angular/cli ng serve --configuration=local --aot", "local_aot_memory": "npx --node-options='--max-old-space-size=4000' -y -p @angular/cli ng serve --configuration=local --aot", - "build-dev": "npm install && npx ng build --configuration=cloud.gov.dev", - "build-stage": "npm install && npx ng build --configuration=cloud.gov.stage", - "build-prod": "npm install && npx ng build --configuration=cloud.gov.prod", - "build-test": "npm install && npx ng build --configuration=cloud.gov.test", - "build-local": "npm install && npx ng build --configuration=local", + "build-dev": "npx ng build --configuration=cloud.gov.dev", + "build-stage": "npx ng build --configuration=cloud.gov.stage", + "build-prod": "npx ng build --configuration=cloud.gov.prod", + "build-test": "npx ng build --configuration=cloud.gov.test", + "build-local": "npx ng build --configuration=local", "test": "npx --node-options='--max-old-space-size=4000' -y -p @angular/cli ng test", "e2e": "npx -y -p @angular/cli ng e2e", "e2e:headless": "npm run e2e -- --headless --watch=false", diff --git a/tasks.py b/tasks.py index 0fb6f6ac70..1affd15e46 100644 --- a/tasks.py +++ b/tasks.py @@ -48,7 +48,7 @@ def _detect_branch(repo): ("prod", lambda _, branch: branch == "main"), ("test", lambda _, branch: branch == "release/test"), ("stage", lambda _, branch: branch.startswith("release/sprint")), - ("dev", lambda _, branch: branch == "feature/2928-1"), + ("dev", lambda _, branch: branch == "develop"), ) From e1dcd12ac3c34532dbf0ea0e00c59fd7037c31f0 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Wed, 25 Mar 2026 12:49:12 -0400 Subject: [PATCH 54/69] cypress 15.13.0 --- front-end/package-lock.json | 14 +++++++------- front-end/package.json | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/front-end/package-lock.json b/front-end/package-lock.json index fbf091066d..f0697c3482 100644 --- a/front-end/package-lock.json +++ b/front-end/package-lock.json @@ -49,7 +49,7 @@ "@typescript-eslint/parser": "8.57.2", "@vitest/coverage-v8": "^4.1.1", "axe-core": "^4.11.1", - "cypress": "^15.10.0", + "cypress": "^15.13.0", "cypress-axe": "^1.7.0", "cypress-mochawesome-reporter": "4.0.2", "eslint": "10.1.0", @@ -7597,9 +7597,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.322", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.322.tgz", - "integrity": "sha512-vFU34OcrvMcH66T+dYC3G4nURmgfDVewMIu6Q2urXpumAPSMmzvcn04KVVV8Opikq8Vs5nUbO/8laNhNRqSzYw==", + "version": "1.5.325", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.325.tgz", + "integrity": "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==", "dev": true, "license": "ISC" }, @@ -9769,9 +9769,9 @@ } }, "node_modules/jsdom/node_modules/undici": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz", - "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.6.tgz", + "integrity": "sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==", "dev": true, "license": "MIT", "engines": { diff --git a/front-end/package.json b/front-end/package.json index 12dbbc5496..8d73dfb3b9 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -67,8 +67,9 @@ "@types/node": "^25.5.0", "@typescript-eslint/eslint-plugin": "8.57.2", "@typescript-eslint/parser": "8.57.2", + "@vitest/coverage-v8": "^4.1.1", "axe-core": "^4.11.1", - "cypress": "^15.10.0", + "cypress": "^15.13.0", "cypress-axe": "^1.7.0", "cypress-mochawesome-reporter": "4.0.2", "eslint": "10.1.0", @@ -78,7 +79,6 @@ "rxjs": "7.8.2", "tslib": "2.8.1", "vitest": "^4.1.0", - "@vitest/coverage-v8": "^4.1.1", "zone.js": "0.16.1" }, "overrides": { From 7d2a4307048ac6039ad2bdc97ae0dffaa8debb08 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Wed, 25 Mar 2026 12:50:04 -0400 Subject: [PATCH 55/69] no carat --- front-end/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front-end/package.json b/front-end/package.json index 8d73dfb3b9..dd6490379c 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -69,7 +69,7 @@ "@typescript-eslint/parser": "8.57.2", "@vitest/coverage-v8": "^4.1.1", "axe-core": "^4.11.1", - "cypress": "^15.13.0", + "cypress": "15.13.0", "cypress-axe": "^1.7.0", "cypress-mochawesome-reporter": "4.0.2", "eslint": "10.1.0", From b3b15a65bf85644f921118d012618dec8f14aa15 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Wed, 25 Mar 2026 12:51:28 -0400 Subject: [PATCH 56/69] visibility --- front-end/cypress/docs/unriddling.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 front-end/cypress/docs/unriddling.md diff --git a/front-end/cypress/docs/unriddling.md b/front-end/cypress/docs/unriddling.md new file mode 100644 index 0000000000..dcd0f12314 --- /dev/null +++ b/front-end/cypress/docs/unriddling.md @@ -0,0 +1 @@ +placeholder to make PR visible \ No newline at end of file From 6fb78caed85257faef1bf20c4aa2a4ebe1b59cde Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Wed, 25 Mar 2026 12:57:00 -0400 Subject: [PATCH 57/69] clean install --- front-end/package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front-end/package-lock.json b/front-end/package-lock.json index f0697c3482..84dd2e7c3a 100644 --- a/front-end/package-lock.json +++ b/front-end/package-lock.json @@ -49,7 +49,7 @@ "@typescript-eslint/parser": "8.57.2", "@vitest/coverage-v8": "^4.1.1", "axe-core": "^4.11.1", - "cypress": "^15.13.0", + "cypress": "15.13.0", "cypress-axe": "^1.7.0", "cypress-mochawesome-reporter": "4.0.2", "eslint": "10.1.0", From c383c9a3fa4bd79b06a9389294dc64b40082e2d5 Mon Sep 17 00:00:00 2001 From: Max Zaremba Date: Wed, 25 Mar 2026 13:55:25 -0400 Subject: [PATCH 58/69] PR comment updates --- front-end/src/assets/styles/theme.css | 20 -------------------- front-end/src/styles.scss | 1 - 2 files changed, 21 deletions(-) diff --git a/front-end/src/assets/styles/theme.css b/front-end/src/assets/styles/theme.css index 148f82c5e4..acdca7816a 100644 --- a/front-end/src/assets/styles/theme.css +++ b/front-end/src/assets/styles/theme.css @@ -9,11 +9,6 @@ font-weight: normal; } - -.p-button:disabled { - border: 2px solid gray; -} - .p-disabled, .p-component:disabled, .p-select:disabled { @@ -1859,7 +1854,6 @@ p-treeselect.p-treeselect-clearable .p-treeselect-clear-icon { .p-button:enabled:active { background: var(--primary-button-hover-color); color: #ffffff; - border-color: var(--primary-button-hover-color);; } .p-button.p-button-outlined { background-color: transparent; @@ -2007,14 +2001,12 @@ p-treeselect.p-treeselect-clearable .p-treeselect-clear-icon { .p-splitbutton.p-button-secondary > .p-button { color: #ffffff; background: #5b616b; - border: 1px solid #5b616b; } .p-button.p-button-secondary:enabled:hover, .p-buttonset.p-button-secondary > .p-button:enabled:hover, .p-splitbutton.p-button-secondary > .p-button:enabled:hover { background: #494e56; color: #ffffff; - border-color: #494e56; } .p-button.p-button-secondary:enabled:focus, @@ -2027,7 +2019,6 @@ p-treeselect.p-treeselect-clearable .p-treeselect-clear-icon { .p-splitbutton.p-button-secondary > .p-button:enabled:active { background: #5b616b; color: #ffffff; - border-color: #5b616b; } .p-button.p-button-secondary.p-button-outlined, .p-buttonset.p-button-secondary > .p-button.p-button-outlined, @@ -2084,7 +2075,6 @@ p-treeselect.p-treeselect-clearable .p-treeselect-clear-icon { .p-splitbutton.p-button-info > .p-button:enabled:hover { background: #2b9896; color: #212121; - border-color: #2b9896; } .p-button.p-button-info:enabled:focus, .p-buttonset.p-button-info > .p-button:enabled:focus, @@ -2096,7 +2086,6 @@ p-treeselect.p-treeselect-clearable .p-treeselect-clear-icon { .p-splitbutton.p-button-info > .p-button:enabled:active { background: #138496; color: #212121; - border-color: #117a8b; } .p-button.p-button-info.p-button-outlined, .p-buttonset.p-button-info > .p-button.p-button-outlined, @@ -2215,14 +2204,12 @@ p-treeselect.p-treeselect-clearable .p-treeselect-clear-icon { .p-splitbutton.p-button-warning > .p-button { color: var(--p-content-color); background: #fdb838; - border: 1px solid #fdb838; } .p-button.p-button-warning:enabled:hover, .p-buttonset.p-button-warning > .p-button:enabled:hover, .p-splitbutton.p-button-warning > .p-button:enabled:hover { background: #cc942d; color: var(--p-content-color); - border-color: #cc942d; } .p-button.p-button-warning:enabled:focus, .p-buttonset.p-button-warning > .p-button:enabled:focus, @@ -2234,48 +2221,41 @@ p-treeselect.p-treeselect-clearable .p-treeselect-clear-icon { .p-splitbutton.p-button-warning > .p-button:enabled:active { background: #d39e00; color: var(--p-content-color); - border-color: #c69500; } .p-button.p-button-warning.p-button-outlined, .p-buttonset.p-button-warning > .p-button.p-button-outlined, .p-splitbutton.p-button-warning > .p-button.p-button-outlined { background-color: transparent; color: #fdb838; - border: 1px solid; } .p-button.p-button-warning.p-button-outlined:enabled:hover, .p-buttonset.p-button-warning > .p-button.p-button-outlined:enabled:hover, .p-splitbutton.p-button-warning > .p-button.p-button-outlined:enabled:hover { background: rgba(255, 193, 7, 0.04); color: #fdb838; - border: 1px solid; } .p-button.p-button-warning.p-button-outlined:enabled:active, .p-buttonset.p-button-warning > .p-button.p-button-outlined:enabled:active, .p-splitbutton.p-button-warning > .p-button.p-button-outlined:enabled:active { background: rgba(255, 193, 7, 0.16); color: #fdb838; - border: 1px solid; } .p-button.p-button-warning.p-button-text, .p-buttonset.p-button-warning > .p-button.p-button-text, .p-splitbutton.p-button-warning > .p-button.p-button-text { background-color: transparent; color: #fdb838; - border-color: transparent; } .p-button.p-button-warning.p-button-text:enabled:hover, .p-buttonset.p-button-warning > .p-button.p-button-text:enabled:hover, .p-splitbutton.p-button-warning > .p-button.p-button-text:enabled:hover { background: rgba(255, 193, 7, 0.04); - border-color: transparent; color: #fdb838; } .p-button.p-button-warning.p-button-text:enabled:active, .p-buttonset.p-button-warning > .p-button.p-button-text:enabled:active, .p-splitbutton.p-button-warning > .p-button.p-button-text:enabled:active { background: rgba(255, 193, 7, 0.16); - border-color: transparent; color: #fdb838; } diff --git a/front-end/src/styles.scss b/front-end/src/styles.scss index e039f04738..8b80ec8eff 100644 --- a/front-end/src/styles.scss +++ b/front-end/src/styles.scss @@ -412,7 +412,6 @@ label.disabled { .p-button-primary.p-button:enabled:hover { background-color: var(--primary-button-hover-color); - border-color: var(--primary-button-hover-color); } .p-button-secondary.p-button { From ac1e3c2266e8e1cae32aaf62d9342145c677f49d Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Wed, 25 Mar 2026 14:11:25 -0400 Subject: [PATCH 59/69] null-path date, beforeEach timeouts; unit tests --- .../committee-info.component.spec.ts | 8 ++++---- .../contact-dialog.component.spec.ts | 2 +- .../additional-info-input.component.spec.ts | 16 ++++------------ .../navigation-control.component.spec.ts | 1 - .../transaction-type-base.component.spec.ts | 11 +++-------- .../app/shared/services/login.service.spec.ts | 17 +++++++++++++---- 6 files changed, 25 insertions(+), 30 deletions(-) diff --git a/front-end/src/app/committee/committee-info/committee-info.component.spec.ts b/front-end/src/app/committee/committee-info/committee-info.component.spec.ts index b30224958d..272223d561 100644 --- a/front-end/src/app/committee/committee-info/committee-info.component.spec.ts +++ b/front-end/src/app/committee/committee-info/committee-info.component.spec.ts @@ -77,18 +77,18 @@ describe('CommitteeInfoComponent', () => { const link = 'https://webforms.stage.gov/webforms/form1/index.htm'; await setEnvironment(link); - vi.spyOn(window, 'open'); + const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null); const f1FormLink = fixture.debugElement.nativeElement.querySelector('#update-form-1-link'); f1FormLink.click(); - expect(window.open).toHaveBeenCalledWith(link, '_blank', 'noopener'); + expect(windowOpenSpy).toHaveBeenCalledWith(link, '_blank', 'noopener'); }); it('should use the staging link in non-production environment', async () => { const link = 'https://webforms.stage.efo.fec.gov/webforms/form1/index.htm'; await setEnvironment(link); - vi.spyOn(window, 'open'); + const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null); const f1FormLink = fixture.debugElement.nativeElement.querySelector('#update-form-1-link'); f1FormLink.click(); - expect(window.open).toHaveBeenCalledWith(link, '_blank', 'noopener'); + expect(windowOpenSpy).toHaveBeenCalledWith(link, '_blank', 'noopener'); }); }); diff --git a/front-end/src/app/shared/components/contact-dialog/contact-dialog.component.spec.ts b/front-end/src/app/shared/components/contact-dialog/contact-dialog.component.spec.ts index 843aabb745..016ed21142 100644 --- a/front-end/src/app/shared/components/contact-dialog/contact-dialog.component.spec.ts +++ b/front-end/src/app/shared/components/contact-dialog/contact-dialog.component.spec.ts @@ -119,7 +119,7 @@ describe('ContactDialogComponent', () => { describe('transactions', () => { it('should route to transaction', async () => { - const spy = vi.spyOn(component.router, 'navigate'); + const spy = vi.spyOn(component.router, 'navigate').mockResolvedValue(true); const testTransactionListRecord = createTestTransactionListRecord(); testTransactionListRecord.report_ids = ['abc']; await component.openTransaction(testTransactionListRecord); diff --git a/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts b/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts index 8afb897a91..3c68a2028b 100644 --- a/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts +++ b/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts @@ -42,18 +42,10 @@ class TestHostComponent { this.transaction.transactionType.purposeDescriptionPrefix = 'Prefix: '; } - updateFormWithQuaternaryContact(event: SelectItem) { - console.log(event); - } - clearFormQuaternaryContact() { - console.log('clear quaternary contact'); - } - updateFormWithQuinaryContact(event: SelectItem) { - console.log(event); - } - clearFormQuinaryContact() { - console.log('clear quinary contact'); - } + updateFormWithQuaternaryContact(_event: SelectItem) {} + clearFormQuaternaryContact() {} + updateFormWithQuinaryContact(_event: SelectItem) {} + clearFormQuinaryContact() {} } describe('AdditionalInfoInputComponent', () => { diff --git a/front-end/src/app/shared/components/navigation-control/navigation-control.component.spec.ts b/front-end/src/app/shared/components/navigation-control/navigation-control.component.spec.ts index d34ee87ae1..082d568b4e 100644 --- a/front-end/src/app/shared/components/navigation-control/navigation-control.component.spec.ts +++ b/front-end/src/app/shared/components/navigation-control/navigation-control.component.spec.ts @@ -124,7 +124,6 @@ describe('NavigationControlComponent', () => { // spy on event emitter const storeSpy = vi.spyOn(store, 'dispatch'); - console.log(component.dropdownOptions); component.onDropdownChange(component.dropdownOptions()[0]); // simulate selecting the first dropdown option fixture.detectChanges(); diff --git a/front-end/src/app/shared/components/transaction-type-base/transaction-type-base.component.spec.ts b/front-end/src/app/shared/components/transaction-type-base/transaction-type-base.component.spec.ts index 1091c86122..d773c3f03b 100644 --- a/front-end/src/app/shared/components/transaction-type-base/transaction-type-base.component.spec.ts +++ b/front-end/src/app/shared/components/transaction-type-base/transaction-type-base.component.spec.ts @@ -102,12 +102,7 @@ describe('TransactionTypeBaseComponent', () => { { provide: MessageService, useValue: { - add: vi - .fn() - .mockName('MessageService.add') - .mockReturnValue((message: { severity: string; summary: string; detail: string; life: number }) => { - console.log(message.summary); - }), + add: vi.fn().mockName('MessageService.add').mockReturnValue(() => undefined), }, }, FormBuilder, @@ -187,11 +182,11 @@ describe('TransactionTypeBaseComponent', () => { }); it('should not trigger effect if NavigationEvent has no transaction', async () => { - const handleNavSpy = vi.spyOn(component, 'handleNavigate'); + const handleNavSpy = vi.spyOn(component, 'handleNavigate').mockResolvedValue(); const navEvent = new NavigationEvent(NavigationAction.SAVE, NavigationDestination.LIST); store.dispatch(navigationEventSetAction(navEvent)); fixture.detectChanges(); - await fixture.whenStable(); + await Promise.resolve(); expect(handleNavSpy).toHaveBeenCalledTimes(0); }); diff --git a/front-end/src/app/shared/services/login.service.spec.ts b/front-end/src/app/shared/services/login.service.spec.ts index 495fe8d4cb..464ea5a776 100644 --- a/front-end/src/app/shared/services/login.service.spec.ts +++ b/front-end/src/app/shared/services/login.service.spec.ts @@ -10,6 +10,7 @@ import { selectUserLoginData } from 'app/store/user-login-data.selectors'; import { LoginService } from './login.service'; import { provideHttpClient } from '@angular/common/http'; import { SECURITY_CONSENT_VERSION } from 'app/login/security-notice/security-notice.component'; +import { environment } from 'environments/environment'; describe('LoginService', () => { let service: LoginService; @@ -42,10 +43,18 @@ describe('LoginService', () => { }); it('#logOut login.gov happy path', () => { - vi.spyOn(store, 'dispatch'); - - service.logOut(); - expect(store.dispatch).toHaveBeenCalledWith(userLoginDataDiscardedAction()); + const dispatchSpy = vi.spyOn(store, 'dispatch'); + const originalLogoutUrl = environment.loginDotGovLogoutUrl; + environment.loginDotGovLogoutUrl = window.location.href; + const userIsAuthenticatedSpy = vi.spyOn(service, 'userIsAuthenticated').mockReturnValue(true); + + try { + service.logOut(); + expect(dispatchSpy).toHaveBeenCalledWith(userLoginDataDiscardedAction()); + expect(userIsAuthenticatedSpy).toHaveBeenCalled(); + } finally { + environment.loginDotGovLogoutUrl = originalLogoutUrl; + } }); it('userHasProfileData should return true', () => { From 8f7a2ec7962ebf9d002d1ca30f5b10c3a7177420 Mon Sep 17 00:00:00 2001 From: Max Zaremba Date: Wed, 25 Mar 2026 14:24:41 -0400 Subject: [PATCH 60/69] Made p-button border zero --- front-end/src/assets/styles/theme.css | 1 + 1 file changed, 1 insertion(+) diff --git a/front-end/src/assets/styles/theme.css b/front-end/src/assets/styles/theme.css index acdca7816a..10489b80f1 100644 --- a/front-end/src/assets/styles/theme.css +++ b/front-end/src/assets/styles/theme.css @@ -1849,6 +1849,7 @@ p-treeselect.p-treeselect-clearable .p-treeselect-clear-icon { border-color 0.15s, box-shadow 0.15s; border-radius: var(--border-radius); + border: 0; } .p-button:enabled:hover, .p-button:enabled:active { From 194babc6fc7a03686f3be09dc57cd7e7c1d1b7e5 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Wed, 25 Mar 2026 14:29:32 -0400 Subject: [PATCH 61/69] unit tests and FE bug fix for the (previous commit) aformentioned issues --- .../additional-info-input.component.spec.ts | 16 ++++++++++++---- .../amount-input/amount-input.component.ts | 2 +- .../memo-code/memo-code.component.spec.ts | 8 ++++++++ .../inputs/memo-code/memo-code.component.ts | 17 ++++++++++++----- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts b/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts index 3c68a2028b..671f47ce00 100644 --- a/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts +++ b/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts @@ -42,10 +42,18 @@ class TestHostComponent { this.transaction.transactionType.purposeDescriptionPrefix = 'Prefix: '; } - updateFormWithQuaternaryContact(_event: SelectItem) {} - clearFormQuaternaryContact() {} - updateFormWithQuinaryContact(_event: SelectItem) {} - clearFormQuinaryContact() {} + updateFormWithQuaternaryContact(_event: SelectItem): void { + return; + } + clearFormQuaternaryContact(): void { + return; + } + updateFormWithQuinaryContact(_event: SelectItem): void { + return; + } + clearFormQuinaryContact(): void { + return; + } } describe('AdditionalInfoInputComponent', () => { diff --git a/front-end/src/app/shared/components/inputs/amount-input/amount-input.component.ts b/front-end/src/app/shared/components/inputs/amount-input/amount-input.component.ts index b20fb586b1..30fdbbfa50 100644 --- a/front-end/src/app/shared/components/inputs/amount-input/amount-input.component.ts +++ b/front-end/src/app/shared/components/inputs/amount-input/amount-input.component.ts @@ -96,7 +96,7 @@ export class AmountInputComponent extends BaseInputComponent implements OnInit { ); // Opening of 'Just checking...' pop-up is handled in app-memo-code component directly. }, this.destroy$); - date2Control.addSubscription((date: Date) => { + date2Control.addSubscription((date: Date | null) => { dateControl.updateValueAndValidity({ emitEvent: false }); // Only show the 'Just checking...' pop-up if there is no date in the 'date' field. if (!dateControl.value) { diff --git a/front-end/src/app/shared/components/inputs/memo-code/memo-code.component.spec.ts b/front-end/src/app/shared/components/inputs/memo-code/memo-code.component.spec.ts index 71db007eae..e8271704be 100644 --- a/front-end/src/app/shared/components/inputs/memo-code/memo-code.component.spec.ts +++ b/front-end/src/app/shared/components/inputs/memo-code/memo-code.component.spec.ts @@ -180,6 +180,14 @@ describe('MemoCodeInputComponent', () => { expect(component.dateIsOutsideReport).toBe(true); }); + it('should not crash when the memo coverage date is temporarily cleared', () => { + setForm3X(); + component.coverageDate.set(null); + + expect(() => component.form.get('contribution_date')?.patchValue(new Date('12/25/2019'))).not.toThrow(); + expect(component.dateIsOutsideReport).toBe(true); + }); + it('should update transaction type identifiers correctly based on the TransactionType', () => { host.transaction = getTestTransactionByType(ScheduleATransactionTypes.CONDUIT_EARMARK_RECEIPT); fixture.detectChanges(); diff --git a/front-end/src/app/shared/components/inputs/memo-code/memo-code.component.ts b/front-end/src/app/shared/components/inputs/memo-code/memo-code.component.ts index 438aefcee6..e1989617b3 100644 --- a/front-end/src/app/shared/components/inputs/memo-code/memo-code.component.ts +++ b/front-end/src/app/shared/components/inputs/memo-code/memo-code.component.ts @@ -44,7 +44,7 @@ export class MemoCodeInputComponent extends BaseInputComponent implements OnInit : 'The dollar amount in a memo item is not incorporated into the total figures for the schedule.', ); readonly memoCodeReadOnly = computed(() => TransactionFormUtils.isMemoCodeReadOnly(this.transactionType())); - readonly coverageDate = signal(new Date()); + readonly coverageDate = signal(null); readonly coverageDateQuestion = signal( 'Did you mean to date this transaction outside of the report coverage period?', ); @@ -77,8 +77,8 @@ export class MemoCodeInputComponent extends BaseInputComponent implements OnInit ngOnInit(): void { const dateControl = this.form.get(this.templateMap.date) as SubscriptionFormControl; if (dateControl?.enabled) { - dateControl.addSubscription((date: Date) => { - if (date && date.getTime() !== this.coverageDate().getTime()) { + dateControl.addSubscription((date: Date | null) => { + if (date?.getTime() !== this.coverageDate()?.getTime()) { this.coverageDate.set(date); this.updateMemoItemWithDate(date); } @@ -120,11 +120,18 @@ export class MemoCodeInputComponent extends BaseInputComponent implements OnInit } } - updateMemoItemWithDate(date: Date) { + updateMemoItemWithDate(date: Date | null | undefined) { const coverageFromDate = this.coverageFromDate(); const coverageThrough = this.coverageThroughDate(); if (this.transactionType()?.doMemoCodeDateCheck && coverageFromDate && coverageThrough) { - if (date && (date < coverageFromDate || date > coverageThrough)) { + if (!date) { + if (this.dateIsOutsideReport && this.memoControl.hasValidator(Validators.requiredTrue)) { + this.memoControl.removeValidators([Validators.requiredTrue]); + this.memoControl.markAsTouched(); + this.memoControl.updateValueAndValidity(); + } + this.dateIsOutsideReport = false; + } else if (date < coverageFromDate || date > coverageThrough) { this.memoControl.addValidators(Validators.requiredTrue); this.memoControl.markAsTouched(); this.memoControl.markAsDirty(); From 75f18d8738f7128b17941129808df68e89c1ea64 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Wed, 25 Mar 2026 14:39:38 -0400 Subject: [PATCH 62/69] brassic lint --- .../additional-info-input.component.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts b/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts index 06aa841f7c..431e876824 100644 --- a/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts +++ b/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts @@ -42,14 +42,14 @@ class TestHostComponent { this.transaction.transactionType.purposeDescriptionPrefix = 'Prefix: '; } - updateFormWithQuaternaryContact(_event: SelectItem): void { - return; + updateFormWithQuaternaryContact(event: SelectItem): void { + void event; } clearFormQuaternaryContact(): void { return; } - updateFormWithQuinaryContact(_event: SelectItem): void { - return; + updateFormWithQuinaryContact(event: SelectItem): void { + void event; } clearFormQuinaryContact(): void { return; From 0f51e7fcbfc1dd08c2eb2570ed65c44a64db6491 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Wed, 25 Mar 2026 14:43:32 -0400 Subject: [PATCH 63/69] brassic lint --- .../transaction-type-base.component.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/front-end/src/app/shared/components/transaction-type-base/transaction-type-base.component.spec.ts b/front-end/src/app/shared/components/transaction-type-base/transaction-type-base.component.spec.ts index d773c3f03b..c5b6e0e28f 100644 --- a/front-end/src/app/shared/components/transaction-type-base/transaction-type-base.component.spec.ts +++ b/front-end/src/app/shared/components/transaction-type-base/transaction-type-base.component.spec.ts @@ -102,7 +102,10 @@ describe('TransactionTypeBaseComponent', () => { { provide: MessageService, useValue: { - add: vi.fn().mockName('MessageService.add').mockReturnValue(() => undefined), + add: vi + .fn() + .mockName('MessageService.add') + .mockReturnValue(() => undefined), }, }, FormBuilder, From 8be0ace9b4ff2e077e1ec328de2b2abaee4658cf Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Wed, 25 Mar 2026 15:03:23 -0400 Subject: [PATCH 64/69] brassic lint --- .../additional-info-input.component.spec.ts | 6 +- .../inputs/memo-code/memo-code.component.ts | 63 +++++++++++-------- .../app/shared/services/login.service.spec.ts | 2 +- 3 files changed, 43 insertions(+), 28 deletions(-) diff --git a/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts b/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts index 431e876824..cc274762f6 100644 --- a/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts +++ b/front-end/src/app/shared/components/inputs/additional-info-input/additional-info-input.component.spec.ts @@ -37,19 +37,21 @@ class TestHostComponent { formSubmitted = false; templateMap = testTemplateMap(); transaction: Transaction = testScheduleATransaction(); + designatingCommitteeSelection?: SelectItem; + subordinateCommitteeSelection?: SelectItem; component = viewChild.required(AdditionalInfoInputComponent); constructor() { this.transaction.transactionType.purposeDescriptionPrefix = 'Prefix: '; } updateFormWithQuaternaryContact(event: SelectItem): void { - void event; + this.designatingCommitteeSelection = event; } clearFormQuaternaryContact(): void { return; } updateFormWithQuinaryContact(event: SelectItem): void { - void event; + this.subordinateCommitteeSelection = event; } clearFormQuinaryContact(): void { return; diff --git a/front-end/src/app/shared/components/inputs/memo-code/memo-code.component.ts b/front-end/src/app/shared/components/inputs/memo-code/memo-code.component.ts index e1989617b3..5674c1943e 100644 --- a/front-end/src/app/shared/components/inputs/memo-code/memo-code.component.ts +++ b/front-end/src/app/shared/components/inputs/memo-code/memo-code.component.ts @@ -120,34 +120,47 @@ export class MemoCodeInputComponent extends BaseInputComponent implements OnInit } } + private clearOutOfDateRequirement(): void { + if (this.dateIsOutsideReport && this.memoControl.hasValidator(Validators.requiredTrue)) { + this.memoControl.removeValidators([Validators.requiredTrue]); + this.memoControl.markAsTouched(); + this.memoControl.updateValueAndValidity(); + } + this.dateIsOutsideReport = false; + } + + private setOutOfDateRequirement(): void { + this.memoControl.addValidators(Validators.requiredTrue); + this.memoControl.markAsTouched(); + this.memoControl.markAsDirty(); + this.memoControl.updateValueAndValidity(); + this.dateIsOutsideReport = true; + if (!this.memoControl.value) { + this.outOfDateDialogVisible.set(true); + } + } + + private isMemoDateWithinCoverage(date: Date, coverageFromDate: Date, coverageThroughDate: Date): boolean { + return date >= coverageFromDate && date <= coverageThroughDate; + } + updateMemoItemWithDate(date: Date | null | undefined) { const coverageFromDate = this.coverageFromDate(); const coverageThrough = this.coverageThroughDate(); - if (this.transactionType()?.doMemoCodeDateCheck && coverageFromDate && coverageThrough) { - if (!date) { - if (this.dateIsOutsideReport && this.memoControl.hasValidator(Validators.requiredTrue)) { - this.memoControl.removeValidators([Validators.requiredTrue]); - this.memoControl.markAsTouched(); - this.memoControl.updateValueAndValidity(); - } - this.dateIsOutsideReport = false; - } else if (date < coverageFromDate || date > coverageThrough) { - this.memoControl.addValidators(Validators.requiredTrue); - this.memoControl.markAsTouched(); - this.memoControl.markAsDirty(); - this.memoControl.updateValueAndValidity(); - this.dateIsOutsideReport = true; - if (!this.memoControl.value) { - this.outOfDateDialogVisible.set(true); - } - } else { - if (this.dateIsOutsideReport && this.memoControl.hasValidator(Validators.requiredTrue)) { - this.memoControl.removeValidators([Validators.requiredTrue]); - this.memoControl.markAsTouched(); - this.memoControl.updateValueAndValidity(); - } - this.dateIsOutsideReport = false; - } + if (!this.transactionType()?.doMemoCodeDateCheck || !coverageFromDate || !coverageThrough) { + return; + } + + if (!date) { + this.clearOutOfDateRequirement(); + return; + } + + if (this.isMemoDateWithinCoverage(date, coverageFromDate, coverageThrough)) { + this.clearOutOfDateRequirement(); + return; } + + this.setOutOfDateRequirement(); } } diff --git a/front-end/src/app/shared/services/login.service.spec.ts b/front-end/src/app/shared/services/login.service.spec.ts index 464ea5a776..bd7cff917d 100644 --- a/front-end/src/app/shared/services/login.service.spec.ts +++ b/front-end/src/app/shared/services/login.service.spec.ts @@ -45,7 +45,7 @@ describe('LoginService', () => { it('#logOut login.gov happy path', () => { const dispatchSpy = vi.spyOn(store, 'dispatch'); const originalLogoutUrl = environment.loginDotGovLogoutUrl; - environment.loginDotGovLogoutUrl = window.location.href; + environment.loginDotGovLogoutUrl = globalThis.location.href; const userIsAuthenticatedSpy = vi.spyOn(service, 'userIsAuthenticated').mockReturnValue(true); try { From d2eb0b4c492acbb4e8e206c01873e84a8fd9c7b6 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Wed, 25 Mar 2026 16:14:39 -0400 Subject: [PATCH 65/69] boom snap clap --- .../e2e-smoke/F1M/f1m-affiliation.cy.ts | 4 ++-- .../cypress/e2e-smoke/F3X/disbursements.cy.ts | 7 ++++++ .../cypress/e2e-smoke/F3X/loans-bank.cy.ts | 20 ++++++++-------- .../cypress/e2e-smoke/F3X/receipts.cy.ts | 2 +- .../cypress/e2e-smoke/F3X/reports-f3x.cy.ts | 2 +- .../cypress/e2e-smoke/pages/pageUtils.ts | 24 +++++++++++++++++-- .../cypress/e2e-smoke/pages/reportListPage.ts | 3 +-- .../e2e-smoke/pages/transactionDetailPage.ts | 6 ++--- 8 files changed, 47 insertions(+), 21 deletions(-) diff --git a/front-end/cypress/e2e-smoke/F1M/f1m-affiliation.cy.ts b/front-end/cypress/e2e-smoke/F1M/f1m-affiliation.cy.ts index e3fa2d86ed..f7630a2189 100644 --- a/front-end/cypress/e2e-smoke/F1M/f1m-affiliation.cy.ts +++ b/front-end/cypress/e2e-smoke/F1M/f1m-affiliation.cy.ts @@ -102,7 +102,7 @@ describe('Manage reports', () => { excludeIds.push(candidates[index].id!); } - PageUtils.clickButton('Save and continue', '[data-cy="save-cancel-actions"]:visible'); + PageUtils.clickFormActionButton('Save & continue', '[data-cy="save-cancel-actions"]:visible'); cy.get('[data-cy="print-preview"]').should('be.visible'); PageUtils.clickSidebarSection('SIGN & SUBMIT'); @@ -112,7 +112,7 @@ describe('Manage reports', () => { PageUtils.clickSidebarItem('Add a report level memo'); const memoText = faker.lorem.sentence({ min: 1, max: 4 }); ReportLevelMemoPage.enterFormData(memoText); - PageUtils.clickButton('Save & continue', '[data-cy="report-level-memo-actions"]:visible'); + PageUtils.clickFormActionButton('Save & continue', '[data-cy="report-level-memo-actions"]:visible'); // Verify we've landed on the submit report page before checking // back on the memo page cy.get('#submit-report-container').should('exist'); diff --git a/front-end/cypress/e2e-smoke/F3X/disbursements.cy.ts b/front-end/cypress/e2e-smoke/F3X/disbursements.cy.ts index 6a93b2d64d..24126d71a7 100644 --- a/front-end/cypress/e2e-smoke/F3X/disbursements.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/disbursements.cy.ts @@ -23,6 +23,11 @@ const independentExpVoidData: DisbursementFormData = { signatoryLastName: faker.person.lastName(), }; +function checkLocationCheck() { + PageUtils.locationCheck('/list'); + cy.contains('Transactions in this report').should('be.visible'); +} + describe('Disbursements', () => { beforeEach(() => { Initialize(); @@ -60,6 +65,7 @@ describe('Disbursements', () => { ); TransactionDetailPage.clickSave(); + checkLocationCheck(); PageUtils.clickLink('Independent Expenditure - Void'); cy.contains('Address').should('exist'); cy.get('#organization_name').should('have.value', result.organization.name); @@ -81,6 +87,7 @@ describe('Disbursements', () => { 'date_signed', ); TransactionDetailPage.clickSave(); + checkLocationCheck(); PageUtils.closeToast(); // Check that fields saved correctly diff --git a/front-end/cypress/e2e-smoke/F3X/loans-bank.cy.ts b/front-end/cypress/e2e-smoke/F3X/loans-bank.cy.ts index b54602ed20..139bcdcc80 100644 --- a/front-end/cypress/e2e-smoke/F3X/loans-bank.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/loans-bank.cy.ts @@ -118,15 +118,14 @@ function handleLoanAgreementSetup(q3: string) { TransactionDetailPage.enterNewLoanAgreementFormData(fd); - cy.intercept({ - method: 'POST', - pathname: '/api/v1/transactions/', - }).as('saveNewAgreement'); + cy.intercept('POST', '**/api/v1/transactions/**').as('saveNewAgreement'); TransactionDetailPage.clickSave(); - cy.wait('@saveNewAgreement'); - cy.contains('Loan Received from Bank').should('exist'); - PageUtils.urlCheck('/list'); + cy.wait('@saveNewAgreement').then((interception) => { + expect(interception.response?.statusCode).to.equal(200); + }); + PageUtils.locationCheck('/list'); + cy.contains('Loan Received from Bank').should('be.visible'); clickLoan('Review loan agreement'); PageUtils.valueCheck('input[id^="loan-agreement-amount-"]', '$65,000.00'); PageUtils.valueCheck('#loan_incurred_date', `05/27/${currentYear}`); @@ -159,8 +158,9 @@ describe('Loans', () => { PageUtils.clickAccordion('STEP TWO'); TransactionDetailPage.enterLoanFormDataStepTwo(defaultLoanFormData); - PageUtils.clickButton('Save transactions', '[data-cy="navigation-control-button"]:visible'); - PageUtils.urlCheck('/list'); + PageUtils.clickFormActionButton('Save transactions', '[data-cy="navigation-control-button"]:visible'); + PageUtils.locationCheck('/list'); + cy.contains('Transactions in this report').should('be.visible'); cy.contains('Loan Received from Bank').should('exist'); assertNoDeleteButtonInLoanReceivedFromBankRow(); @@ -176,7 +176,7 @@ describe('Loans', () => { PageUtils.enterValue('#amount', formData.amount); TransactionDetailPage.clickSave(); PageUtils.urlCheck('/list'); - cy.contains('Loan Repayment Made').should('exist'); + cy.contains('Loan Repayment Made').should('be.visible'); }); }); diff --git a/front-end/cypress/e2e-smoke/F3X/receipts.cy.ts b/front-end/cypress/e2e-smoke/F3X/receipts.cy.ts index 396298f6fc..d5c0fb9871 100644 --- a/front-end/cypress/e2e-smoke/F3X/receipts.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/receipts.cy.ts @@ -68,7 +68,7 @@ describe('Receipt Transactions', () => { // Check for regression on date error cy.get('#contribution_date').clear(); TransactionDetailPage.clickSave(); // Triggers errors to show - cy.get('app-calendar').should('exist').should('contain', 'This is a required field.'); + cy.get('[data-cy="contribution_date-error"]').should('contain', 'This is a required field.'); }); }); diff --git a/front-end/cypress/e2e-smoke/F3X/reports-f3x.cy.ts b/front-end/cypress/e2e-smoke/F3X/reports-f3x.cy.ts index a6cbdf5328..a158361e2a 100644 --- a/front-end/cypress/e2e-smoke/F3X/reports-f3x.cy.ts +++ b/front-end/cypress/e2e-smoke/F3X/reports-f3x.cy.ts @@ -108,7 +108,7 @@ describe('Manage reports', () => { PageUtils.clickSidebarItem('Add a report level memo'); const memoText = faker.lorem.sentence({ min: 1, max: 4 }); ReportLevelMemoPage.enterFormData(memoText); - PageUtils.clickButton('Save & continue', '[data-cy="report-level-memo-actions"]:visible'); + PageUtils.clickFormActionButton('Save & continue', '[data-cy="report-level-memo-actions"]:visible'); // Verify it is still there when we go back to the page PageUtils.clickSidebarSection('REVIEW A REPORT'); diff --git a/front-end/cypress/e2e-smoke/pages/pageUtils.ts b/front-end/cypress/e2e-smoke/pages/pageUtils.ts index 863d636ce8..bb86d59c40 100644 --- a/front-end/cypress/e2e-smoke/pages/pageUtils.ts +++ b/front-end/cypress/e2e-smoke/pages/pageUtils.ts @@ -9,6 +9,14 @@ export class PageUtils { return value.replaceAll(/\s+/g, ' ').trim(); } + private static canonicalizeButtonLabel(value: string): string { + return PageUtils.normalizeButtonLabel(value) + .replaceAll('&', ' and ') + .replaceAll(/\s+/g, ' ') + .trim() + .toLowerCase(); + } + private static normalizeSidebarLabel(value: string): string { return value.replaceAll(/\s+/g, ' ').trim().toLowerCase(); } @@ -271,7 +279,7 @@ export class PageUtils { static clickButton(name: string, alias = '', force = false) { alias = PageUtils.getAlias(alias); - const normalizedName = PageUtils.normalizeButtonLabel(name); + const normalizedName = PageUtils.canonicalizeButtonLabel(name); const resolveLabel = (button: HTMLElement): string => { const sources = [ @@ -281,7 +289,7 @@ export class PageUtils { button.textContent ?? '', ]; const matchingLabel = sources.find((value) => PageUtils.normalizeButtonLabel(value).length > 0) ?? ''; - return PageUtils.normalizeButtonLabel(matchingLabel); + return PageUtils.canonicalizeButtonLabel(matchingLabel); }; cy.get(alias).then(($root) => { @@ -305,6 +313,18 @@ export class PageUtils { }); } + static clickFormActionButton(name: string, alias = '', force = false) { + return cy + .get('body') + .then(($body) => { + if ($body.find('.p-datepicker-panel:visible').length > 0) { + cy.get('body').type('{esc}'); + } + }) + .blurActiveField() + .then(() => PageUtils.clickButton(name, alias, force)); + } + static dateToString(date: Date) { return ( (date.getMonth() > 8 ? date.getMonth() + 1 : '0' + (date.getMonth() + 1)) + diff --git a/front-end/cypress/e2e-smoke/pages/reportListPage.ts b/front-end/cypress/e2e-smoke/pages/reportListPage.ts index 9332a8c92c..d859c0f1e9 100644 --- a/front-end/cypress/e2e-smoke/pages/reportListPage.ts +++ b/front-end/cypress/e2e-smoke/pages/reportListPage.ts @@ -94,8 +94,7 @@ export class ReportListPage { ReportListPage.clickCreateAndSelectForm('F3X'); F3xCreateReportPage.waitForCoverage(); F3xCreateReportPage.enterFormData(fd); - PageUtils.clickButton('Save and continue', '[data-cy="save-cancel-actions"]:visible'); - + PageUtils.clickFormActionButton('Save & continue', '[data-cy="save-cancel-actions"]:visible'); } static createF1M() { diff --git a/front-end/cypress/e2e-smoke/pages/transactionDetailPage.ts b/front-end/cypress/e2e-smoke/pages/transactionDetailPage.ts index 7bb21899c4..446027edff 100644 --- a/front-end/cypress/e2e-smoke/pages/transactionDetailPage.ts +++ b/front-end/cypress/e2e-smoke/pages/transactionDetailPage.ts @@ -282,15 +282,15 @@ export class TransactionDetailPage { static clickSave(buttonType = this.SPLIT_BUTTON) { - PageUtils.clickButton('Save', `app-navigation-control-bar,[data-cy="${buttonType}"]:visible`); + PageUtils.clickFormActionButton('Save', `app-navigation-control-bar,[data-cy="${buttonType}"]:visible`); } static clickInlineSave() { - PageUtils.clickButton('Save', `[data-cy="${this.BUTTON}"]:visible`); + PageUtils.clickFormActionButton('Save', `[data-cy="${this.BUTTON}"]:visible`); } static clickSaveBothTransactions() { - PageUtils.clickButton('Save both transactions', `[data-cy="${this.BUTTON}"]:visible`); + PageUtils.clickFormActionButton('Save both transactions', `[data-cy="${this.BUTTON}"]:visible`); } static clickCancel() { From ec72c3071644c40dc198cc5341f947174641ce55 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Wed, 25 Mar 2026 17:10:58 -0400 Subject: [PATCH 66/69] removing placeholder doc --- front-end/cypress/docs/unriddling.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 front-end/cypress/docs/unriddling.md diff --git a/front-end/cypress/docs/unriddling.md b/front-end/cypress/docs/unriddling.md deleted file mode 100644 index dcd0f12314..0000000000 --- a/front-end/cypress/docs/unriddling.md +++ /dev/null @@ -1 +0,0 @@ -placeholder to make PR visible \ No newline at end of file From eb5f028048f140c8a8f83936866aed4a88f2b541 Mon Sep 17 00:00:00 2001 From: sVmsepi0l Date: Wed, 25 Mar 2026 17:15:20 -0400 Subject: [PATCH 67/69] typo --- front-end/cypress/e2e-smoke/pages/pageUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/front-end/cypress/e2e-smoke/pages/pageUtils.ts b/front-end/cypress/e2e-smoke/pages/pageUtils.ts index bb86d59c40..83d3dfc5eb 100644 --- a/front-end/cypress/e2e-smoke/pages/pageUtils.ts +++ b/front-end/cypress/e2e-smoke/pages/pageUtils.ts @@ -9,7 +9,7 @@ export class PageUtils { return value.replaceAll(/\s+/g, ' ').trim(); } - private static canonicalizeButtonLabel(value: string): string { + private static canonizeButtonLabel(value: string): string { return PageUtils.normalizeButtonLabel(value) .replaceAll('&', ' and ') .replaceAll(/\s+/g, ' ') @@ -279,7 +279,7 @@ export class PageUtils { static clickButton(name: string, alias = '', force = false) { alias = PageUtils.getAlias(alias); - const normalizedName = PageUtils.canonicalizeButtonLabel(name); + const normalizedName = PageUtils.canonizeButtonLabel(name); const resolveLabel = (button: HTMLElement): string => { const sources = [ @@ -289,7 +289,7 @@ export class PageUtils { button.textContent ?? '', ]; const matchingLabel = sources.find((value) => PageUtils.normalizeButtonLabel(value).length > 0) ?? ''; - return PageUtils.canonicalizeButtonLabel(matchingLabel); + return PageUtils.canonizeButtonLabel(matchingLabel); }; cy.get(alias).then(($root) => { From bfdc786bc958a6940ab7501896c89496c71b8215 Mon Sep 17 00:00:00 2001 From: Sasha Dresden Date: Fri, 27 Mar 2026 10:07:48 -0400 Subject: [PATCH 68/69] Fix height of security container --- .../app/login/security-notice/security-notice.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front-end/src/app/login/security-notice/security-notice.component.scss b/front-end/src/app/login/security-notice/security-notice.component.scss index b4489c8b18..8c05208480 100644 --- a/front-end/src/app/login/security-notice/security-notice.component.scss +++ b/front-end/src/app/login/security-notice/security-notice.component.scss @@ -52,7 +52,7 @@ border-radius: 8px; width: 80vw; margin: auto; - height: 902px; + height: 600px; display: flex; flex-direction: column; } From bf6aaa53b9e3ff82a9d6100f55f0d6e21f432101 Mon Sep 17 00:00:00 2001 From: Sasha Dresden Date: Fri, 27 Mar 2026 12:41:32 -0400 Subject: [PATCH 69/69] Convert unit tests to vitest --- .../shared/components/dialog/dialog.component.spec.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/front-end/src/app/shared/components/dialog/dialog.component.spec.ts b/front-end/src/app/shared/components/dialog/dialog.component.spec.ts index 4a24e2db4a..d038562550 100644 --- a/front-end/src/app/shared/components/dialog/dialog.component.spec.ts +++ b/front-end/src/app/shared/components/dialog/dialog.component.spec.ts @@ -33,29 +33,28 @@ describe('DialogComponent', () => { host.closeOnEscape.set(true); host.visible.set(true); fixture.detectChanges(); - - const rejectSpy = jasmine.createSpy('rejectSpy'); + const rejectSpy = vi.spyOn(component.reject, 'emit'); component.reject.subscribe(rejectSpy); const event = new KeyboardEvent('keydown', { key: 'Escape' }); - spyOn(event, 'preventDefault'); + vi.spyOn(event, 'preventDefault'); component.handleEscape(event); expect(event.preventDefault).not.toHaveBeenCalled(); expect(rejectSpy).toHaveBeenCalled(); - expect(component.visible()).toBeFalse(); + expect(component.visible()).toBeFalsy(); }); it('should prevent default when closeOnEscape is false', () => { host.closeOnEscape.set(false); fixture.detectChanges(); - const rejectSpy = jasmine.createSpy('rejectSpy'); + const rejectSpy = vi.spyOn(component.reject, 'emit'); component.reject.subscribe(rejectSpy); const event = new KeyboardEvent('keydown', { key: 'Escape' }); - spyOn(event, 'preventDefault'); + vi.spyOn(event, 'preventDefault'); component.handleEscape(event);