Files
marketplaces/e2e/currency-switch.spec.ts

125 lines
5.2 KiB
TypeScript
Raw Permalink Normal View History

feat: FX-quote-backed currency conversion, delete admin rate editor F10-F12 of the frontend backlog. Contract: PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §3. Removed the failure mode §5 of that contract exists to close: rates were typed once by an admin into Settings, persisted to localStorage, seeded from a hardcoded DEFAULT_RATES table (USD: 0.011, AMD: 4.3) that never updated and drifted from market. Nothing recorded which rate produced a displayed price or when. - currency-rates.service.ts now fetches through FX_QUOTE_GATEWAY instead of reading admin-typed/localStorage numbers. Stays synchronous at the call site (getRate/convert) - rewriting every consuming template to `| async` is a separate, larger change (F13, not this commit). Before a quote has loaded for a pair, getRate returns 1 rather than a fabricated market rate; isRateReady() lets a caller that cares distinguish the two. ensureFreshQuote() added for checkout to await before charging, per contract §3.2's stale-quote policy. - language.service.ts setCurrency() now triggers a quote fetch instead of just flipping the display signal. - cart.component.ts openPaymentPopup() awaits ensureFreshQuote() before computing the charged amount. - admin-settings-page.* currency-rate editor deleted (F11) - card, component state, and the three orphaned i18n keys it was the only consumer of. Two real bugs surfaced fixing this, neither cosmetic: 1. fx-quote-local.gateway.ts had CurrencyRatesService.convert() as its rate source. That is now circular - CurrencyRatesService depends on FX_QUOTE_GATEWAY, and under useMockData:true this gateway IS FX_QUOTE_GATEWAY. Would have recursed the moment mock FX data was exercised. Fixed by giving the local gateway its own static mock table - the correct home for those numbers now: explicitly labelled dev/mock data, only wired in behind useMockData, never presented as a live rate. 2. currency-convert.pipe.ts memoized its result on (amount, from, to) alone. That was already latently wrong - rates could change via the old setRate() without the pipe re-evaluating for an already-rendered price - but never surfaced because rates never changed mid-session in practice. Async quote loading made it concrete and reproducible: a price rendered before its quote arrived stayed wrong forever, because none of the three cached inputs ever changed again on their own. Fixed with a ratesVersion counter on the service, bumped on every quote arrival, included in the pipe's cache key. Both found and fixed via the E2E suite (docs from the prior commit) actually exercising the real code path: GET /api/v2/pricing/fx-quote intercepted with a contract-shaped response rather than flipping the whole app into mock mode, so the test runs the real FxQuoteApiGateway, not a stand-in for it. Verified: 3/3 E2E green, 115/115 unit tests green, arch:check clean, production build succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:03:02 +04:00
import { Page, Route, expect, test } from '@playwright/test';
feat: stand up E2E harness, fix a real bootstrap bug it found Track Q Q1/Q4 (docs/PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md). No E2E existed before this. Playwright chosen - no existing test runner preference, and it needs zero extra infra beyond the dev server this repo already has. - playwright.config.ts, package.json e2e/e2e:ui/e2e:report scripts - e2e/smoke.spec.ts app boots, no console errors (network 404s from the absent backend are filtered - expected, not a bug) - e2e/currency-switch.spec.ts Track Q Q4: switching currency must change the displayed price VALUE, not just the label next to it. Written specifically so the upcoming checkout money-truth rewrite (frontend backlog F10-F16, which replaces client-side FX math with a server-computed total) has a regression net under it before that rewrite starts. The first run found a real, current bug: @marketplaces/auth ships plain tsc output (dist/index.js), not Angular Package Format, so it carries no compiled Ivy DI metadata. Any class-based provider from it - not just the Ed25519 Noop stub, AuthService itself hit the same failure - forces Angular to JIT-compile at runtime, which throws immediately when @angular/compiler isn't loaded. That breaks app bootstrap outright, for real users, not just this test. Fixed here with the minimum honest scope: - src/main.ts: import '@angular/compiler' before bootstrap, so JIT works everywhere the package is injected, not just at one call site - src/app/app.config.ts: useFactory instead of useClass for the Noop Ed25519 provider, since it has zero constructor deps and doesn't need Angular to derive metadata for it at all - angular.json: raised the initial-bundle hard-error budget 1.5MB -> 1.8MB, because the compiler import made a correct build refuse to complete. A build that fails outright is worse than a bundle that's honestly larger than it should be. The real fix belongs in the vitanovaPackages auth repo: publish via ng-packagr so consumers get Ivy-compiled output and none of this is necessary. Do not remove the compiler import until that ships - see the comment left in main.ts. Also fixed a genuine test defect while getting this to a real green: the page renders duplicate .currency-option elements (desktop/mobile variants of the same selector), so the first attempt at this test clicked into a hidden duplicate and silently no-opped. Scoped the click to .currency-dropdown.open and added an explicit poll for the DOM to reflect the new currency before reading it back, rather than trusting a fixed timeout. Verified: 3/3 E2E green, 115/115 unit tests green, arch:check clean, production build succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:46:43 +04:00
/**
* Track Q Q4: currency switch must recalculate by FX quote. Explicitly,
* "160 RUB" must not become "160 USD" - the number has to change, not just
* the label next to it.
*
* Written before the checkout money-truth rewrite (frontend backlog F10-F16,
* which deletes CurrencyRatesService's client-side float math and switches
* checkout to a server-computed total per
* docs/backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §5). This test exists so
* that rewrite has something to break loudly if it silently stops converting.
feat: FX-quote-backed currency conversion, delete admin rate editor F10-F12 of the frontend backlog. Contract: PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §3. Removed the failure mode §5 of that contract exists to close: rates were typed once by an admin into Settings, persisted to localStorage, seeded from a hardcoded DEFAULT_RATES table (USD: 0.011, AMD: 4.3) that never updated and drifted from market. Nothing recorded which rate produced a displayed price or when. - currency-rates.service.ts now fetches through FX_QUOTE_GATEWAY instead of reading admin-typed/localStorage numbers. Stays synchronous at the call site (getRate/convert) - rewriting every consuming template to `| async` is a separate, larger change (F13, not this commit). Before a quote has loaded for a pair, getRate returns 1 rather than a fabricated market rate; isRateReady() lets a caller that cares distinguish the two. ensureFreshQuote() added for checkout to await before charging, per contract §3.2's stale-quote policy. - language.service.ts setCurrency() now triggers a quote fetch instead of just flipping the display signal. - cart.component.ts openPaymentPopup() awaits ensureFreshQuote() before computing the charged amount. - admin-settings-page.* currency-rate editor deleted (F11) - card, component state, and the three orphaned i18n keys it was the only consumer of. Two real bugs surfaced fixing this, neither cosmetic: 1. fx-quote-local.gateway.ts had CurrencyRatesService.convert() as its rate source. That is now circular - CurrencyRatesService depends on FX_QUOTE_GATEWAY, and under useMockData:true this gateway IS FX_QUOTE_GATEWAY. Would have recursed the moment mock FX data was exercised. Fixed by giving the local gateway its own static mock table - the correct home for those numbers now: explicitly labelled dev/mock data, only wired in behind useMockData, never presented as a live rate. 2. currency-convert.pipe.ts memoized its result on (amount, from, to) alone. That was already latently wrong - rates could change via the old setRate() without the pipe re-evaluating for an already-rendered price - but never surfaced because rates never changed mid-session in practice. Async quote loading made it concrete and reproducible: a price rendered before its quote arrived stayed wrong forever, because none of the three cached inputs ever changed again on their own. Fixed with a ratesVersion counter on the service, bumped on every quote arrival, included in the pipe's cache key. Both found and fixed via the E2E suite (docs from the prior commit) actually exercising the real code path: GET /api/v2/pricing/fx-quote intercepted with a contract-shaped response rather than flipping the whole app into mock mode, so the test runs the real FxQuoteApiGateway, not a stand-in for it. Verified: 3/3 E2E green, 115/115 unit tests green, arch:check clean, production build succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:03:02 +04:00
*
* This session has no live backend to run against, so GET
* /api/v2/pricing/fx-quote is intercepted with a response shaped exactly per
* contract §3.1. That exercises the REAL code path - FxQuoteApiGateway,
* CurrencyRatesService, the currencyConvert pipe - rather than switching the
* whole app into mock mode, which would test a different (mock) gateway
* instead of the one actually shipped.
feat: stand up E2E harness, fix a real bootstrap bug it found Track Q Q1/Q4 (docs/PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md). No E2E existed before this. Playwright chosen - no existing test runner preference, and it needs zero extra infra beyond the dev server this repo already has. - playwright.config.ts, package.json e2e/e2e:ui/e2e:report scripts - e2e/smoke.spec.ts app boots, no console errors (network 404s from the absent backend are filtered - expected, not a bug) - e2e/currency-switch.spec.ts Track Q Q4: switching currency must change the displayed price VALUE, not just the label next to it. Written specifically so the upcoming checkout money-truth rewrite (frontend backlog F10-F16, which replaces client-side FX math with a server-computed total) has a regression net under it before that rewrite starts. The first run found a real, current bug: @marketplaces/auth ships plain tsc output (dist/index.js), not Angular Package Format, so it carries no compiled Ivy DI metadata. Any class-based provider from it - not just the Ed25519 Noop stub, AuthService itself hit the same failure - forces Angular to JIT-compile at runtime, which throws immediately when @angular/compiler isn't loaded. That breaks app bootstrap outright, for real users, not just this test. Fixed here with the minimum honest scope: - src/main.ts: import '@angular/compiler' before bootstrap, so JIT works everywhere the package is injected, not just at one call site - src/app/app.config.ts: useFactory instead of useClass for the Noop Ed25519 provider, since it has zero constructor deps and doesn't need Angular to derive metadata for it at all - angular.json: raised the initial-bundle hard-error budget 1.5MB -> 1.8MB, because the compiler import made a correct build refuse to complete. A build that fails outright is worse than a bundle that's honestly larger than it should be. The real fix belongs in the vitanovaPackages auth repo: publish via ng-packagr so consumers get Ivy-compiled output and none of this is necessary. Do not remove the compiler import until that ships - see the comment left in main.ts. Also fixed a genuine test defect while getting this to a real green: the page renders duplicate .currency-option elements (desktop/mobile variants of the same selector), so the first attempt at this test clicked into a hidden duplicate and silently no-opped. Scoped the click to .currency-dropdown.open and added an explicit poll for the DOM to reflect the new currency before reading it back, rather than trusting a fixed timeout. Verified: 3/3 E2E green, 115/115 unit tests green, arch:check clean, production build succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:46:43 +04:00
*/
feat: FX-quote-backed currency conversion, delete admin rate editor F10-F12 of the frontend backlog. Contract: PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §3. Removed the failure mode §5 of that contract exists to close: rates were typed once by an admin into Settings, persisted to localStorage, seeded from a hardcoded DEFAULT_RATES table (USD: 0.011, AMD: 4.3) that never updated and drifted from market. Nothing recorded which rate produced a displayed price or when. - currency-rates.service.ts now fetches through FX_QUOTE_GATEWAY instead of reading admin-typed/localStorage numbers. Stays synchronous at the call site (getRate/convert) - rewriting every consuming template to `| async` is a separate, larger change (F13, not this commit). Before a quote has loaded for a pair, getRate returns 1 rather than a fabricated market rate; isRateReady() lets a caller that cares distinguish the two. ensureFreshQuote() added for checkout to await before charging, per contract §3.2's stale-quote policy. - language.service.ts setCurrency() now triggers a quote fetch instead of just flipping the display signal. - cart.component.ts openPaymentPopup() awaits ensureFreshQuote() before computing the charged amount. - admin-settings-page.* currency-rate editor deleted (F11) - card, component state, and the three orphaned i18n keys it was the only consumer of. Two real bugs surfaced fixing this, neither cosmetic: 1. fx-quote-local.gateway.ts had CurrencyRatesService.convert() as its rate source. That is now circular - CurrencyRatesService depends on FX_QUOTE_GATEWAY, and under useMockData:true this gateway IS FX_QUOTE_GATEWAY. Would have recursed the moment mock FX data was exercised. Fixed by giving the local gateway its own static mock table - the correct home for those numbers now: explicitly labelled dev/mock data, only wired in behind useMockData, never presented as a live rate. 2. currency-convert.pipe.ts memoized its result on (amount, from, to) alone. That was already latently wrong - rates could change via the old setRate() without the pipe re-evaluating for an already-rendered price - but never surfaced because rates never changed mid-session in practice. Async quote loading made it concrete and reproducible: a price rendered before its quote arrived stayed wrong forever, because none of the three cached inputs ever changed again on their own. Fixed with a ratesVersion counter on the service, bumped on every quote arrival, included in the pipe's cache key. Both found and fixed via the E2E suite (docs from the prior commit) actually exercising the real code path: GET /api/v2/pricing/fx-quote intercepted with a contract-shaped response rather than flipping the whole app into mock mode, so the test runs the real FxQuoteApiGateway, not a stand-in for it. Verified: 3/3 E2E green, 115/115 unit tests green, arch:check clean, production build succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:03:02 +04:00
/** Rate relative to RUB, only what this test needs. */
const MOCK_RATE: Record<string, number> = { USD: 0.0108, EUR: 0.0092, AMD: 4.31 };
feat: stand up E2E harness, fix a real bootstrap bug it found Track Q Q1/Q4 (docs/PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md). No E2E existed before this. Playwright chosen - no existing test runner preference, and it needs zero extra infra beyond the dev server this repo already has. - playwright.config.ts, package.json e2e/e2e:ui/e2e:report scripts - e2e/smoke.spec.ts app boots, no console errors (network 404s from the absent backend are filtered - expected, not a bug) - e2e/currency-switch.spec.ts Track Q Q4: switching currency must change the displayed price VALUE, not just the label next to it. Written specifically so the upcoming checkout money-truth rewrite (frontend backlog F10-F16, which replaces client-side FX math with a server-computed total) has a regression net under it before that rewrite starts. The first run found a real, current bug: @marketplaces/auth ships plain tsc output (dist/index.js), not Angular Package Format, so it carries no compiled Ivy DI metadata. Any class-based provider from it - not just the Ed25519 Noop stub, AuthService itself hit the same failure - forces Angular to JIT-compile at runtime, which throws immediately when @angular/compiler isn't loaded. That breaks app bootstrap outright, for real users, not just this test. Fixed here with the minimum honest scope: - src/main.ts: import '@angular/compiler' before bootstrap, so JIT works everywhere the package is injected, not just at one call site - src/app/app.config.ts: useFactory instead of useClass for the Noop Ed25519 provider, since it has zero constructor deps and doesn't need Angular to derive metadata for it at all - angular.json: raised the initial-bundle hard-error budget 1.5MB -> 1.8MB, because the compiler import made a correct build refuse to complete. A build that fails outright is worse than a bundle that's honestly larger than it should be. The real fix belongs in the vitanovaPackages auth repo: publish via ng-packagr so consumers get Ivy-compiled output and none of this is necessary. Do not remove the compiler import until that ships - see the comment left in main.ts. Also fixed a genuine test defect while getting this to a real green: the page renders duplicate .currency-option elements (desktop/mobile variants of the same selector), so the first attempt at this test clicked into a hidden duplicate and silently no-opped. Scoped the click to .currency-dropdown.open and added an explicit poll for the DOM to reflect the new currency before reading it back, rather than trusting a fixed timeout. Verified: 3/3 E2E green, 115/115 unit tests green, arch:check clean, production build succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:46:43 +04:00
test.describe('currency switch', () => {
feat: FX-quote-backed currency conversion, delete admin rate editor F10-F12 of the frontend backlog. Contract: PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §3. Removed the failure mode §5 of that contract exists to close: rates were typed once by an admin into Settings, persisted to localStorage, seeded from a hardcoded DEFAULT_RATES table (USD: 0.011, AMD: 4.3) that never updated and drifted from market. Nothing recorded which rate produced a displayed price or when. - currency-rates.service.ts now fetches through FX_QUOTE_GATEWAY instead of reading admin-typed/localStorage numbers. Stays synchronous at the call site (getRate/convert) - rewriting every consuming template to `| async` is a separate, larger change (F13, not this commit). Before a quote has loaded for a pair, getRate returns 1 rather than a fabricated market rate; isRateReady() lets a caller that cares distinguish the two. ensureFreshQuote() added for checkout to await before charging, per contract §3.2's stale-quote policy. - language.service.ts setCurrency() now triggers a quote fetch instead of just flipping the display signal. - cart.component.ts openPaymentPopup() awaits ensureFreshQuote() before computing the charged amount. - admin-settings-page.* currency-rate editor deleted (F11) - card, component state, and the three orphaned i18n keys it was the only consumer of. Two real bugs surfaced fixing this, neither cosmetic: 1. fx-quote-local.gateway.ts had CurrencyRatesService.convert() as its rate source. That is now circular - CurrencyRatesService depends on FX_QUOTE_GATEWAY, and under useMockData:true this gateway IS FX_QUOTE_GATEWAY. Would have recursed the moment mock FX data was exercised. Fixed by giving the local gateway its own static mock table - the correct home for those numbers now: explicitly labelled dev/mock data, only wired in behind useMockData, never presented as a live rate. 2. currency-convert.pipe.ts memoized its result on (amount, from, to) alone. That was already latently wrong - rates could change via the old setRate() without the pipe re-evaluating for an already-rendered price - but never surfaced because rates never changed mid-session in practice. Async quote loading made it concrete and reproducible: a price rendered before its quote arrived stayed wrong forever, because none of the three cached inputs ever changed again on their own. Fixed with a ratesVersion counter on the service, bumped on every quote arrival, included in the pipe's cache key. Both found and fixed via the E2E suite (docs from the prior commit) actually exercising the real code path: GET /api/v2/pricing/fx-quote intercepted with a contract-shaped response rather than flipping the whole app into mock mode, so the test runs the real FxQuoteApiGateway, not a stand-in for it. Verified: 3/3 E2E green, 115/115 unit tests green, arch:check clean, production build succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:03:02 +04:00
test.beforeEach(async ({ page }) => {
await mockFxQuoteEndpoint(page);
});
feat: stand up E2E harness, fix a real bootstrap bug it found Track Q Q1/Q4 (docs/PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md). No E2E existed before this. Playwright chosen - no existing test runner preference, and it needs zero extra infra beyond the dev server this repo already has. - playwright.config.ts, package.json e2e/e2e:ui/e2e:report scripts - e2e/smoke.spec.ts app boots, no console errors (network 404s from the absent backend are filtered - expected, not a bug) - e2e/currency-switch.spec.ts Track Q Q4: switching currency must change the displayed price VALUE, not just the label next to it. Written specifically so the upcoming checkout money-truth rewrite (frontend backlog F10-F16, which replaces client-side FX math with a server-computed total) has a regression net under it before that rewrite starts. The first run found a real, current bug: @marketplaces/auth ships plain tsc output (dist/index.js), not Angular Package Format, so it carries no compiled Ivy DI metadata. Any class-based provider from it - not just the Ed25519 Noop stub, AuthService itself hit the same failure - forces Angular to JIT-compile at runtime, which throws immediately when @angular/compiler isn't loaded. That breaks app bootstrap outright, for real users, not just this test. Fixed here with the minimum honest scope: - src/main.ts: import '@angular/compiler' before bootstrap, so JIT works everywhere the package is injected, not just at one call site - src/app/app.config.ts: useFactory instead of useClass for the Noop Ed25519 provider, since it has zero constructor deps and doesn't need Angular to derive metadata for it at all - angular.json: raised the initial-bundle hard-error budget 1.5MB -> 1.8MB, because the compiler import made a correct build refuse to complete. A build that fails outright is worse than a bundle that's honestly larger than it should be. The real fix belongs in the vitanovaPackages auth repo: publish via ng-packagr so consumers get Ivy-compiled output and none of this is necessary. Do not remove the compiler import until that ships - see the comment left in main.ts. Also fixed a genuine test defect while getting this to a real green: the page renders duplicate .currency-option elements (desktop/mobile variants of the same selector), so the first attempt at this test clicked into a hidden duplicate and silently no-opped. Scoped the click to .currency-dropdown.open and added an explicit poll for the DOM to reflect the new currency before reading it back, rather than trusting a fixed timeout. Verified: 3/3 E2E green, 115/115 unit tests green, arch:check clean, production build succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:46:43 +04:00
test('switching currency changes the displayed price value, not just its label', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
const priceLocator = page.locator('.current-price, .original-price').first();
await expect(priceLocator).toBeVisible({ timeout: 15_000 });
const before = await readPrice(priceLocator);
expect(before.value, 'a price must be a real positive number before switching').toBeGreaterThan(0);
await switchCurrency(page, before.currency === 'USD' ? 'RUB' : 'USD');
feat: FX-quote-backed currency conversion, delete admin rate editor F10-F12 of the frontend backlog. Contract: PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §3. Removed the failure mode §5 of that contract exists to close: rates were typed once by an admin into Settings, persisted to localStorage, seeded from a hardcoded DEFAULT_RATES table (USD: 0.011, AMD: 4.3) that never updated and drifted from market. Nothing recorded which rate produced a displayed price or when. - currency-rates.service.ts now fetches through FX_QUOTE_GATEWAY instead of reading admin-typed/localStorage numbers. Stays synchronous at the call site (getRate/convert) - rewriting every consuming template to `| async` is a separate, larger change (F13, not this commit). Before a quote has loaded for a pair, getRate returns 1 rather than a fabricated market rate; isRateReady() lets a caller that cares distinguish the two. ensureFreshQuote() added for checkout to await before charging, per contract §3.2's stale-quote policy. - language.service.ts setCurrency() now triggers a quote fetch instead of just flipping the display signal. - cart.component.ts openPaymentPopup() awaits ensureFreshQuote() before computing the charged amount. - admin-settings-page.* currency-rate editor deleted (F11) - card, component state, and the three orphaned i18n keys it was the only consumer of. Two real bugs surfaced fixing this, neither cosmetic: 1. fx-quote-local.gateway.ts had CurrencyRatesService.convert() as its rate source. That is now circular - CurrencyRatesService depends on FX_QUOTE_GATEWAY, and under useMockData:true this gateway IS FX_QUOTE_GATEWAY. Would have recursed the moment mock FX data was exercised. Fixed by giving the local gateway its own static mock table - the correct home for those numbers now: explicitly labelled dev/mock data, only wired in behind useMockData, never presented as a live rate. 2. currency-convert.pipe.ts memoized its result on (amount, from, to) alone. That was already latently wrong - rates could change via the old setRate() without the pipe re-evaluating for an already-rendered price - but never surfaced because rates never changed mid-session in practice. Async quote loading made it concrete and reproducible: a price rendered before its quote arrived stayed wrong forever, because none of the three cached inputs ever changed again on their own. Fixed with a ratesVersion counter on the service, bumped on every quote arrival, included in the pipe's cache key. Both found and fixed via the E2E suite (docs from the prior commit) actually exercising the real code path: GET /api/v2/pricing/fx-quote intercepted with a contract-shaped response rather than flipping the whole app into mock mode, so the test runs the real FxQuoteApiGateway, not a stand-in for it. Verified: 3/3 E2E green, 115/115 unit tests green, arch:check clean, production build succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:03:02 +04:00
// The currency LABEL flips synchronously (a signal write), but the rate
// itself arrives from the mocked network call asynchronously - polling
// only the label races ahead of the actual conversion and passes before
// the number has caught up. Poll the parsed numeric value instead, since
// that is what this test exists to guard.
feat: stand up E2E harness, fix a real bootstrap bug it found Track Q Q1/Q4 (docs/PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md). No E2E existed before this. Playwright chosen - no existing test runner preference, and it needs zero extra infra beyond the dev server this repo already has. - playwright.config.ts, package.json e2e/e2e:ui/e2e:report scripts - e2e/smoke.spec.ts app boots, no console errors (network 404s from the absent backend are filtered - expected, not a bug) - e2e/currency-switch.spec.ts Track Q Q4: switching currency must change the displayed price VALUE, not just the label next to it. Written specifically so the upcoming checkout money-truth rewrite (frontend backlog F10-F16, which replaces client-side FX math with a server-computed total) has a regression net under it before that rewrite starts. The first run found a real, current bug: @marketplaces/auth ships plain tsc output (dist/index.js), not Angular Package Format, so it carries no compiled Ivy DI metadata. Any class-based provider from it - not just the Ed25519 Noop stub, AuthService itself hit the same failure - forces Angular to JIT-compile at runtime, which throws immediately when @angular/compiler isn't loaded. That breaks app bootstrap outright, for real users, not just this test. Fixed here with the minimum honest scope: - src/main.ts: import '@angular/compiler' before bootstrap, so JIT works everywhere the package is injected, not just at one call site - src/app/app.config.ts: useFactory instead of useClass for the Noop Ed25519 provider, since it has zero constructor deps and doesn't need Angular to derive metadata for it at all - angular.json: raised the initial-bundle hard-error budget 1.5MB -> 1.8MB, because the compiler import made a correct build refuse to complete. A build that fails outright is worse than a bundle that's honestly larger than it should be. The real fix belongs in the vitanovaPackages auth repo: publish via ng-packagr so consumers get Ivy-compiled output and none of this is necessary. Do not remove the compiler import until that ships - see the comment left in main.ts. Also fixed a genuine test defect while getting this to a real green: the page renders duplicate .currency-option elements (desktop/mobile variants of the same selector), so the first attempt at this test clicked into a hidden duplicate and silently no-opped. Scoped the click to .currency-dropdown.open and added an explicit poll for the DOM to reflect the new currency before reading it back, rather than trusting a fixed timeout. Verified: 3/3 E2E green, 115/115 unit tests green, arch:check clean, production build succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:46:43 +04:00
await expect
feat: FX-quote-backed currency conversion, delete admin rate editor F10-F12 of the frontend backlog. Contract: PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §3. Removed the failure mode §5 of that contract exists to close: rates were typed once by an admin into Settings, persisted to localStorage, seeded from a hardcoded DEFAULT_RATES table (USD: 0.011, AMD: 4.3) that never updated and drifted from market. Nothing recorded which rate produced a displayed price or when. - currency-rates.service.ts now fetches through FX_QUOTE_GATEWAY instead of reading admin-typed/localStorage numbers. Stays synchronous at the call site (getRate/convert) - rewriting every consuming template to `| async` is a separate, larger change (F13, not this commit). Before a quote has loaded for a pair, getRate returns 1 rather than a fabricated market rate; isRateReady() lets a caller that cares distinguish the two. ensureFreshQuote() added for checkout to await before charging, per contract §3.2's stale-quote policy. - language.service.ts setCurrency() now triggers a quote fetch instead of just flipping the display signal. - cart.component.ts openPaymentPopup() awaits ensureFreshQuote() before computing the charged amount. - admin-settings-page.* currency-rate editor deleted (F11) - card, component state, and the three orphaned i18n keys it was the only consumer of. Two real bugs surfaced fixing this, neither cosmetic: 1. fx-quote-local.gateway.ts had CurrencyRatesService.convert() as its rate source. That is now circular - CurrencyRatesService depends on FX_QUOTE_GATEWAY, and under useMockData:true this gateway IS FX_QUOTE_GATEWAY. Would have recursed the moment mock FX data was exercised. Fixed by giving the local gateway its own static mock table - the correct home for those numbers now: explicitly labelled dev/mock data, only wired in behind useMockData, never presented as a live rate. 2. currency-convert.pipe.ts memoized its result on (amount, from, to) alone. That was already latently wrong - rates could change via the old setRate() without the pipe re-evaluating for an already-rendered price - but never surfaced because rates never changed mid-session in practice. Async quote loading made it concrete and reproducible: a price rendered before its quote arrived stayed wrong forever, because none of the three cached inputs ever changed again on their own. Fixed with a ratesVersion counter on the service, bumped on every quote arrival, included in the pipe's cache key. Both found and fixed via the E2E suite (docs from the prior commit) actually exercising the real code path: GET /api/v2/pricing/fx-quote intercepted with a contract-shaped response rather than flipping the whole app into mock mode, so the test runs the real FxQuoteApiGateway, not a stand-in for it. Verified: 3/3 E2E green, 115/115 unit tests green, arch:check clean, production build succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:03:02 +04:00
.poll(async () => (await readPrice(priceLocator)).value, {
message: 'price value never diverged from the pre-switch amount',
})
.not.toBeCloseTo(before.value, 2);
feat: stand up E2E harness, fix a real bootstrap bug it found Track Q Q1/Q4 (docs/PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md). No E2E existed before this. Playwright chosen - no existing test runner preference, and it needs zero extra infra beyond the dev server this repo already has. - playwright.config.ts, package.json e2e/e2e:ui/e2e:report scripts - e2e/smoke.spec.ts app boots, no console errors (network 404s from the absent backend are filtered - expected, not a bug) - e2e/currency-switch.spec.ts Track Q Q4: switching currency must change the displayed price VALUE, not just the label next to it. Written specifically so the upcoming checkout money-truth rewrite (frontend backlog F10-F16, which replaces client-side FX math with a server-computed total) has a regression net under it before that rewrite starts. The first run found a real, current bug: @marketplaces/auth ships plain tsc output (dist/index.js), not Angular Package Format, so it carries no compiled Ivy DI metadata. Any class-based provider from it - not just the Ed25519 Noop stub, AuthService itself hit the same failure - forces Angular to JIT-compile at runtime, which throws immediately when @angular/compiler isn't loaded. That breaks app bootstrap outright, for real users, not just this test. Fixed here with the minimum honest scope: - src/main.ts: import '@angular/compiler' before bootstrap, so JIT works everywhere the package is injected, not just at one call site - src/app/app.config.ts: useFactory instead of useClass for the Noop Ed25519 provider, since it has zero constructor deps and doesn't need Angular to derive metadata for it at all - angular.json: raised the initial-bundle hard-error budget 1.5MB -> 1.8MB, because the compiler import made a correct build refuse to complete. A build that fails outright is worse than a bundle that's honestly larger than it should be. The real fix belongs in the vitanovaPackages auth repo: publish via ng-packagr so consumers get Ivy-compiled output and none of this is necessary. Do not remove the compiler import until that ships - see the comment left in main.ts. Also fixed a genuine test defect while getting this to a real green: the page renders duplicate .currency-option elements (desktop/mobile variants of the same selector), so the first attempt at this test clicked into a hidden duplicate and silently no-opped. Scoped the click to .currency-dropdown.open and added an explicit poll for the DOM to reflect the new currency before reading it back, rather than trusting a fixed timeout. Verified: 3/3 E2E green, 115/115 unit tests green, arch:check clean, production build succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:46:43 +04:00
const after = await readPrice(priceLocator);
expect(after.currency, 'the currency label must actually change').not.toBe(before.currency);
// The literal regression this test exists to catch: a rate of 1 disguised
// as a real conversion. RUB->USD or USD->RUB is never a 1:1 rate.
expect(after.value, `${before.value} ${before.currency} must not equal ${after.value} ${after.currency}`).not.toBeCloseTo(before.value, 2);
});
test('an out-of-range rate must not silently pass as valid', async ({ page }) => {
// Guards the specific bad-data class this suite exists to catch: a
// conversion that returns something implausible (zero, negative, or
// absurdly large) instead of erroring visibly.
await page.goto('/');
const priceLocator = page.locator('.current-price, .original-price').first();
await expect(priceLocator).toBeVisible({ timeout: 15_000 });
const { value } = await readPrice(priceLocator);
expect(value).toBeGreaterThan(0);
expect(value).toBeLessThan(100_000_000);
});
});
feat: FX-quote-backed currency conversion, delete admin rate editor F10-F12 of the frontend backlog. Contract: PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §3. Removed the failure mode §5 of that contract exists to close: rates were typed once by an admin into Settings, persisted to localStorage, seeded from a hardcoded DEFAULT_RATES table (USD: 0.011, AMD: 4.3) that never updated and drifted from market. Nothing recorded which rate produced a displayed price or when. - currency-rates.service.ts now fetches through FX_QUOTE_GATEWAY instead of reading admin-typed/localStorage numbers. Stays synchronous at the call site (getRate/convert) - rewriting every consuming template to `| async` is a separate, larger change (F13, not this commit). Before a quote has loaded for a pair, getRate returns 1 rather than a fabricated market rate; isRateReady() lets a caller that cares distinguish the two. ensureFreshQuote() added for checkout to await before charging, per contract §3.2's stale-quote policy. - language.service.ts setCurrency() now triggers a quote fetch instead of just flipping the display signal. - cart.component.ts openPaymentPopup() awaits ensureFreshQuote() before computing the charged amount. - admin-settings-page.* currency-rate editor deleted (F11) - card, component state, and the three orphaned i18n keys it was the only consumer of. Two real bugs surfaced fixing this, neither cosmetic: 1. fx-quote-local.gateway.ts had CurrencyRatesService.convert() as its rate source. That is now circular - CurrencyRatesService depends on FX_QUOTE_GATEWAY, and under useMockData:true this gateway IS FX_QUOTE_GATEWAY. Would have recursed the moment mock FX data was exercised. Fixed by giving the local gateway its own static mock table - the correct home for those numbers now: explicitly labelled dev/mock data, only wired in behind useMockData, never presented as a live rate. 2. currency-convert.pipe.ts memoized its result on (amount, from, to) alone. That was already latently wrong - rates could change via the old setRate() without the pipe re-evaluating for an already-rendered price - but never surfaced because rates never changed mid-session in practice. Async quote loading made it concrete and reproducible: a price rendered before its quote arrived stayed wrong forever, because none of the three cached inputs ever changed again on their own. Fixed with a ratesVersion counter on the service, bumped on every quote arrival, included in the pipe's cache key. Both found and fixed via the E2E suite (docs from the prior commit) actually exercising the real code path: GET /api/v2/pricing/fx-quote intercepted with a contract-shaped response rather than flipping the whole app into mock mode, so the test runs the real FxQuoteApiGateway, not a stand-in for it. Verified: 3/3 E2E green, 115/115 unit tests green, arch:check clean, production build succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:03:02 +04:00
async function mockFxQuoteEndpoint(page: Page): Promise<void> {
await page.route('**/api/v2/pricing/fx-quote**', (route: Route) => {
const url = new URL(route.request().url());
const base = url.searchParams.get('base') ?? 'RUB';
const quote = url.searchParams.get('quote') ?? 'USD';
const rate = MOCK_RATE[quote] ?? 1;
const now = new Date();
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
quoteId: `fxq_e2e_${base}_${quote}_${now.getTime()}`,
base,
quote,
rate,
source: 'e2e-fixture',
observedAt: now.toISOString(),
expiresAt: new Date(now.getTime() + 5 * 60 * 1000).toISOString(),
}),
});
});
}
async function switchCurrency(page: Page, targetCode: string): Promise<void> {
feat: stand up E2E harness, fix a real bootstrap bug it found Track Q Q1/Q4 (docs/PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md). No E2E existed before this. Playwright chosen - no existing test runner preference, and it needs zero extra infra beyond the dev server this repo already has. - playwright.config.ts, package.json e2e/e2e:ui/e2e:report scripts - e2e/smoke.spec.ts app boots, no console errors (network 404s from the absent backend are filtered - expected, not a bug) - e2e/currency-switch.spec.ts Track Q Q4: switching currency must change the displayed price VALUE, not just the label next to it. Written specifically so the upcoming checkout money-truth rewrite (frontend backlog F10-F16, which replaces client-side FX math with a server-computed total) has a regression net under it before that rewrite starts. The first run found a real, current bug: @marketplaces/auth ships plain tsc output (dist/index.js), not Angular Package Format, so it carries no compiled Ivy DI metadata. Any class-based provider from it - not just the Ed25519 Noop stub, AuthService itself hit the same failure - forces Angular to JIT-compile at runtime, which throws immediately when @angular/compiler isn't loaded. That breaks app bootstrap outright, for real users, not just this test. Fixed here with the minimum honest scope: - src/main.ts: import '@angular/compiler' before bootstrap, so JIT works everywhere the package is injected, not just at one call site - src/app/app.config.ts: useFactory instead of useClass for the Noop Ed25519 provider, since it has zero constructor deps and doesn't need Angular to derive metadata for it at all - angular.json: raised the initial-bundle hard-error budget 1.5MB -> 1.8MB, because the compiler import made a correct build refuse to complete. A build that fails outright is worse than a bundle that's honestly larger than it should be. The real fix belongs in the vitanovaPackages auth repo: publish via ng-packagr so consumers get Ivy-compiled output and none of this is necessary. Do not remove the compiler import until that ships - see the comment left in main.ts. Also fixed a genuine test defect while getting this to a real green: the page renders duplicate .currency-option elements (desktop/mobile variants of the same selector), so the first attempt at this test clicked into a hidden duplicate and silently no-opped. Scoped the click to .currency-dropdown.open and added an explicit poll for the DOM to reflect the new currency before reading it back, rather than trusting a fixed timeout. Verified: 3/3 E2E green, 115/115 unit tests green, arch:check clean, production build succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:46:43 +04:00
// The page renders more than one language-selector instance (desktop/mobile
// variants share the same markup) - scoping to the dropdown that actually
// carries the "open" class avoids clicking an option in a hidden duplicate,
// which is silently a no-op rather than a failure.
const trigger = page.locator('.currency-button:visible').first();
await trigger.click();
const openDropdown = page.locator('.currency-dropdown.open').first();
await expect(openDropdown).toBeVisible();
await openDropdown.locator('.currency-option', { hasText: targetCode }).first().click();
}
async function readPrice(locator: import('@playwright/test').Locator): Promise<{ value: number; currency: string }> {
const text = (await locator.textContent()) ?? '';
// Matches "1 234.56 USD" / "1234.56 ₽" shapes the price templates render.
const match = text.replace(/\s/g, '').match(/([\d.,]+)([A-Z]{3}|\D+)$/);
if (!match) {
throw new Error(`could not parse price text: "${text}"`);
}
const value = Number(match[1].replace(/,/g, ''));
return { value, currency: match[2] };
}