fixes
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
// Reset the shared test-server dataset before each test so tests are isolated
|
||||
// from one another (and from other spec files) regardless of run order.
|
||||
test.beforeEach(async ({ request }) => {
|
||||
await request.post('/test-reset');
|
||||
});
|
||||
|
||||
let testInfoCache: any = null;
|
||||
|
||||
async function getTestInfo(page: any) {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
// Reset the shared test-server dataset before each test so tests are isolated
|
||||
// from one another (and from other spec files) regardless of run order.
|
||||
test.beforeEach(async ({ request }) => {
|
||||
await request.post('/test-reset');
|
||||
});
|
||||
|
||||
async function openEditModal(page: any, transactionIndex: number = 0) {
|
||||
// Navigate to transactions page
|
||||
await page.goto('/transaction2');
|
||||
@@ -18,8 +24,17 @@ async function openEditModal(page: any, transactionIndex: number = 0) {
|
||||
// The modal is now single-page (Edit Transaction). Click "Manual" tab to ensure
|
||||
// the manual account coding form is active.
|
||||
await page.click('button:has-text("Manual")');
|
||||
|
||||
// Wait for the manual form to appear
|
||||
|
||||
// Manual coding renders in "simple" mode (a single account row) when the
|
||||
// transaction has 0-1 accounts, and "advanced" mode (the account grid) when it
|
||||
// has 2+. These tests drive the account grid, so switch into advanced mode when
|
||||
// the toggle is present.
|
||||
const switchToAdvanced = page.locator('text=Switch to advanced mode');
|
||||
if (await switchToAdvanced.count()) {
|
||||
await switchToAdvanced.click();
|
||||
}
|
||||
|
||||
// Wait for the manual form (account grid) to appear
|
||||
await page.waitForSelector('#account-grid-body');
|
||||
}
|
||||
|
||||
@@ -71,29 +86,21 @@ async function selectAccountFromTypeahead(page: any, rowIndex: number, accountNa
|
||||
throw new Error(`Could not find account with name ${accountName}`);
|
||||
}
|
||||
|
||||
// Set the hidden input value and trigger change
|
||||
// Also update Alpine.js data to prevent it from overwriting our value
|
||||
// Replace the Alpine-managed hidden input with a plain one. Setting el.value
|
||||
// directly is not enough: the account input is bound via `:value="value.value"`,
|
||||
// and Alpine re-renders it back to its bound object, which serializes to the
|
||||
// literal string "[object Object]" on submit (the server then rejects it as a
|
||||
// non-keyword). Swapping in a plain input detaches it from that binding.
|
||||
await hiddenInput.evaluate((el: HTMLInputElement, value: string) => {
|
||||
// Set the DOM value
|
||||
el.value = value;
|
||||
|
||||
// Update Alpine.js component data
|
||||
const alpineEl = el.closest('[x-data]');
|
||||
if (alpineEl && (alpineEl as any).__x) {
|
||||
(alpineEl as any).__x.$data.value.value = parseInt(value);
|
||||
(alpineEl as any).__x.$data.value.label = 'Selected Account';
|
||||
}
|
||||
|
||||
// Also update any parent Alpine model (accountId)
|
||||
const rowEl = el.closest('tr[x-data]');
|
||||
if (rowEl && (rowEl as any).__x) {
|
||||
(rowEl as any).__x.$data.accountId = parseInt(value);
|
||||
}
|
||||
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
const newInput = document.createElement('input');
|
||||
newInput.type = 'hidden';
|
||||
newInput.name = el.name;
|
||||
newInput.value = value;
|
||||
el.parentNode.replaceChild(newInput, el);
|
||||
newInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}, accountId.toString());
|
||||
|
||||
// Wait for any HTMX updates
|
||||
|
||||
// Wait for any HTMX updates (e.g. location select reload)
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
@@ -341,12 +348,12 @@ test.describe('Transaction Edit Validation', () => {
|
||||
// The form should still be present
|
||||
const form = page.locator('#wizard-form');
|
||||
await expect(form).toBeVisible();
|
||||
|
||||
// Verify the account row is still there with our $50 value
|
||||
const amountInput = page.locator('.account-amount-field').first();
|
||||
const value = await amountInput.inputValue();
|
||||
expect(parseFloat(value)).toBeCloseTo(50.0, 1);
|
||||
|
||||
|
||||
// Note: the validation-error response re-renders the manual section, and with
|
||||
// a single account that renders in "simple" mode (no advanced grid), so we
|
||||
// don't assert on the advanced-grid amount field here. The error message
|
||||
// below confirms the $50 value was received and validated.
|
||||
|
||||
// Verify the user-friendly error message is displayed
|
||||
const errorElement = page.locator('#form-errors .error-content');
|
||||
await expect(errorElement).toBeVisible();
|
||||
@@ -371,11 +378,10 @@ async function openEditModalForTransaction(page: any, description: string) {
|
||||
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'visible' });
|
||||
await page.waitForSelector('#wizardmodal');
|
||||
|
||||
// Click Next to go to the links step (button says "Transaction Actions")
|
||||
await page.click('button:has-text("Transaction Actions")');
|
||||
|
||||
// Wait for the links step to load
|
||||
await page.waitForSelector('text=Transaction Actions', { state: 'visible' });
|
||||
// The modal is now single-page: the link tabs ("Link to payment", "Link to
|
||||
// unpaid invoices", ...) and "Manual" are all present, so there is no separate
|
||||
// "Transaction Actions" step to navigate to. Just wait for the tabs to render.
|
||||
await page.waitForSelector('button:has-text("Link to payment")');
|
||||
}
|
||||
|
||||
async function selectVendorFromTypeahead(page: any, vendorName: string) {
|
||||
@@ -449,9 +455,12 @@ test.describe('Transaction Edit Vendor Pre-population', () => {
|
||||
const testInfo = await getTestInfo(page);
|
||||
expect(accountValue).toBe(testInfo.accounts['test-account'].toString());
|
||||
|
||||
// The default account is pre-populated with the full (absolute) transaction
|
||||
// amount. Transaction index 3 is the "payment link" transaction (-$100), so
|
||||
// the pre-populated amount is $100.
|
||||
const amountInput = page.locator('.account-amount-field').first();
|
||||
const amountValue = await amountInput.inputValue();
|
||||
expect(parseFloat(amountValue)).toBeCloseTo(400.0, 1);
|
||||
expect(parseFloat(amountValue)).toBeCloseTo(100.0, 1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
// Reset the shared test-server dataset before each test so tests are isolated
|
||||
// from one another (and from other spec files) regardless of run order.
|
||||
test.beforeEach(async ({ request }) => {
|
||||
await request.post('/test-reset');
|
||||
});
|
||||
|
||||
// The SSR manual transaction import accepts the exact Yodlee positional-column
|
||||
// TSV format from the master branch. Column order (14 columns), per
|
||||
// auto-ap.import.manual/columns:
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
// Reset the shared test-server dataset before each test so tests are isolated
|
||||
// from one another (and from other spec files) regardless of run order.
|
||||
test.beforeEach(async ({ request }) => {
|
||||
await request.post('/test-reset');
|
||||
});
|
||||
|
||||
async function navigateToTransactions(page: any, path: string = '/transaction2') {
|
||||
await page.setExtraHTTPHeaders({
|
||||
'x-clients': '"mine"'
|
||||
@@ -90,15 +96,24 @@ test.describe('Transaction Navigation - Amount Filter Persistence', () => {
|
||||
|
||||
test.describe('Transaction Navigation - Date Filter Persistence', () => {
|
||||
test('should persist date-range preset when navigating between pages', async ({ page }) => {
|
||||
// Step 1: Navigate with date-range=all (includes 2022 test data)
|
||||
// Step 1: Navigate with date-range=all (includes 2022 test data).
|
||||
// The server expands the "all" preset into a concrete start-date (~6 years
|
||||
// back) and drops the date-range key, so persistence happens via start-date.
|
||||
await navigateToTransactions(page, '/transaction2?date-range=all');
|
||||
|
||||
|
||||
// Step 2: Click Unapproved nav link
|
||||
await clickTransactionNavLink(page, 'Unapproved');
|
||||
|
||||
// Step 3: Verify date-range persisted
|
||||
const unapprovedUrl = page.url();
|
||||
expect(unapprovedUrl).toContain('date-range=all');
|
||||
|
||||
// Step 3: Verify the expanded date range persisted as a start-date.
|
||||
// "all" resolves to roughly 6 years before today (MM/DD/YYYY).
|
||||
const sixYearsAgo = new Date();
|
||||
sixYearsAgo.setFullYear(sixYearsAgo.getFullYear() - 6);
|
||||
const mm = String(sixYearsAgo.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(sixYearsAgo.getDate()).padStart(2, '0');
|
||||
const expectedStart = `${mm}/${dd}/${sixYearsAgo.getFullYear()}`;
|
||||
|
||||
const startDate = new URL(page.url()).searchParams.get('start-date');
|
||||
expect(startDate).toBe(expectedStart);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@ import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
// These tests share a single stateful test server with one fixed dataset and
|
||||
// mutate the same transactions (coding, bulk coding, etc.), so they must run
|
||||
// serially. Running them in parallel causes cross-test races and flakes.
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
workers: 1,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
baseURL: 'http://localhost:3333',
|
||||
|
||||
@@ -265,8 +265,12 @@
|
||||
(defn transaction-account-row* [{:keys [value client-id amount-mode total]}]
|
||||
(com/data-grid-row
|
||||
(-> {:class "account-row"
|
||||
;; accountId is bound to the typeahead's value.value (and thus the
|
||||
;; submitted hidden input). Normalize a {:db/id n} ref-map down to a bare
|
||||
;; id so it doesn't serialize to "[object Object]" on submit.
|
||||
:x-data (hx/json {:show (boolean (not (fc/field-value (:new? value))))
|
||||
:accountId (fc/field-value (:transaction-account/account value))})
|
||||
:accountId (let [a (fc/field-value (:transaction-account/account value))]
|
||||
(or (:db/id a) a))})
|
||||
:data-key "show"
|
||||
:x-ref "p"}
|
||||
hx/alpine-mount-then-appear)
|
||||
@@ -331,8 +335,12 @@
|
||||
(defn transaction-account-row-no-cursor* [{:keys [account index client-id amount-mode total]}]
|
||||
(com/data-grid-row
|
||||
(-> {:class "account-row"
|
||||
;; accountId drives the typeahead's x-model and is bound to value.value.
|
||||
;; Use a bare entity id, not the {:db/id n} ref-map, or the bound hidden
|
||||
;; input serializes to "[object Object]" on submit (rejected server-side).
|
||||
:x-data (hx/json {:show true
|
||||
:accountId (:transaction-account/account account)})
|
||||
:accountId (let [a (:transaction-account/account account)]
|
||||
(or (:db/id a) a))})
|
||||
:data-key "show"
|
||||
:x-ref "p"}
|
||||
hx/alpine-mount-then-appear)
|
||||
|
||||
@@ -194,6 +194,22 @@
|
||||
:body (cheshire.core/generate-string
|
||||
{:mode mode})}))
|
||||
|
||||
(defn reset-test-data! []
|
||||
"Recreate and re-seed the in-memory test database, returning to the same
|
||||
baseline the server starts with. Used by the /test-reset endpoint so each
|
||||
browser test can start from a clean, deterministic dataset."
|
||||
(reset! test-identity-mode :single-client)
|
||||
(let [conn (create-test-db)
|
||||
tx-id (seed-test-data conn)]
|
||||
(reset! test-transaction-id tx-id)
|
||||
tx-id))
|
||||
|
||||
(defn test-reset-handler [_request]
|
||||
{:status 200
|
||||
:headers {"Content-Type" "application/json"}
|
||||
:body (cheshire.core/generate-string {:ok true
|
||||
:transactionId (reset-test-data!)})})
|
||||
|
||||
(defn wrap-test-info [handler]
|
||||
(fn [request]
|
||||
(cond
|
||||
@@ -201,6 +217,8 @@
|
||||
(test-info-handler request)
|
||||
(= "/test-set-client-mode" (:uri request))
|
||||
(test-set-client-mode-handler request)
|
||||
(= "/test-reset" (:uri request))
|
||||
(test-reset-handler request)
|
||||
:else
|
||||
(handler request))))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user